天天看點

vue源碼之響應式資料

分析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啟動的順序已經看到了: 加載生命周期/時間/渲染的方法 =&gt; beforeCreate鈎子 =&gt; 調用injection =&gt; 初始化state =&gt; 調用provide =&gt; 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上的行為組合.