寫在前面
最新 vue-next
的源碼發佈了,雖然是 pre-alpha
版本,但這時候其實是閱讀源碼的比較好的時機。在 vue
中,比較重要的東西當然要數它的響應式系統,在之前的版本中,已經有若干篇文章對它的響應式原理和實現進行了介紹,這裏就不贅述了。在 vue-next
中,其實現原理和之前還是相同的,即通過觀察者模式和數據劫持,只不過對其實現方式進行了改變。
對於解析原理的文章,我個人是比較喜歡那種“小白”風格的文章,即不要摘錄特別多的代碼,也不要闡述一些很深奧的原理與概念。在我剛接觸 react
的時候,還記得有一篇利用 jquery
來介紹 react
的文章,從簡入繁,面面俱到,其背後闡述的知識點對我後來學習 react
起到很多的幫助。
因此,這篇文章我也打算按這種風格來寫一下利用最近空閒時間閱讀 vue-next
響應式模塊的源碼的一些心得與體會,算是拋磚引玉,同時實現一個極簡的響應式系統。
如有錯誤,還望指正。
預備知識
無論是閱讀這篇文章,還是閱讀 vue-next
響應式模塊的源碼,首先有兩個知識點是必備的:
-
Proxy
:es6 中新的代理內建工具類 -
Reflect
:es6 中新的反射工具類
由於篇幅有限,這裏也不詳細贅述這兩個類的用途與使用方法了,推薦三篇我認爲不錯的文章,僅供參考:
接口
對於 vue-next
響應式系統的 RFC
,可以參考這裏。雖然距離現在有一段時間了,但是通過閱讀源碼,可以發現一些影子。
我們大體要實現的效果如下面的代碼所示:
// 實現兩個方法 reactive 和 effect
const state = reactive({
count: 0
})
effect(() => {
console.log('count: ', state.count)
})
state.count++ // 輸入 count: 1
可以發現我們熟悉的依賴收集階段(同時也是觀察者模式的訂閱過程),是在 effect
中進行的,依賴收集的準備工作(即數據劫持邏輯),是在 reactive
中進行的,而數據變化的觸發響應的邏輯在後面的 state.count++
代碼執行時進行(同時也是觀察者模式的發佈過程),之後便會執行之前傳入 effect
內部的回調函數並輸入 count: 1
。
類型與公共變量
由於 vue-next
用 ts
進行了重寫,這裏我也使用 ts
來實現這個極簡版本的響應式系統。主要涉及到的類型和公共變量如下:
type Effect = Function;
type EffectMap = Map<string, Effect[]>;
let currentEffect: Effect;
const effectMap: EffectMap = new Map();
-
currentEffect
:用來儲存當前正在收集依賴的effect
-
effectMap
:代表目標對象每個key
所對應的依賴於它的effect
數組,也可以把它理解爲觀察者模式中的訂閱者字典
利用 Proxy 實現數據劫持
在之前的版本中,vue
利用 Object.defineProperty
中的 setter
和 getter
來對數據對象進行劫持,vue-next
則通過 Proxy
。衆所周知,Object.defineProperty
所實現的數據劫持是有一定限制的,而 Proxy
就會強大很多。
首先,我們在腦後中,設想一下如何使用 Proxy
來實現數據劫持呢?很簡單,大體結構如下所示:
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
}
由於這裏是極簡版本的實現,那麼我們就僅僅實現 get
和 set
兩個 trap
就可以了,分別對應依賴收集和觸發響應的邏輯。
依賴收集
對於依賴收集的實現,由於是極簡版本,實現的前提如下:
- 不考慮對象的嵌套
- 不考慮集合類型
- 不考慮基礎類型
- 不考慮對代理對象的處理
哈哈,基本這四點排除之後,這個依賴收集函數就會很輕很薄,如下:
function(target, key: string, receiver) {
// 僅僅在某個 effect 內部進行依賴收集
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);
}
實現的邏輯很簡單,其實就是觀察者模式中註冊訂閱者的實現邏輯,值得注意的是,這裏對於 target
的賦值邏輯,我們委託給 Reflect
來完成,雖然 target[key]
也是可以工作的,但是使用 Reflect
是更提倡的方式。
觸發響應
觸發響應的邏輯就比較簡單了,其實是對應觀察者模式中,發佈事件的邏輯,如下:
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
進行賦值操作,因爲它會返回一個 boolean
值代表是否成功,而 set
這個 trap
也需要代表相同含義的值。
通過 reactive 方法來初始化代理對象
實現了數據劫持的代理邏輯之後,我們只需要在 reactive
這個方法中,返回一個代理對象的實例即可,還記的上文中我們在實現之前腦海中浮現的大致代碼框架嗎?
如下:
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;
}
依賴收集的準備工作
上文中提到了,對於依賴收集的工作,我們是有條件地進行的,即在一個 effect
中,我們纔會進行收集,其他情況下的取值邏輯,我們則不會進行依賴收集,因此,effect
方法正式爲了實現這點而存在的,如下:
export function effect(fn: Function) {
const effected = function() {
fn();
};
currentEffect = effected;
effected();
currentEffect = undefined;
return effected;
}
之所以實現如此簡單,是因爲我們這裏是極簡版本,不需要考慮諸如 readOnly
、異常以及收集時機等因素。可以發現,就是將傳入的回調函數包裹在另一個方法中,然後將這個方法用 currentEffect
這個變量暫存,之後嘗試運行一下即可。當 effect
運行完畢之後,再將 currentEffect
置空,這樣就可以達到只在 effect
下進行依賴收集的目的。
運行效果
我在 codepen
上簡單寫了一個計數器 demo
,鏈接如下:
https://codepen.io/littlelyon1/pen/mddVPgo
寫在最後
這個極簡的響應式系統雖然能用,但是有很多未考慮的因素,其實就是在上文中被我們忽略的那些前提條件,這裏再列舉一下,並給出源代碼中的解法:
- 基礎數據類型的處理:可以將基礎數據類型封裝爲一個
ref
對象,其value
指向基礎數據類型的值 - 嵌套對象:遞歸進行執行代理過程即可
- 集合對象:編寫專門的
trap
處理邏輯 - 代理實例:緩存這些代理實例,下次遇到直接返回即可
但我仍然推薦你直接去閱讀一下源碼,因爲你會發現,源碼會在這個極簡版本基礎上,利用了更加複雜數據結構以及流程,來控制依賴收集和觸發響應的流程,同時各種特殊情況也有更加明細的考慮。
另外,這僅僅是 vue-next
響應式系統的簡易實現,諸如其他功能模塊,比如指令、模板解析、vdom
等,我也準備利用最近的空閒時間再去看看,有時間的話,最近也整理出來,分享給大家。