vue是雙向數據綁定的框架,數據驅動是他的靈魂,他的實現原理衆所周知是Object.defineProperty方法實現的get、set重寫,可是這樣說太牽強外門了。本文將宏觀介紹他的實現javascript
舉個很是簡單的栗子html
# html <div id="#app"> {{msg}} </div> # script <script> new Vue({ el: '#app', data: { msg: 'hello' }, mounted() { setTimeout(() => { this.msg = 'hi' }, 1000); } }) </script>
上面代碼, new Vue進行建立vue對象, el屬性是掛載的dom選擇器,這裏選擇id爲app的dom,data對象保存這全部數據響應的屬性,當其中的某一屬性值改變,就觸發view渲染,從而實現了「數據->視圖」的動態響應;vue
示例中msg初始值爲hello,所以頁面渲染時爲hello,一秒以後,msg變爲了hi,觸發了view渲染,咱們看到hello變爲了li。那麼接下來就從這簡單的栗子來說解vue的數據驅動把。java
咱們說vue是怎麼實現雙向數據綁定的?是Object.defineProperty實現了,那麼咱們就直接聚焦Object.defineProperty
node
如下是代碼react
function defineReactive ( obj, key, val, customSetter, shallow ) { // 建立派發器 var dep = new Dep(); var property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return } // cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; if ((!getter || setter) && arguments.length === 2) { val = obj[key]; } var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val; // 收集依賴對象 if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val; /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if ("development" !== 'production' && customSetter) { customSetter(); } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); dep.notify(); } }); }
vue在給每個data的屬性執行defineReactive函數,來達到數據綁定的目的。從代碼中能夠看到幾點:面試
dep.depend()
操做;dep.notify()
操做;這一部分很容易瞭解,在data的屬性get時,觸發了派發器的依賴收集(dep.depend),在data的屬性set時,觸發了派發器的事件通知(dep.notify);express
結合已知知識,Vue的數據綁定是上面這個函數帶來的反作用,所以能夠得出結論:bash
上一節已經肯定,當更改屬性值時,是Dep.target.update更新了view,所以帶着這個目的,此小節作一個簡單的源碼解析app
function Vue (options) { this._init(options); } Vue.prototype._init = function (options) { var vm = this; callHook(vm, 'beforeCreate'); initState(vm); callHook(vm, 'created'); if (vm.$options.el) { vm.$mount(vm.$options.el); } }; function initState (vm) { vm._watchers = []; var opts = vm.$options; if (opts.data) { initData(vm); } else { observe(vm._data = {}, true /* asRootData */); } } function initData (vm) { var data = vm.$options.data; observe(data, true /* asRootData */); } function observe (value, asRootData) { if (!isObject(value) || value instanceof VNode) { return } var ob = new Observer(value);; return ob }
從頭開始,一步一步進入,發現最終咱們new Vue傳進來的data進入了new Observer中;
var Observer = function Observer (value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, '__ob__', this); if (Array.isArray(value)) { var augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys); this.observeArray(value); } else { this.walk(value); } }; Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]); } };
Observer構造函數中,最終執行了defineReactive爲每個屬性進行定義,而且是遞歸調用,以樹型遍歷咱們傳入的data對象的全部節點屬性,每個節點都會被包裝爲一個觀察者,當數據get時,進行依賴收集,當數據set時,事件分發。
看到這裏,感受好像少了點什麼,好像data到這裏就結束了,可是並無看懂爲何數據改變動新視圖的,那麼繼續往下看
回看一切從頭開始的_init方法,在這個方法中,最後調用了vm.$mount(vm.$options.el)
,這是把vm掛載到真實dom,並渲染view的地方,所以接着看下去。
Vue.prototype.$mount = function ( el, hydrating ) { return mountComponent(this, el, hydrating) }; // 渲染dom的真實函數 function mountComponent ( vm, el, hydrating ) { vm.$el = el; callHook(vm, 'beforeMount'); var updateComponent; updateComponent = function () { vm._update(vm._render(), hydrating); }; // new 一個Watcher,開啓了數據驅動之旅 new Watcher(vm, updateComponent, noop, { before: function before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate'); } } }, true /* isRenderWatcher */); hydrating = false; if (vm.$vnode == null) { vm._isMounted = true; callHook(vm, 'mounted'); } return vm }
上面部分看到的是,vue將vue對象掛載到真實dom的經歷,最終執行了new Watcher,而且回調爲vm._update(vm._render(), hydrating)
。顧名思義,這裏是執行了vue的更新view的操做(本文暫且不講更新view,在其餘文章已經講過。本文專一數據驅動部分)。
問:爲何說new Watcher開啓了數據驅動之旅呢?Watcher又是什麼功能?
若是說Object.defineProperty是vue數據驅動的靈魂,那麼Watcher則是他的骨骼。
// 超級簡單的Watcher var Watcher = function Watcher ( vm, expOrFn, cb, options, isRenderWatcher ) { this.cb = cb; this.deps = []; this.newDeps = []; // 計算屬性走if if (this.computed) { this.value = undefined; this.dep = new Dep(); } else { this.value = this.get(); } }; Watcher.prototype.get = function get () { pushTarget(this); var value; var vm = this.vm; try { value = this.getter.call(vm, vm); } catch (e) { if (this.user) { handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\"")); } else { throw e } } finally { popTarget(); this.cleanupDeps(); } return value };
簡化後Watcher在new時,最終會調用本身的get方法,get方法中第一個語句pushTarget(this)
是開啓數據驅動的第一把鑰匙,看下文
function pushTarget (_target) { if (Dep.target) { targetStack.push(Dep.target); } Dep.target = _target; }
pushTarget將傳入的Watcher對象賦值給了Dep.target,還記得在講Object.defineProperty時提到了,Dep.target.update是更新view的觸發點,在這裏終於找到了!
下面看Dep.targe.update
Watcher.prototype.update = function update () { var this$1 = this; /* istanbul ignore else */ if (this.computed) { if (this.dep.subs.length === 0) { this.dirty = true; } else { this.getAndInvoke(function () { this$1.dep.notify(); }); } } else if (this.sync) { this.run(); } else { // update執行了這裏 queueWatcher(this); } };
咱們看到update方法最後執行了queueWatcher,繼續看下去發現,這實際上是一個更新隊列,vue對同一個微任務的全部update進行了收集更新,最終執行了watcher.run,run方法又執行了getAndInvoke
方法,getAndInvoke又執行了this.get
方法。
到來一大圈,終於找到:在改變屬性值時,觸發了Dep.target所對應的Watcher的 this.get
方法,this.get方法其實就是傳入進來的回調函數。回想前面介紹的,vue在掛載到真實dom時,new Watcher傳入的回調是updateComponent。串聯起來獲得告終論:
this.get
,這時候view會更新。到這裏,有沒有明白爲何全部屬性的派發器都會收集updateComponent的Watcher,從而在本身set時通知更新?若是沒明白,那就看下一節分析
this.get
初始化一次值,對標updateComponent函數,這個時候會觸發vue渲染過程vue數據驅動是有前提條件的,不是怎麼用均可以的,前提條件就是必須在data中聲明的屬性纔會參與數據驅動,數據->視圖。看下面栗子
有以下html:
<div id="app"> <div>{{prev}}{{next}}</div> </div>
以下js:
new Vue({ el: "#app", data: { prev: 'hello', }, created() { }, mounted() { this.next = 'world'; } })
頁面渲染的結果是什呢?
答:hello;
爲何this.next明明賦值,沒有渲染到view中去?由於他並無參與數據驅動的觀察者,還記得前面講到vue會把傳入的data對象深度遍歷包裝爲觀察者來吧,這裏next屬性並無被成爲觀察者,所以並不會引起view更新。
爲何看到的未必真實的,上面的栗子咱們發現,view中看到的只有hello,可是數據真的是有hello麼?未必,看下面栗子。
new Vue({ el: "#app", data: { prev: 'hello', }, created() { }, mounted() { this.next = 'world'; setTimeout(() => { this.prev = 'hi'; }, 1000); } })
這個代碼比上面栗子就多了3行代碼,再頁面渲染1秒後,改變prev的值爲hi,那麼頁面會展示什麼效果呢?
答:hi world
從這裏能夠看到,雖然next賦值並無引發view更新,可是data確實成功變動了,當prev改變時,觸發了update,從而將view更新,此時next有值,所以就顯示在了view中。這就是不少初學者會遇到爲何明明賦值沒有顯示,可是點了一下其餘的東西,卻顯示了的問題。
仍是根據第一個栗子引伸一個案例以下:
new Vue({ el: "#app", data: { prev: 'hello', }, created() { this.next = 'world'; }, mounted() { setTimeout(() => { this.next = 'memory' }, 1000) } })
咱們在created生命週期中賦值next,在mounted生命週期延遲一秒改變next的值,那結果會這樣?
答:永遠顯示helloworld
若是已經掌握了vue實例化過程的同窗可能已經猜到了爲何
當created生命週期執行時,此時尚未作vnode轉化爲真實dom的操做,此時data屬性已經代理到this下,所以修改this.next就修改了data對象的值,data就變爲了{prev: 'hello', next: 'world'}
,所以在render時就將next也渲染到了頁面上
另外此時已經完成了數據驅動的靈魂步驟(將data遍歷包裝爲觀察者),所以在延遲1s後改變next值,仍然跟栗子2同樣不會引發view更新的。
所以,寫vue出現以上改變data時view未更新,首先要檢查本身的代碼,而不是懷疑vue框架的問題。。
有些面試官會問在異步獲取數據並改變data值時,放在created仍是mounted?
我感受沒什麼可答的,2個都沒問題,固然對於代碼優化,放在created更早的發出請求,所以放在created裏更合適。