Vue 響應(yīng)式和依賴收集實現(xiàn)原理淺入深出

前言
vue 是一個易上手的框架,許多便捷功能都在其內(nèi)部做了集成,其中最有區(qū)別性的功能就是其潛藏于底層的響應(yīng)式系統(tǒng)。組件狀態(tài)都是響應(yīng)式的 JavaScript 對象。當(dāng)更改它們時,視圖會隨即更新,這讓狀態(tài)管理更加簡單直觀。那么,Vue 響應(yīng)性系統(tǒng)是如何實現(xiàn)的呢?本文也是在閱讀了 Vue 源碼后的理解以及模仿實現(xiàn),所以跟隨作者的思路,我們一起由淺入深的探索一下vue吧!
本文 Vue 源碼版本:2.6.14,為了便于理解,代碼都最簡化。

Vue 是如何實現(xiàn)的數(shù)據(jù)響應(yīng)式
當(dāng)你把一個普通的 JavaScript 對象傳入 Vue 實例作為 data 選項,Vue 將遍歷此對象所有的 property,并使用 Object.defineProperty 把這些 property 全部轉(zhuǎn)為 getter/setter,然后圍繞 getter/setter來運(yùn)行。
一句話概括Vue 的響應(yīng)式系統(tǒng)就是: 觀察者模式 + Object.defineProperty 攔截getter/setter
https://developer.mozilla.org/zhCN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
https://refactoringguru.cn/design-patterns/observer
什么是Object.defineProperty ?
Object.defineProperty()?方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現(xiàn)有屬性,并返回此對象。
簡單的說,就是通過此方式定義的 property,執(zhí)行 obj.xxx 時會觸發(fā) get,執(zhí)行 obj.xxx = xxx`會觸發(fā) set,這便是響應(yīng)式的關(guān)鍵。
Object.defineProperty 是 ES5 中一個無法 shim(無法通過polyfill實現(xiàn)) 的特性,這也就是 Vue 不支持 IE8 以及更低版本瀏覽器的原因。
響應(yīng)式系統(tǒng)基礎(chǔ)實現(xiàn)
現(xiàn)在,我們來基于Object.defineProperty
實現(xiàn)一個簡易的響應(yīng)式更新系統(tǒng)作為“開胃菜”
接下來我們在瀏覽器中運(yùn)行這段代碼,會得到期望的效果
通過上面的代碼,我想你對響應(yīng)式系統(tǒng)的工作原理已經(jīng)有了一定的理解。為了讓這個“開胃菜”易于消化,這個簡易的響應(yīng)式系統(tǒng)還有很多缺點(diǎn),例如:數(shù)據(jù)和響應(yīng)更新函數(shù)是通過硬編碼強(qiáng)耦合在一起的、只實現(xiàn)了一對一的情況、不夠模塊化等等……所以接下來,我們來一一完善。
設(shè)計一個完善的響應(yīng)式系統(tǒng)
要設(shè)計一個完善的響應(yīng)式系統(tǒng),我們需要先了解一個前置知識,什么是觀察者模式?
什么是觀察者模式?
它就是一種行為設(shè)計模式, 允許你定義一種訂閱機(jī)制, 可在對象事件發(fā)生時通知多個 “觀察” 該對象的其他對象。
擁有一些值得關(guān)注狀態(tài)的對象通常被稱為目標(biāo),由于它自身狀態(tài)發(fā)生改變時需要通知其他對象,我們也將其成為發(fā)布者(pub-lish-er)?。所有希望關(guān)注發(fā)布者狀態(tài)變化的其他對象被稱為訂閱者(sub-scribers)?。此外,發(fā)布者與所有訂閱者直接僅通過接口交互,都必須具有同樣的接口。

舉個例子??:
你(即應(yīng)用中的訂閱者)對某個書店的周刊感興趣,你給老板(即應(yīng)用中的發(fā)布者)留了電話,讓老板一有新周刊就給你打電話,其他對這本周刊感興趣的人,也給老板留了電話。新周刊到貨時,老板就挨個打電話,通知讀者來取。
假如某個讀者一不小心留的是 qq 號,不是電話號碼,老版打電話時就會打不通,該讀者就收不到通知了。這就是我們上面說的,必須具有相同的接口。
了解了觀察者模式后,我們就開始著手設(shè)計響應(yīng)式系統(tǒng)。
抽象觀察者(訂閱者)類Watcher
在上面的例子中,數(shù)據(jù)和響應(yīng)更新函數(shù)是通過硬編碼強(qiáng)耦合在一起的。而實際開發(fā)過程中,更新函數(shù)不一定叫fn
,更有可能是一個匿名函數(shù)。所以我們需要抽像一個觀察者(訂閱者)類Watcher
來保存并執(zhí)行更新函數(shù),同時向外提供一個update
更新接口。
抽象被觀察者(發(fā)布者)類Dep
我們再想一想,實際開發(fā)過程中,data 中肯定不止一個數(shù)據(jù),而且每個數(shù)據(jù),都有不同的訂閱者,所以說我們還需要抽象一個被觀察者(發(fā)布者)Dep
類來保存數(shù)據(jù)對應(yīng)的觀察者(Watcher
),以及數(shù)據(jù)變化時通知觀察者更新。
抽象 Observer
現(xiàn)在,Watcher
和Dep
只是兩個獨(dú)立的模塊,我們怎么把它們關(guān)聯(lián)起來呢?
答案就是Object.defineProperty
,在數(shù)據(jù)被讀取,觸發(fā)get
方法,Dep 將當(dāng)前觸發(fā) get 的 Watcher 當(dāng)做訂閱者放到 subs中,Watcher
?就與?Dep
建立關(guān)系;在數(shù)據(jù)被修改,觸發(fā)set
方法,Dep
就遍歷 subs 中的訂閱者,通知Watcher
更新。
下面我們就來完善將數(shù)據(jù)轉(zhuǎn)換為getter/setter的處理。
上面基礎(chǔ)的響應(yīng)式系統(tǒng)實現(xiàn)中,我們只定義了一個響應(yīng)式數(shù)據(jù),當(dāng) data 中有其他property時我們就處理不了了。所以,我們需要抽象一個?Observer
類來完成對 data數(shù)據(jù)的遍歷,并調(diào)用defineReactive
轉(zhuǎn)換為 getter/setter,最終完成響應(yīng)式綁定。
為了簡化,我們只處理data中單層數(shù)據(jù)。
這里我們通過參數(shù) value 的閉包,來保存最新的數(shù)據(jù),避免新增其他變量
至此,響應(yīng)式系統(tǒng)就大功告成了??!下面我們來測試一下
測試
我們通過下面代碼測試一下:
在瀏覽器中運(yùn)行這段代碼,和我們期望的一樣,兩個render
都執(zhí)行了,并且在控制臺上打印了結(jié)果。

我們嘗試修改?data.name = '李四 23333333'
,測試兩個?render
?都會重新執(zhí)行:

我們只修改?data.address = '北京'
,測試一下是否只有render 1
回調(diào)都會重新執(zhí)行:

都完美通過測試?。??
總結(jié)

Vue
響應(yīng)式原理的核心就是Observer
、Dep
、Watcher
,三者共同構(gòu)成 MVVM 中的 VM
Observer
中進(jìn)行數(shù)據(jù)響應(yīng)式處理以及最終的Watcher
?和Dep
關(guān)系綁定,在數(shù)據(jù)被讀的時候,觸發(fā)get
方法,將?Watcher
收集到?Dep
中作為依賴;在數(shù)據(jù)被修改的時候,觸發(fā)set
方法,Dep
就遍歷 subs 中的訂閱者,通知Watcher
更新。
本篇文章屬于入門篇,并非源碼實現(xiàn),在源碼的基礎(chǔ)上簡化了很多內(nèi)容,能夠便于理解Observer
、Dep
、Watcher
三者的作用和關(guān)系。
本文的源碼,以及作者學(xué)習(xí) Vue 源碼完整的逐行注釋源碼地址:https://github.com/yue1123/vue-2.6-study
參考內(nèi)容
https://v2.cn.vuejs.org/
https://developer.mozilla.org/zh-CN/
https://refactoringguru.cn/design-patterns/observer
文章首發(fā)于掘金:https://juejin.cn/post/7139078234905247774