上一篇大概解释了双向绑定实现原理,可是没有例子,因此可能看起来比较很差理解,vue
这一篇作为补充,会用一个例子来说解实现过程,首先上例子express
<template> <div id="app" class="app"> <label>姓名:</label>
<input v-model="name"> <!--<input :value="name" @input="name = $event.target.value">-->
<select v-model="sex"> <option value="1">男</option> <option value="0">女</option> </select> <p>{{infoName}}</p> <p>{{infoSex}}</p> </div> </template> <script> export default { data(){ return { name:"", sex:1, } }, computed:{ infoName(){ return "您的姓名是:" + this.name; }, infoSex(){ return "您的性别是:" + (this.sex == 1 ? "男":"女")); } }, watch:{ sex:function(newSex){ alert("你的性别已经改成"+ this.sex == 1 ? "男":"女"); }, name:function(newName){ alert("你的名字已经改成"+ newName); } }, } </script>
而后下面再上一个上面编译后的对应的render函数代码:数组
module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h; return _c('div', { staticClass: "app", attrs: { "id": "app" } }, [_c('label', [_vm._v("姓名:")]), _vm._v(" "), _c('input', { domProps: { "value": _vm.name }, on: { "input": function($event) { _vm.name = $event.target.value } } }), _vm._v(" "), _c('select', { directives: [{ name: "model", rawName: "v-model", value: (_vm.sex), expression: "sex" }], on: { "change": function($event) { var $$selectedVal = Array.prototype.filter.call($event.target.options, function(o) { return o.selected }).map(function(o) { var val = "_value" in o ? o._value : o.value; return val }); _vm.sex = $event.target.multiple ? $$selectedVal : $$selectedVal[0] } } }, [_c('option', { attrs: { "value": "1" } }, [_vm._v("男")]), _vm._v(" "), _c('option', { attrs: { "value": "0" } }, [_vm._v("女")])]), _vm._v(" "), _c('p', [_vm._v(_vm._s(_vm.infoName))]), _vm._v(" "), _c('p', [_vm._v(_vm._s(_vm.infoSex))])]) },staticRenderFns: []}
当上面组件在建立以后,调用$mount方法进行挂载的时候,会首先建立vm对应的watcher,缓存
在watcher的构造函数实现中,最后会调用一次get方法去初始化value值,而get方法对应的方法为render(简化以后),代码为:app
var updateComponent = function () { vm._update(vm._render(), hydrating); }; vm._watcher = new Watcher(vm, updateComponent, noop);
注:1.update方法是更新组件的方法,这个和vDom(虚拟dom)相关,这里忽略它,就认为render就是更新组件的方法dom
2.其实render只是返回一个vNode,并非真正的更新渲染实现函数
而render方法 就是上面编译后的实现,每一个文件最后都会把template里面的内容编译成一个render方法oop
当运行这个render方法的时候,你们能够看到会去调用_vm.name,_vm.sex,_vm.infoName,_vm.infoSex得到值,性能
而这些属性值,在vm这个对象里面,其实只是一个代理,在调用initSate的时候初始化data方法里面返回的对象,并设置代理到vm,优化
而computed里面的属性值,是在建立这个vm对应的构造函数的时候,进行初始化并设置代理到vm的,其方法是 Vue.extend
_vm.name 实际上是多级代理:首先返回this[sourceKey][key], 这里sourceKey为_data,key为name因此实际上是返回_vm._data.name,
而_vm._data.name 会调用上一篇说到的Object.defineProperty定义的get方法,
这个时候,由于Dep.target不为空(由于这个时候是运行watcher的get方法,因此Dep.target为当前watcher),那么name 属性对应的dep就和这个watcher
创建的关联了,sex同理,
这里 说说 vm和infoName设置代理的过程,先上代码:
Vue.extend = function (extendOptions: Object): Function { extendOptions = extendOptions || {} const Super = this const SuperId = Super.cid const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}) if (cachedCtors[SuperId]) { return cachedCtors[SuperId] } const Sub = function VueComponent (options) { this._init(options) } Sub['super'] = Super if (Sub.options.computed) { initComputed(Sub) } // cache constructor cachedCtors[SuperId] = Sub return Sub } } function initComputed (Comp) { const computed = Comp.options.computed for (const key in computed) { defineComputed(Comp.prototype, key, computed[key]) } } export function defineComputed (target: any, key: string, userDef: Object | Function) { if (typeof userDef === 'function') { sharedPropertyDefinition.get = createComputedGetter(key) sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? userDef.cache !== false ? createComputedGetter(key) : userDef.get : noop sharedPropertyDefinition.set = userDef.set ? userDef.set : noop } Object.defineProperty(target, key, sharedPropertyDefinition) } function createComputedGetter (key) { return function computedGetter () { var watcher = this._computedWatchers && this._computedWatchers[key]; if (watcher) { if (watcher.dirty) { watcher.evaluate(); } if (Dep.target) { watcher.depend(); } return watcher.value } } }
注: 1.extend方法里面的实现删除了不少,这个方法是用来继承基本vue组件类并返回一个构造函数,而且会缓存起来,当下次调用的时候就直接返回而不用再初始化
2. 咱们须要关注的执行顺序是 extend->initComputed->defineComputed->createComoputederGetter
经过实现能够看出,当这个组件在第一次建立的时候,会利用Vue.extend建立其对应的构造函数,而extend方法会初始化他对应的computed,而后对computed对象里面的每一个属性设置一个代理,
当调用vm.infoName的时候,其实的获取 watcher对应的value值,
咱们在看看上面的执行过程,建立vm -> 建立对应的watcher -> 执行render -> 调用_vm.infoName -> 调用computedGetter方法,
而根据 上一篇 computed属性建立对应的watcher过程,每一个属性都有一个对应的watcher,而且会用key作对应,并且这个watcher的dirty为true,在初始化的时候也没有
调用对应的get方法,这个时候由于dirt为true,因此为调用watcher的evaluate强行计算一次,而后dirty为false,之后若是对应的dep的对应的data值没有改变,那么就会一直用这个value值
而再也不从新计算,当涉及到的data值,好比在这个里面就是name值改变的时候,会通知watcher进行更新,而watcher会把dirty再次设置为true,这个时候再调用infoName的时候就会从新计算
上面就是初始化的过程当中创建关联的过程,
而当咱们在name对应的input的进行输入的时候,这个时候触发绑定的input事件,会执行 name = $event.target.value 的操做
这个时候最终会执行Object.defineProperty定义的get方法,这个时候确定不会和以前的value值相同,由于初始化的value值为"",而新的值确定不会为"",
会调用dep.notify通知watcher方法更新,执行过程代码为:
dep.notify() notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i >= 0 && queue[i].id > watcher.id) { i-- } queue.splice(Math.max(i, index) + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true nextTick(flushSchedulerQueue) } } } function flushSchedulerQueue () { flushing = true let watcher, id, vm // Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child) // 2. A component's user watchers are run before its render watcher (because // user watchers are created before the render watcher) // 3. If a component is destroyed during a parent component's watcher run, // its watchers can be skipped. queue.sort((a, b) => a.id - b.id) // do not cache length because more watchers might be pushed // as we run existing watchers for (index = 0; index < queue.length; index++) { watcher = queue[index] id = watcher.id has[id] = null watcher.run() } }
咱们须要关注的执行流程为: dep.notify -> watcher.update -> queueWatcher -> nextTick -> flushSchedulerQueue
在watcher.update 方法里面 能够看到 有3个分支,第一: 若是lazy 那么就是 只设置为dirty为true,等待获取value值的时候再去从新计算
第二:若是 sync为true 那么就当即执行run,
第三:把这个watcher加入到scheduler(调度者)的更新队列中去,若是当前没有执行更新,那么加入数组,等待nextTick(下一个时间片)去更新,
若是当前正在执行更新,那么就会根据id作一个从小到大的排序,更小的id老是更先更新,由于从id的产生来讲,父节点的id老是比子节点的id小,
nextTick的源码实现由于有点多,并且和此话题关系不是很重,我大概讲一下nextTick的实现就好了
nextTick方法的主要做用 加入一个function,这个function 会在下一个时间片执行,具体更具环境不一样而利用不一样实现,
优先使用Promise,其次使用MutationObserver,若是上述两种方式都不能使用,那么就使用setTimeout(functionName,0)的方式来实现
这个方法实现了,讲多个watcher的更新集中到一块儿实现一次更新,而避免每一个watcher更新都直接当即更新,由于watcher的关联性很大,并且有不少dep对应的watcher多是
同一个watcher,这样能够避免避免重复更新,从而大大的优化了性能,好比上面的列子,当我同时改变name 和sex的时候,其实他们的watcher都是同样,都是vm对应的那个watcher,
那么若是name改变的时候实行一次render,sex改变的时候又执行一次render,那就有点可怕了,固然这又会照成一个影响,就是我改变了数据以后,dom没有当即改变,这个时候咱们能够
利用vue2 提供的nextTick方法来实现咱们的需求
在下一个时间片,会调用flushSchedulerQueue方法,首先对队列里面的watcher按照id进行升序排序,而后循环队列,调用watcher.run,而后再运行get方法进行更新
上面就是整个通知更新执行过程了
总的归纳过程就是, 当调用watcher的get方法进行计算的时候, 获取data的数据即调用get方法,这个时候会进行进行关联,
当数据改变的时候执行set方法,会通知dep相关了的watcher进行更新