Vue原始碼閱讀 – 依賴收集原理

發表於2018-10-10

vue已是目前國內前端web端三分天下之一,同時也作為本人主要技術棧之一,在日常使用中知其然也好奇著所以然,另外最近的社群湧現了一大票vue原始碼閱讀類的文章,在下借這個機會從大家的文章和討論中汲取了一些營養,同時對一些閱讀原始碼時的想法進行總結,出產一些文章,作為自己思考的總結,本人水平有限,歡迎留言討論~

目標Vue版本:2.5.17-beta.0

vue原始碼註釋:github.com/SHERlocked9…

宣告:文章中原始碼的語法都使用 Flow,並且原始碼根據需要都有刪節(為了不被迷糊 @_@),如果要看完整版的請進入上面的github地址,本文是系列文章,文章地址見底部~

1. 響應式系統

通過官網的介紹我們知道 Vue.js 是一個MVVM框架,它並不關心檢視變化,而通過資料驅動檢視更新,這讓我們的狀態管理非常簡單,而這是怎麼實現的呢。盜用官網一張圖

Vue原始碼閱讀 – 依賴收集原理

每個元件例項都有相應的 Watcher 例項物件,它會在元件渲染的過程中把屬性記錄為依賴,之後當依賴項的 setter 被呼叫時,會通知 watcher 重新計算,從而致使它關聯的元件得以更新。

這裡有三個重要的概念 ObserveDepWatcher,分別位於src/core/observer/index.jssrc/core/observer/dep.jssrc/core/observer/watcher.js

  • Observe 類主要給響應式物件的屬性新增 getter/setter 用於依賴收集與派發更新
  • Dep 類用於收集當前響應式物件的依賴關係
  • Watcher 類是觀察者,例項分為渲染 watcher、計算屬性 watcher、偵聽器 watcher三種

2. 程式碼實現

2.1 initState

響應式化的入口位於 src/core/instance/init.js 的 initState 中:

它非常規律的定義了幾個方法來初始化 propsmethodsdatacomputedwathcer,這裡看一下 initData 方法,來窺一豹

首先判斷了下 data 是不是函式,是則取返回值不是則取自身,之後有一個 observe 方法對 data 進行處理,這個方法嘗試給建立一個Observer例項 __ob__,如果成功建立則返回新的Observer例項,如果已有Observer例項則返回現有的Observer例項

2.2 Observer/defineReactive

這個方法主要用 data 作為引數去例項化一個 Observer 物件例項,Observer 是一個 Class,用於依賴收集和 notify 更新,Observer 的建構函式使用 defineReactive 方法給物件的鍵響應式化,給物件的屬性遞迴新增 getter/setter ,當data被取值的時候觸發 getter 並蒐集依賴,當被修改值的時候先觸發 getter 再觸發 setter 並派發更新

getter 的時候進行依賴的收集,注意這裡,只有在 Dep.target 中有值的時候才會進行依賴收集,這個 Dep.target 是在Watcher例項的 get 方法呼叫的時候 pushTarget 會把當前取值的watcher推入 Dep.target,原先的watcher壓棧到 targetStack 棧中,當前取值的watcher取值結束後出棧並把原先的watcher值賦給 Dep.targetcleanupDeps 最後把新的 newDeps 裡已經沒有的watcher清空,以防止檢視上已經不需要的無用watcher觸發

setter 的時候首先 getter,並且比對舊值沒有變化則return,如果發生變更,則dep通知所有subs中存放的依賴本資料的Watcher例項 update 進行更新,這裡 update 中會 queueWatcher( ) 非同步推送到排程者觀察者佇列 queue 中,在nextTick時 flushSchedulerQueue( ) 把佇列中的watcher取出來執行 watcher.run 且執行相關鉤子函式

2.3 Dep

上面多次提到了一個關鍵詞 Dep,他是依賴收集的容器,或者稱為依賴蒐集器,他記錄了哪些Watcher依賴自己的變化,或者說,哪些Watcher訂閱了自己的變化;這裡引用一個網友的發言:

@liuhongyi0101 :簡單點說就是引用計數 ,誰借了我的錢,我就把那個人記下來,以後我的錢少了 我就通知他們說我沒錢了

而把借錢的人記下來的小本本就是這裡 Dep 例項裡的subs

這裡 Dep 的例項中的 subs 蒐集的依賴就是 watcher 了,它是 Watcher 的例項,將來用來通知更新

2.4 Watcher

get 方法中執行的 getter 就是在一開始new渲染watcher時傳入的 updateComponent = () => { vm._update(vm._render(), hydrating) },這個方法首先 vm._render() 生成渲染VNode樹,在這個過程中完成對當前Vue例項 vm 上的資料訪問,觸發相應一眾響應式物件的 getter,然後 vm._update()patch

注意這裡的 get 方法最後執行了 getAndInvoke,這個方法首先遍歷watcher中存的 deps,移除 newDep 中已經沒有的訂閱,然後 depIds = newDepIds; deps = newDeps ,把 newDepIdsnewDeps 清空。每次新增完新的訂閱後移除舊的已經不需要的訂閱,這樣在某些情況,比如 v-if 已不需要的模板依賴的資料發生變化時就不會通知watcher去 update

2.5 小結

整個收集的流程大約是這樣的,可以對照著上面的流程看一下

Vue原始碼閱讀 – 依賴收集原理

watcher 有下面幾種使用場景:

  • render watcher 渲染 watcher,渲染檢視用的 watcher
  • computed watcher 計算屬性 watcher,因為計算屬性即依賴別人也被人依賴,因此也會持有一個 Dep 例項
  • watch watcher 偵聽器 watcher

只要會被別的觀察者 (watchers) 依賴,比如data、data的屬性、計算屬性、props,就會在閉包裡生成一個 Dep 的例項 dep 並在被呼叫 getter 的時候 dep.depend 收集它被誰依賴了,並把被依賴的watcher存放到自己的subs中 this.subs.push(sub),以便在自身改變的時候通知 notify 存放在 dep.subs 陣列中依賴自己的 watchers 自己改變了,請及時 update ~

只要依賴別的響應式化物件的物件,都會生成一個觀察者 watcher ,用來統計這個 watcher 依賴了哪些響應式物件,在這個 watcher 求值前把當前 watcher 設定到全域性 Dep.target,並在自己依賴的響應式物件發生改變的時候及時 update


本文是系列文章,隨後會更新後面的部分,共同進步~

  1. Vue原始碼閱讀 – 檔案結構與執行機制
  2. Vue原始碼閱讀 – 依賴收集原理
  3. Vue原始碼閱讀 – 批量非同步更新與nextTick原理

網上的帖子大多深淺不一,甚至有些前後矛盾,在下的文章都是學習過程中的總結,如果發現錯誤,歡迎留言指出~

相關文章