上一篇大概解釋了雙向綁定實現原理,可是沒有例子,因此可能看起來比較很差理解,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進行更新