分析vue是如何實作資料響應的.
現在回顧一下看資料響應的原因. 之前看了vuex和vue-i18n的源碼, 他們都有自己内部的vm, 也就是vue執行個體. 使用的都是vue的響應式資料特性及<code>$watch</code>api. 是以決定看一下vue的源碼, 了解vue是如何實作響應式資料.
本文叙事方式為樹藤摸瓜, 順着看源碼的邏輯走一遍, 檢視的vue的版本為2.5.2.
明确調查方向才能直至目标, 先說一下目标行為:
vue中的資料改變, 視圖層面就能獲得到通知并進行渲染.
<code>$watch</code>api監聽表達式的值, 在表達式中任何一個元素變化以後獲得通知并執行回調.
那麼準備開始以這個方向為目标從vue源碼的入口開始找答案.
來到<code>src/core/index.js</code>, 調了<code>initGlobalAPI()</code>, 其他代碼是ssr相關, 暫不關心.
進入<code>initGlobalAPI</code>方法, 做了一些暴露全局屬性和方法的事情, 最後有4個init, initUse是Vue的install方法, 前面vuex和vue-i18n的源碼分析已經分析過了. initMixin是我們要深入的部分.
在<code>initMixin</code>前面部分依舊做了一些變量的處理, 具體的init動作為:
vue啟動的順序已經看到了: 加載生命周期/時間/渲染的方法 => beforeCreate鈎子 => 調用injection => 初始化state => 調用provide => created鈎子.
injection和provide都是比較新的api, 我還沒用過. 我們要研究的東西在initState中.
來到initState:
做的事情很簡單: 如果有props就處理props, 有methods就處理methods, …, 我們直接看<code>initData(vm)</code>.
initData做了兩件事: proxy, observe.
先貼代碼, 前面做了小的事情寫在注釋裡了.
我們來看一下proxy和observe是幹嘛的.
proxy的參數: vue執行個體, <code>_data</code>, 鍵.
作用: 把vm.key的setter和getter都代理到vm._data.key, 效果就是vm.a實際實際是vm._data.a, 設定vm.a也是設定vm._data.a.
代碼是:
代理完成之後是本文的核心, initData最後調用了<code>observe(data, true)</code>,來實作資料的響應.
observe方法其實是一個濾空和單例的入口, 最後行為是建立一個observe對象放到observe目标的<code>__ob__</code>屬性裡, 代碼如下:
那麼關鍵是<code>new Observer(value)</code>了, 趕緊跳到Observe這個類看看是如何構造的.
以下是Observer的構造函數:
做了幾件事:
建立内部Dep對象. (作用是之後在watcher中遞歸的時候把自己添加到依賴中)
把目标的<code>__ob__</code>屬性指派成Observe對象, 作用是上面提過的單例.
如果目标是數組, 進行方法的劫持. (下面來看)
如果是數組就observeArray, 否則walk.
那麼我們來看看observeArray和walk方法.
我們發現, observeArray的作用是遞歸調用, 最後調用的方法是<code>defineReactive</code>, 可以說這個方法是最終的核心了.
下面我們先看一下數組方法劫持的目的和方法, 之後再看<code>defineReactive</code>的做法.
之後會知道defineReactive的實作劫持的方法是<code>Object.defineProperty</code>來劫持對象的getter, setter, 那麼數組的變化不會觸發這些劫持器, 是以vue劫持了數組的一些方法, 代碼比較零散就不貼了.
最後的結果就是: array.prototype.push = function () {…}, 被劫持的方法有<code>['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']</code>, 也就是調用這些方法也會觸發響應. 具體劫持以後的方法是:
做了兩件事:
遞歸調用
觸發所屬Dep的<code>notify()</code>方法.
接下來就說最終的核心方法, defineReactive, 這個方法最後也調用了notify().
這裡先貼整個代碼:
解釋都在注釋中了, 總結一下這個方法的做的幾件重要的事:
建立Dep對象. (下面會說調用的Dep的方法的具體作用)
遞歸調用. 可以說很大部分代碼都在遞歸調用, 分别在建立子observe對象, setter, getter中.
getter中: 調用原來的getter, 收集依賴(Dep.depend(), 之後會解釋收集的原理), 同樣也是遞歸收集.
setter中: 調用原來的setter, 并判斷是否需要通知, 最後調用<code>dep.notify()</code>.
總結一下, 總的來說就是, 進入傳入的data資料會被劫持, 在get的時候調用<code>Dep.depend()</code>, 在set的時候調用<code>Dep.notify()</code>. 那麼Dep是什麼, 這兩個方法又幹了什麼, 帶着疑問去看Dep對象.
Dep應該是dependencies的意思. dep.js整個檔案隻有62行, 是以貼一下:
首先來分析變量:
全局Target. 這個其實是用來跟watcher互動的, 也保證了普通get的時候沒有target就不設定依賴, 後面會解釋.
id. 這是用來在watcher裡依賴去重的, 也要到後面解釋.
subs: 是一個watcher數組. sub應該是subscribe的意思, 也就是目前dep(依賴)的訂閱者清單.
再來看方法:
構造: 設uid, subs. addSub: 添加wathcer, removeSub: 移除watcher. 這3個好無聊.
depend: 如果有Dep.target, 就把自己添加到Dep.target中(調用了<code>Dep.target.addDep(this)</code>).那麼什麼時候有Dep.target呢, 就由<code>pushTarget()</code>和<code>popTarget()</code>來操作了, 這些方法在Dep中沒有調用, 後面會分析是誰在操作Dep.target.(這個是重點)
notify: 這個是setter劫持以後調用的最終方法, 做了什麼: 把目前Dep訂閱中的每個watcher都調用<code>update()</code>方法.
Dep看完了, 我們的疑問都轉向了Watcher對象了. 現在看來有點糊塗, 看完Watcher就都明白了.
watcher非常大(而且打watcher這個單詞也非常容易手誤, 心煩), 我們先從構造看起:
注釋都寫了, 我來高度總結一下構造器做了什麼事:
處理傳入的參數并設定成自己的屬性.
parse表達式. watcher表達式接受2種: 方法/字元串. 如果是方法就設為getter, 如果是字元串會進行處理:
處理的效果寫在上面代碼的注釋裡.
調用<code>get()</code>方法.
下面說一下get方法. get()方法是核心, 看完了就能把之前的碎片都串起來了. 貼get()的代碼:
注釋都在代碼中了, 這段了解了就對整個響應系統了解了.
我來總結一下: (核心, 非常重要)
dep方面: 傳入vue參數的data(實際是所有調用<code>defineReactive</code>的屬性)都會産生自己的Dep對象.
Watcher方面: 在所有new Watcher的地方産生Watcher對象.
dep與Watcher關系: Watcher的get方法建立了雙方關系:把自己設為target, 運作watcher的表達式(即調用相關資料的getter), 因為getter有鈎子, 調用了Watcher的addDep, addDep方法把dep和Watcher互相推入互相的屬性數組(分别是deps和subs)
dep與Watcher建立了多對多的關系: dep含有訂閱的watcher的數組, watcher含有所依賴的變量的數組
當dep的資料調動setter, 調用notify, 最終調用Watcher的update方法.
前面提到dep與Watcher建立關系是通過<code>get()</code>方法, 這個方法在3個地方出現: 構造方法, run方法, evaluate方法. 也就是說, notify了以後會重新調用一次get()方法. (是以在lifycycle中調用的時候把依賴和觸發方法都寫到getter方法中了).
那麼接下來要看一看watcher在什麼地方調用的.
找了一下, 一共三處:
initComputed的時候: (state.js)
<code>watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions )</code>
$watch api: (state.js)
<code>new Watcher(vm, expOrFn, cb, options)</code>
lifecycle的mount階段: (lifecycle.js)
<code>new Watcher(vm, updateComponent, noop)</code>
看完源碼就不神秘了, 寫得也算很清楚了. 當然還有很多細節沒寫, 因為沖着目标來.
總結其實都在上一節的粗體裡了.
我們隻從data看了, 那麼props和computed應該也是這樣的, 因為props應該與組建相關, 下回分解吧, 我們來看看computed是咋回事吧.
已注釋, 總結為:
周遊每個computed鍵值, 過濾錯誤文法.
周遊每個computed鍵值, 為他們建立watcher, options為<code>{ lazy: true}</code>.
周遊每個computed鍵值, 調用defineComputed.
那麼繼續看defineComputed.
因為computed可以設定getter, setter, 是以computed的值不一定是function, 可以為set和get的function, 很大部分代碼是做這些處理, 核心的事情有2件:
使用Object.defineProperty在vm上挂載computed屬性.
為屬性設定getter, getter做了和data一樣的事: depend. 但是多了一步: <code>watcher.evalueate()</code>.
看到這裡, computed注冊核心一共做了兩件事:
為每個computed建立watcher(lazy: true)
建立一個getter來depend, 并挂到vm上.
那麼dirty成了疑問, 我們回到watcher的代碼中去看, lazy和dirty和evaluate是幹什麼的.
精選相關代碼:
(構造函數中) <code>this.dirty = this.lazy</code>
(構造函數中) <code>this.value = this.lazy ? undefined : this.get()</code>
(evaluate函數)
<code>evaluate () { this.value = this.get() this.dirty = false }</code>
到這裡已經很清楚了. 因為還沒設定getter, 是以在建立watcher的時候不立即調用getter, 是以構造函數沒有馬上調用get, 在設定好getter以後調用evaluate來進行依賴注冊.
總結: computed是watch+把屬性挂到vm上的行為組合.