15 分鐘掌握vue-next響應(yīng)式原理
寫在前面
最新 vue-next 的源碼發(fā)布了,雖然是 pre-alpha 版本,但這時候其實(shí)是閱讀源碼的比較好的時機(jī)。在 vue 中,比較重要的東西當(dāng)然要數(shù)它的響應(yīng)式系統(tǒng),在之前的版本中,已經(jīng)有若干篇文章對它的響應(yīng)式原理和實(shí)現(xiàn)進(jìn)行了介紹,這里就不贅述了。在 vue-next 中,其實(shí)現(xiàn)原理和之前還是相同的,即通過觀察者模式和數(shù)據(jù)劫持,只不過對其實(shí)現(xiàn)方式進(jìn)行了改變。
對于解析原理的文章,我個人是比較喜歡那種“小白”風(fēng)格的文章,即不要摘錄特別多的代碼,也不要闡述一些很深奧的原理與概念。在我剛接觸 react 的時候,還記得有一篇利用 jquery 來介紹 react 的文章,從簡入繁,面面俱到,其背后闡述的知識點(diǎn)對我后來學(xué)習(xí) react 起到很多的幫助。
因此,這篇文章我也打算按這種風(fēng)格來寫一下利用最近空閑時間閱讀 vue-next 響應(yīng)式模塊的源碼的一些心得與體會,算是拋磚引玉,同時實(shí)現(xiàn)一個極簡的響應(yīng)式系統(tǒng)。
如有錯誤,還望指正。
預(yù)備知識
無論是閱讀這篇文章,還是閱讀 vue-next 響應(yīng)式模塊的源碼,首先有兩個知識點(diǎn)是必備的:
- Proxy:es6 中新的代理內(nèi)建工具類
- Reflect:es6 中新的反射工具類
由于篇幅有限,這里也不詳細(xì)贅述這兩個類的用途與使用方法了,推薦三篇我認(rèn)為不錯的文章,僅供參考:
接口
對于 vue-next 響應(yīng)式系統(tǒng)的 RFC,可以參考這里。雖然距離現(xiàn)在有一段時間了,但是通過閱讀源碼,可以發(fā)現(xiàn)一些影子。
我們大體要實(shí)現(xiàn)的效果如下面的代碼所示:
// 實(shí)現(xiàn)兩個方法 reactive 和 effect const state = reactive({ count: 0 }) effect(() => { console.log('count: ', state.count) }) state.count++ // 輸入 count: 1
可以發(fā)現(xiàn)我們熟悉的依賴收集階段(同時也是觀察者模式的訂閱過程),是在 effect 中進(jìn)行的,依賴收集的準(zhǔn)備工作(即數(shù)據(jù)劫持邏輯),是在 reactive 中進(jìn)行的,而數(shù)據(jù)變化的觸發(fā)響應(yīng)的邏輯在后面的 state.count++ 代碼執(zhí)行時進(jìn)行(同時也是觀察者模式的發(fā)布過程),之后便會執(zhí)行之前傳入 effect 內(nèi)部的回調(diào)函數(shù)并輸入 count: 1。
類型與公共變量
由于 vue-next 用 ts 進(jìn)行了重寫,這里我也使用 ts 來實(shí)現(xiàn)這個極簡版本的響應(yīng)式系統(tǒng)。主要涉及到的類型和公共變量如下:
type Effect = Function; type EffectMap = Map<string, Effect[]>; let currentEffect: Effect; const effectMap: EffectMap = new Map();
- currentEffect:用來儲存當(dāng)前正在收集依賴的 effect
- effectMap:代表目標(biāo)對象每個 key 所對應(yīng)的依賴于它的 effect 數(shù)組,也可以把它理解為觀察者模式中的訂閱者字典
利用 Proxy 實(shí)現(xiàn)數(shù)據(jù)劫持
在之前的版本中,vue 利用 Object.defineProperty 中的 setter 和 getter 來對數(shù)據(jù)對象進(jìn)行劫持,vue-next 則通過 Proxy。眾所周知,Object.defineProperty 所實(shí)現(xiàn)的數(shù)據(jù)劫持是有一定限制的,而 Proxy 就會強(qiáng)大很多。
首先,我們在腦后中,設(shè)想一下如何使用 Proxy 來實(shí)現(xiàn)數(shù)據(jù)劫持呢?很簡單,大體結(jié)構(gòu)如下所示:
export function reactive(obj) { const proxied = new Proxy(obj, handlers); return proxied; }
這里的 handlers 是聲明如何處理各個 trap 的邏輯,比如:
const handlers = { get: function(target, key, receiver) { ... }, set: function(target, key, value, receiver) { ... }, deleteProperty(target, key) { ... } // ...以及其他 trap }
由于這里是極簡版本的實(shí)現(xiàn),那么我們就僅僅實(shí)現(xiàn) get 和 set 兩個 trap 就可以了,分別對應(yīng)依賴收集和觸發(fā)響應(yīng)的邏輯。
依賴收集
對于依賴收集的實(shí)現(xiàn),由于是極簡版本,實(shí)現(xiàn)的前提如下:
- 不考慮對象的嵌套
- 不考慮集合類型
- 不考慮基礎(chǔ)類型
- 不考慮對代理對象的處理
哈哈,基本這四點(diǎn)排除之后,這個依賴收集函數(shù)就會很輕很薄,如下:
function(target, key: string, receiver) { // 僅僅在某個 effect 內(nèi)部進(jìn)行依賴收集 if (currentEffect) { if (effectMap.has(key)) { const effects = effectMap.get(key); if (effects.indexOf(currentEffect) === -1) { effects.push(currentEffect); } } else { effectMap.set(key, [currentEffect]); } } return Reflect.get(target, key, receiver); }
實(shí)現(xiàn)的邏輯很簡單,其實(shí)就是觀察者模式中注冊訂閱者的實(shí)現(xiàn)邏輯,值得注意的是,這里對于 target 的賦值邏輯,我們委托給 Reflect 來完成,雖然 target[key] 也是可以工作的,但是使用 Reflect 是更提倡的方式。
觸發(fā)響應(yīng)
觸發(fā)響應(yīng)的邏輯就比較簡單了,其實(shí)是對應(yīng)觀察者模式中,發(fā)布事件的邏輯,如下:
function(target, key: string, value, receiver) { const result = Reflect.set(target, key, value, receiver); if (effectMap.has(key)) { effectMap.get(key).forEach(effect => effect()); } return result; }
同樣,這里使用 Reflect 來對 target 進(jìn)行賦值操作,因?yàn)樗鼤祷匾粋€ boolean 值代表是否成功,而 set 這個 trap 也需要代表相同含義的值。
通過 reactive 方法來初始化代理對象
實(shí)現(xiàn)了數(shù)據(jù)劫持的代理邏輯之后,我們只需要在 reactive 這個方法中,返回一個代理對象的實(shí)例即可,還記的上文中我們在實(shí)現(xiàn)之前腦海中浮現(xiàn)的大致代碼框架嗎?
如下:
export function reactive(obj: any) { const proxied = new Proxy(obj, { get: function(target, key: string, receiver) { if (currentEffect) { if (effectMap.has(key)) { const effects = effectMap.get(key); if (effects.indexOf(currentEffect) === -1) { effects.push(currentEffect); } } else { effectMap.set(key, [currentEffect]); } } return Reflect.get(target, key, receiver); }, set: function(target, key: string, value, receiver) { const result = Reflect.set(target, key, value, receiver); if (effectMap.has(key)) { effectMap.get(key).forEach(effect => effect()); } return result; } }); return proxied; }
依賴收集的準(zhǔn)備工作
上文中提到了,對于依賴收集的工作,我們是有條件地進(jìn)行的,即在一個 effect 中,我們才會進(jìn)行收集,其他情況下的取值邏輯,我們則不會進(jìn)行依賴收集,因此,effect 方法正式為了實(shí)現(xiàn)這點(diǎn)而存在的,如下:
export function effect(fn: Function) { const effected = function() { fn(); }; currentEffect = effected; effected(); currentEffect = undefined; return effected; }
之所以實(shí)現(xiàn)如此簡單,是因?yàn)槲覀冞@里是極簡版本,不需要考慮諸如 readOnly 、異常以及收集時機(jī)等因素??梢园l(fā)現(xiàn),就是將傳入的回調(diào)函數(shù)包裹在另一個方法中,然后將這個方法用 currentEffect 這個變量暫存,之后嘗試運(yùn)行一下即可。當(dāng) effect 運(yùn)行完畢之后,再將 currentEffect 置空,這樣就可以達(dá)到只在 effect 下進(jìn)行依賴收集的目的。
運(yùn)行效果
我在 codepen 上簡單寫了一個計數(shù)器 demo,鏈接如下:
https://codepen.io/littlelyon1/pen/mddVPgo
寫在最后
這個極簡的響應(yīng)式系統(tǒng)雖然能用,但是有很多未考慮的因素,其實(shí)就是在上文中被我們忽略的那些前提條件,這里再列舉一下,并給出源代碼中的解法:
- 基礎(chǔ)數(shù)據(jù)類型的處理:可以將基礎(chǔ)數(shù)據(jù)類型封裝為一個 ref 對象,其 value 指向基礎(chǔ)數(shù)據(jù)類型的值
- 嵌套對象:遞歸進(jìn)行執(zhí)行代理過程即可
- 集合對象:編寫專門的 trap 處理邏輯
- 代理實(shí)例:緩存這些代理實(shí)例,下次遇到直接返回即可
但我仍然推薦你直接去閱讀一下源碼,因?yàn)槟銜l(fā)現(xiàn),源碼會在這個極簡版本基礎(chǔ)上,利用了更加復(fù)雜數(shù)據(jù)結(jié)構(gòu)以及流程,來控制依賴收集和觸發(fā)響應(yīng)的流程,同時各種特殊情況也有更加明細(xì)的考慮。
另外,這僅僅是 vue-next 響應(yīng)式系統(tǒng)的簡易實(shí)現(xiàn),諸如其他功能模塊,比如指令、模板解析、vdom 等,我也準(zhǔn)備利用最近的空閑時間再去看看,有時間的話,最近也整理出來,分享給大家。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Vue綁定class和綁定內(nèi)聯(lián)樣式的實(shí)現(xiàn)方法
本文主要介紹了Vue綁定class和綁定內(nèi)聯(lián)樣式的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11詳解vue中在循環(huán)中使用@mouseenter 和 @mouseleave事件閃爍問題解決方法
這篇文章主要介紹了詳解vue中在循環(huán)中使用@mouseenter 和 @mouseleave事件閃爍問題解決方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04關(guān)于element同時使用Drawer和Dialog出現(xiàn)多個遮罩問題
這篇文章主要介紹了關(guān)于element同時使用Drawer和Dialog出現(xiàn)多個遮罩問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07elementui中使用el-tabs切換實(shí)時更新數(shù)據(jù)
這篇文章主要介紹了elementui中使用el-tabs切換實(shí)時更新數(shù)據(jù),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08vue組件props不同數(shù)據(jù)類型傳參的默認(rèn)值問題
這篇文章主要介紹了vue組件props不同數(shù)據(jù)類型傳參的默認(rèn)值問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07Vue3+TS實(shí)現(xiàn)動態(tài)路由權(quán)限的示例詳解
當(dāng)我們在開發(fā)一個大型的前端應(yīng)用時,動態(tài)路由權(quán)限是一個必不可少的功能,本文將介紹如何使用Vue 3和TypeScript來實(shí)現(xiàn)動態(tài)路由權(quán)限,希望對大家有所幫助2024-01-01vue用復(fù)選框?qū)崿F(xiàn)組件且支持單選和多選操作方式
這篇文章主要介紹了vue用復(fù)選框?qū)崿F(xiàn)組件且支持單選和多選操作方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-04-04