Vue 如日中天,几乎每一个Web 开发者都知道Vue 是通过Object.defineProperty 对数据劫持以达到响应式处理,通过发布订阅模式进行事件处理,通过key 确保元素状态复用...
针对这些知识点,通过一个例子进行梳理。俗话说,“光练不说傻把式”,那么进行一次自我检阅。
(以下内容结合vue v2.6.11阅读)
<div id="app"> <div> <input type="text" v-model="name" /> <my-button text="添加" @click="add" /> </div> <ul> <li v-for="(item, i) in showList" :key="i"> <input type="checkbox"> {{ item.name }} </li> </ul> </div> <script> const app = new Vue({ el: '#app', components: { MyButton: { template: `<button @click="$emit('click')">{{ text }}</button>`, props: { text: String, }, }, }, data: { name: '', newId: 3, list: [ Object.freeze({ id: 1, name: '李斯' }), Object.freeze({ id: 2, name: '吕不韦' }), Object.freeze({ id: 3, name: '嬴政' }), ], }, computed: { showList() { return this.list.filter(v => v.id <= 5); }, }, methods: { add() { if (!this.name) return; this.list.unshift(Object.freeze({ id: ++this.newId, name: this.name })); this.name = ''; } } }); </script> 复制代码
Vue 在初始化阶段做了挺多事情,配置合并,初始化生命周期相关信息,初始化事件中心,初始化data,初始化props,初始化computed,初始化watcher。本文只分析例子涉及的关键点。
初始化data 的调用栈new Observer(value)
←observe
←initData
数据监听的调用栈defineReactive(obj, keys[i])
←walk(value)
←new Observer(value)
在defineReactive
内还会对值进行观测observe(obj[key])
递归下来,就能深度监听data对象。
本文只分析value 是data.list的过程。
由于Object.defineProperty 不能够监听数组下标,所以Vue 是通过hack,重写所有能改变数组自身的方法,比如push,pop,先执行原逻辑函数,如果是往数组新增元素,则把新增元素变成响应式。
再遍历data.list的元素,执行new Observer(value)
,此时value 为数组元素,遍历元素属性,执行defineReactive(obj, keys[i])
为每一个元素的属性添加getter setter,例子中的元素被Object.freeze()处理,不会对子元素处理。
遍历computed 对象,对每一个computed 属性实例化watcher,其实computed 就是computed watcher。
// https://github.com/vuejs/vue/blob/v2.6.11/src/core/instance/state.js#L187 new Watcher( vm, getter || noop, noop, computedWatcherOptions // { lazy: true } ) 复制代码
重新定义computed 的getter
// https://github.com/vuejs/vue/blob/v2.6.11/src/core/instance/state.js#L241 function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value } } } 复制代码
Vue 有Runtime only 版本和Runtime + complier版本。
对于基于webpack 的工程,只需要Runtime,通过vue-loader 和vue-template-compiler 将template 预编译为渲染函数,避免运行时编译开销。
编译调用栈generate
←createFunction
←compileToFunctions
←$mount
// options.render = render function() { with(this){return _c('div',{attrs:{"id":"app"}},[_c('div',[_c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}}),_v(" "),_c('my-button',{attrs:{"text":"添加"},on:{"click":add}})],1),_v(" "),_c('ul',_l((showList),function(item,i){return _c('li',{key:i},[_c('input',{attrs:{"type":"checkbox"}}),_v(" "+_s(item.name)+"\n ")])}),0)])} } 复制代码
首次渲染调用栈createElm
←patch
←vm.__patch__
←vm._update
←mountComponent
←vm.$mount
// https://github.com/vuejs/vue/blob/v2.6.11/src/core/instance/lifecycle.js#L197 updateComponent = () => { vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) 复制代码
实例化一个渲染watcher,在get
内执行watcher getter 也就是updateComponent。
关键的一个点,执行pushTarget(this)
,将渲染watcher 储存到全局Dep.target。
export function pushTarget (target: ?Watcher) { targetStack.push(target) Dep.target = target } 复制代码
执行vm._render()
,递归生成VNode Tree。
// https://github.com/vuejs/vue/blob/v2.6.11/src/core/instance/render.js#L91 vnode = render.call(vm._renderProxy, vm.$createElement) 复制代码
render 就是经过编译得到的匿名函数,执行函数,此时获取data属性,触发属性的getter,此数据就收集了订阅者渲染watcher,这个过程也就是依赖收集。
当渲染模版读取computed 时,触发getter,执行回调,此时watcher.dirty
为true
,执行watcher.evaluate()
,先执行pushTarget(this)
,这时全局Dep.target 指向computed watcher。又触发了data.list 的getter,此时data.list 的收集器就能收集到computed watcher,且computed watcher 的deps也保存有 data.list 的依赖收集器 dep。
// data.list.getter() ← computedWatcher.get() ← computedWatcher.evaluate() targetStack = [renderWatcher, computendWatcher] Dep.target = computedWatcher dataListDep = { subs: [computed watcher] } computedWatcher = { deps: [dataListDep] } // popTarget targetStack = [renderWatcher] Dep.target = renderWatcher computedWatcher.dirty = false 复制代码
接着执行watcher.depend()
,data.list 的收集器就能收集到渲染watcher。
// dep.addSub() ← Dep.target.addDep() ← dep.depend() ← watcher.depend() // 遍历computedWatcher = { deps: [dataListDep] } // 执行dataListDep.depend() // 执行Dep.target.addDep(this) // 执行dataListDep.addSub(this) dataListDep = { subs: [computedWatcher, renderWatcher] } 复制代码
执行vm._update
,递归遍历createElm
,通过先子后父的插入顺序将Vnode 渲染成DOM。
如果是组件,会走createComponent
创建组件调用栈updateComponent
←Vue.$mount
←Vue._init
←new Vue.extend(options)
←createComponentInstanceForVnode
←init
←createComponent
又回到渲染调用栈。最后挂载到#app的父节点即body上,再移除旧节点。
patch
是createPatchFunction
柯里化返回的函数,通过柯里化抹平平台差异,无须没次调用传入参数。
除此之外,还将元素的钩子函数聚合在一起。元素事件,属性,样式的设置更新都是通过这些钩子完成。
本文跳过v-model 的实现,重点在click。
在构造组件VNode的时候,会把新建一个参数listeners,指向data.on,并作为options 属性,去实例化VNode。
在组件渲染成dom进行组件初始化的时候,调用初始化事件中心,将自定义事件传递给子组件实例,存储在vm._events
。
// createComponent ← createElement() ← vm._render() listeners = data.on = { "click": this.add } componentVnode.componentOptions.listeners = { "click": this.add } // updateListeners() ... ← initEvent() ← Vue._init() ← ... ← createComponent ← ... ← vm._update() // createComponentInstanceForVnode new vnode.componentOptions.Ctor({ ..., _parentVnode: vnode }) // initInternalComponent vm.$options._parentListeners = options._parentVnode.componentOptions.listeners // initEvents updateComponentListeners(vm, listeners) // ... vm.$on('click', this.add) // ... vm._events['click'] = [this.add] 复制代码
在patch
过程触发元素的钩子函数create
,进行事件的绑定。
在dom 插入父节点前会执行invokeCreateHooks
。
// updateListeners ← updateDOMListeners ← invokeCreateHooks(vnode, insertedVnodeQueue) vnode.elm.addEventListener('click', this.$emit('click')); 复制代码
现在分析,勾选李斯,输入框输入张三,点击button,这个更新过程。
涉及v-model,简单介绍一下。实际上v-model是一个语法糖,一个指令,在编译过程,针对不同标签生成不同事件和属性,通过composing 细节处理混合输入法的抬手问题。例如<input type="radio" />
生成change 事件和 checked 属性,<input type="text" />
生成 input 事件和 value 属性。 这就是Vue的双向绑定原理。
当点击button,触发click 回调,通过MyButton.$emit('click')
派发事件,执行MyButton._event['click']
存储的所有函数。这时定义在父组件的add
被触发,执行this.list.unshift
,触发Vue 子定义的方法,执行dataList.__ob__.dep.notify()
,触发订阅者更新。
[computedWatcher, renderWatcher].forEach(sub => sub.update) // computedWatcher.update() computedWatcher.dirty = true // renderWatcher.update() queueWatcher(renderWatcher) function queueWatcher(watcher) { if (has[id] == null) { ... queue.push(watcher) } } // 这里涉及更新队列优化,比如,在这个例子中 // this.list.unshift 会触发renderWatcher.update // this.name = '' 也会触发renderWatcher.update // 同一个renderWatcher,所以只会添加一次 queue = [renderWatcher] nextTick(flushSchedulerQueue); // 使用nextTick,更新队列将在下一个Tick执行 // 下一个Tick就是当主线程为空,从任务队列读取任务到调用栈中等待主线程执行。 // patchVnode() ← vm.__patch__() ← ... ← vm._update(vm._render(), hydrating) // ← updateComponent()(renderWatcher.getter()) ← renderWatcher.run() // ← flushSchedulerQueue() // 在执行vm.render()的时候,触发computed 的getter,重新计算返回新值 // 这个过程又会触发dataList 的getter,进行新一路的依赖收集,流程还是一样 // 所以说computed 是惰性的(缓存的),只有真正去获取值的时候才会执行计算 复制代码
Vue 的patchVnode
逻辑通过判断节点类型进行更新:
通过递归的方式完成整棵VNode 树的更新。
以updateCHildren 为主线,分析key 的作用。
当对ul 新旧VNode 进行比对时,去到updateChildren 分支,通过伪代码的形式分析。
VNode: [{ tag: 'li', key: 0, children: [{ tag: 'input', elm: { value: 'on' } }, { text: '李斯' }]}, ...] newVNode: [{ tag: 'li', key: 0, children: [{ tag: 'input': elm: undefined }, { text: '张三' }]}, ...] // 当对新旧liVnode 比对时,判断key tag 相等,patchVnode(inputVNode, newInputVNode) // 当对新旧inputVnode 比对时,判断key tag inputType 相等,patchVnode,复用节点,造成错误渲染 var elm = vnode.elm = oldVnode.elm; // 把模版li 标签的id 更换为item.id VNode: [{ tag: 'li', key: 1, children: [{ tag: 'input', elm: { value: 'on' } }, { text: '李斯' }]}, ...] newVNode: [{ tag: 'li', key: 4, children: [{ tag: 'input': elm: undefined }, { text: '张三' }]}, { tag: 'li', key: 1, children: [{ tag: 'input', elm: { value: 'on' } }, { text: '李斯' }]}, ...] // 简化一下 VNode: [a, b, c] newVNode: [d, a, b, c] oldStartIdx = 0 newStartIdx = 0 oldEndIdx = 2 newEndIdx = 3 oldStartVnode = a oldEndVnode = c newStartVnode = d newEndVnode = c while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 进行首首对比same(a, d)不符合,进行尾尾对比same(c, c),复用节点 oldEndIdx = 1 oldEndVnode = b newEndIdx = 2 newEndVnode = b // 进行首首对比same(a, d)不符合,进行尾尾对比same(b, b),复用节点 oldEndIdx = 0 oldEndVnode = a newEndIdx = 1 newEndVnode = a // 进行首首对比same(a, d)不符合,进行尾尾对比same(a, a),复用节点 oldEndIdx = -1 oldEndVnode = undefined newEndIdx = 0 newEndVnode = d } newStartIdx = 0 newEndIdx = 0 addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); // 满足条件,创建新元素并插入 createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx); 复制代码
此次梳理只是针对例子,整体流程走了一遍,很多细节上的内容并没有展开,感兴趣可以针对某一模块进行单步调试加深理解。
通过以上流程可以梳理出几个知识点: