Vue
的響應式系統很讓人著迷,Vue2
使用的是Object.defineProperty
,Vue3
使用的是Proxy
,這個是大家都知道的技術點;
但是知道了這些個技術點就能寫出一個響應式系統嗎?答案是肯定是NO
,Vue
的響應式系統是一個非常複雜的系統,技術只是實現的手段,今天我們就來看看背後實現的思想。
本章內容有點多,如果不能耐著性子看的話,建議先看看我線上實現的小 demo 去理解核心思想,不明白再來在文章中尋找答案:https://codesandbox.io/s/magical-knuth-mjyh7j?file=/src/main.js
reactive 和 effect
Vue3
的響應式系統透過官網的API
可以看到有很多,例如ref
、computed
、reactive
、readonly
、watchEffect
、watch
等等,這些都是Vue3
的響應式系統的一部分;
reactive
reactive
根據官網的介紹,有如下特點:
- 接收一個普通物件,返回一個響應式的代理物件;
- 響應式的物件是深層的,會影響物件內部所有巢狀的屬性;
- 會自動對
ref
物件進行解包; - 對於陣列、物件、
Map
、Set
等原生型別中的元素,如果是ref
物件不會自動解包; - 返回的物件會透過
Proxy
進行包裝,所以不等於原始物件;
上面的這些特點都是可以在官網中有介紹,如果我說的不是很好理解建議去官網看看,官網對這些特點都有詳細的介紹,並且還有示例程式碼。
對於reactive
的作用其實使用Vue3
的同學都知道是幹嘛的,就不多說了。
effect
effect
在官網上是沒有提到這個API
的,但是在原始碼中是有的,並且我們也是可以直接使用,如下程式碼所示:
import { reactive, effect } from "vue";
const data = reactive({
foo: 1,
bar: 2
});
effect(() => {
console.log(data.foo);
});
data.foo = 10;
通常情況下我們是不會直接使用effect
的,因為effect
是一個底層的API
,在我們使用Vue3
的時候Vue
預設會幫我們呼叫effect
,所以我們的關注點通常都是在reactive
上。
但是reactive
需要和effect
配合使用才會有響應式的效果,所以我們需要了解一下effect
的作用。
effect
直接翻譯為作用
,意思是使其發生作用
,這個使其
的其
就是我們傳入的函式,所以effect
的作用就是讓我們傳入的函式發生作用,也就是執行這個函式。
但是effect
是怎麼知道我們傳入的函式需要執行呢?這些答案都在原始碼中,現在來進入正式的原始碼閱讀環節。
原始碼
Vue
的響應式系統的原始碼在packages/reactivity
目錄下,Vue3
將其單獨抽離出來為一個獨立的系統,我們可以看看這個工程的README
檔案;
根據README
檔案中的介紹,響應系統被內聯到面向使用者的生產和開發構建的包中,但是也可以單獨使用。
如果你想單獨使用的話,建議不要和Vue
混合使用,因為獨立使用的話,和Vue
的響應式系統內部的資料並不互通,這樣就會有兩個響應式系統發揮作用,這樣可能會有產生一些不可預知的問題。
響應式系統出了對Array
、Map
、WeakMap
、Set
和WeakSet
這些原生型別進行了響應式處理,對其他的原生型別,例如Date
、RegExp
、Error
等等,都沒有進行響應式處理。
reactive
reactive
的原始碼在packages/reactivity/src/reactive.ts
檔案中,還是老樣子,我們不看原始的ts
程式碼,直接看編譯後的js
程式碼,這樣更容易理解。
function reactive(target) {
// 如果對只讀的代理物件進行再次代理,那麼應該返回原始的只讀代理物件
if (isReadonly(target)) {
return target;
}
// 透過 createReactiveObject 方法建立響應式物件
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}
reactive
的原始碼很簡單,就是呼叫了createReactiveObject
方法,這個方法是一個工廠方法,用來建立響應式物件的,我們來看看這個方法的原始碼。
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
// 如果 target 不是物件,那麼直接返回 target
if (!isObject(target)) {
{
console.warn(`value cannot be made reactive: ${String(target)}`);
}
return target;
}
// 如果 target 已經是一個代理物件了,那麼直接返回 target
// 異常:如果對一個響應式物件呼叫 readonly() 方法
if (target["__v_raw" /* ReactiveFlags.RAW */] &&
!(isReadonly && target["__v_isReactive" /* ReactiveFlags.IS_REACTIVE */])) {
return target;
}
// 如果 target 已經有對應的代理物件了,那麼直接返回代理物件
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
// 對於不能被觀察的型別,直接返回 target
const targetType = getTargetType(target);
if (targetType === 0 /* TargetType.INVALID */) {
return target;
}
// 建立一個響應式物件
const proxy = new Proxy(target, targetType === 2 /* TargetType.COLLECTION */ ? collectionHandlers : baseHandlers);
// 將 target 和 proxy 儲存到 proxyMap 中
proxyMap.set(target, proxy);
// 返回 proxy
return proxy;
}
createReactiveObject
方法的原始碼也很簡單,最開始的一些程式碼都是對需要代理的target
進行一些判斷,判斷的邊界都是target
不是物件的情況和target
已經是一個代理物件的情況;
其中的核心的程式碼主要是最後七行程式碼:
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
// 對於不能被觀察的型別,直接返回 target
const targetType = getTargetType(target);
if (targetType === 0 /* TargetType.INVALID */) {
return target;
}
// 建立一個響應式物件
const proxy = new Proxy(target, targetType === 2 /* TargetType.COLLECTION */ ? collectionHandlers : baseHandlers);
// 將 target 和 proxy 儲存到 proxyMap 中
proxyMap.set(target, proxy);
// 返回 proxy
return proxy;
}
這裡有一個targetType
的判斷,那麼這個targetType
是什麼呢?我們來看看getTargetType
方法的原始碼:
// 獲取原始資料型別
const toRawType = (value) => {
// extract "RawType" from strings like "[object RawType]"
return toTypeString(value).slice(8, -1);
};
// 獲取資料型別
function targetTypeMap(rawType) {
switch (rawType) {
case 'Object':
case 'Array':
return 1 /* TargetType.COMMON */;
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return 2 /* TargetType.COLLECTION */;
default:
return 0 /* TargetType.INVALID */;
}
}
// 獲取 target 的型別
function getTargetType(value) {
return value["__v_skip" /* ReactiveFlags.SKIP */] || !Object.isExtensible(value)
? 0 /* TargetType.INVALID */
: targetTypeMap(toRawType(value));
}
這裡主要看的是Vue
寫的程式碼註釋,這裡的註釋是Vue
的ts
原始碼中的列舉型別,最後返回的值列舉型別的值:
const enum TargetType {
// 無效的資料型別,對應的值是 0,表示 Vue 不會對這種型別的資料進行響應式處理
INVALID = 0,
// 普通的資料型別,對應的值是 1,表示 Vue 會對這種型別的資料進行響應式處理
COMMON = 1,
// 集合型別,對應的值是 2,表示 Vue 會對這種型別的資料進行響應式處理
COLLECTION = 2
}
export const enum ReactiveFlags {
// 用於標識一個物件是否不可被轉為代理物件,對應的值是 __v_skip
SKIP = '__v_skip',
// 用於標識一個物件是否是響應式的代理,對應的值是 __v_isReactive
IS_REACTIVE = '__v_isReactive',
// 用於標識一個物件是否是隻讀的代理,對應的值是 __v_isReadonly
IS_READONLY = '__v_isReadonly',
// 用於標識一個物件是否是淺層代理,對應的值是 __v_isShallow
IS_SHALLOW = '__v_isShallow',
// 用於儲存原始物件的 key,對應的值是 __v_raw
RAW = '__v_raw'
}
這裡的列舉值以及含義都列出來了,然後結合原始碼,我們就可以更清晰的理解每段的程式碼的含義了。
collectionHandlers 和 baseHandlers
其實代理根據這幾年的推廣,早就不是什麼新鮮事物了,createReactiveObject
方法最後返回的就是一個代理物件;
關鍵點就在於這個代理物件的handler
,而這個handler
就是collectionHandlers
和baseHandlers
這兩個物件;
原始碼中透過targetType
來判斷使用哪個handler
,targetType
為2
的時候使用collectionHandlers
,否則使用baseHandlers
;
其實這個targetType
根據列舉值也就只有3
個值,最後走向代理的也就只有兩種情況:
targetType
為1
的時候,這個時候target
是一個普通的物件或者陣列,這個時候使用baseHandlers
;targetType
為2
的時候,這個時候target
是一個集合型別,這個時候使用collectionHandlers
;
而這兩個handler
的是透過外部傳入的,也就是createReactiveObject
方法的第三個和第四個引數,而傳入這兩個引數的地方就是reactive
方法:
function reactive(target) {
// ...
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}
可以看到的是mutableHandlers
和mutableCollectionHandlers
分別對應baseHandlers
和collectionHandlers
;
而這兩個handler
的定義在reactivity/src/baseHandlers.ts
和reactivity/src/collectionHandlers.ts
中;
感興趣的可以去翻看一下這兩個檔案的原始碼,這裡還是貼出打包之後的程式碼,先從baseHandlers
開始;
baseHandlers
注意這裡的baseHandlers
指向的是mutableHandlers
,mutableHandlers
是baseHandlers
的一個export
;
const mutableHandlers = {
get: get$1,
set: set$1,
deleteProperty,
has: has$1,
ownKeys
};
這裡分別定義了get
、set
、deleteProperty
、has
、ownKeys
這幾個方法攔截器,簡單介紹一下作用:
get
:攔截物件的getter
操作,比如obj.name
;set
:攔截物件的setter
操作,比如obj.name = '田八'
;deleteProperty
:攔截delete
操作,比如delete obj.name
;has
:攔截in
操作,比如'name' in obj
;ownKeys
:攔截Object.getOwnPropertyNames
、Object.getOwnPropertySymbols
、Object.keys
等操作;
更具體的可以看看MDN的介紹;
再來看看這些個攔截器的具體實現。
get
const get$1 = /*#__PURE__*/ createGetter();
function createGetter(isReadonly = false, shallow = false) {
// 閉包返回 get 攔截器方法
return function get(target, key, receiver) {
// 如果訪問的是 __v_isReactive 屬性,那麼返回 isReadonly 的取反值
if (key === "__v_isReactive" /* ReactiveFlags.IS_REACTIVE */) {
return !isReadonly;
}
// 如果訪問的是 __v_isReadonly 屬性,那麼返回 isReadonly 的值
else if (key === "__v_isReadonly" /* ReactiveFlags.IS_READONLY */) {
return isReadonly;
}
// 如果訪問的是 __v_isShallow 屬性,那麼返回 shallow 的值
else if (key === "__v_isShallow" /* ReactiveFlags.IS_SHALLOW */) {
return shallow;
}
// 如果訪問的是 __v_raw 屬性,並且有一堆條件滿足,那麼返回 target
else if (key === "__v_raw" /* ReactiveFlags.RAW */ &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap).get(target)) {
return target;
}
// target 是否是陣列
const targetIsArray = isArray(target);
// 如果不是隻讀的
if (!isReadonly) {
// 如果是陣列,並且訪問的是陣列的一些方法,那麼返回對應的方法
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver);
}
// 如果訪問的是 hasOwnProperty 方法,那麼返回 hasOwnProperty 方法
if (key === 'hasOwnProperty') {
return hasOwnProperty;
}
}
// 獲取 target 的 key 屬性值
const res = Reflect.get(target, key, receiver);
// 如果是內建的 Symbol,或者是不可追蹤的 key,那麼直接返回 res
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res;
}
// 如果不是隻讀的,那麼進行依賴收集
if (!isReadonly) {
track(target, "get" /* TrackOpTypes.GET */, key);
}
// 如果是淺的,那麼直接返回 res
if (shallow) {
return res;
}
// 如果 res 是 ref,對返回的值進行解包
if (isRef(res)) {
// 對於陣列和整數型別的 key,不進行解包
return targetIsArray && isIntegerKey(key) ? res : res.value;
}
// 如果 res 是物件,遞迴代理
if (isObject(res)) {
// 將返回的值也轉換為代理。我們在這裡進行 isObject 檢查,以避免無效的值警告。
// 還需要延遲訪問 readonly 和 reactive,以避免迴圈依賴。
return isReadonly ? readonly(res) : reactive(res);
}
// 返回 res
return res;
};
}
稍微有點複雜,但是也不難理解,我來拆解一下:
function get(target, key, receiver) {
// 如果訪問的是 __v_isReactive 屬性,那麼返回 isReadonly 的取反值
if (key === "__v_isReactive" /* ReactiveFlags.IS_REACTIVE */) {
return !isReadonly;
}
// 如果訪問的是 __v_isReadonly 屬性,那麼返回 isReadonly 的值
else if (key === "__v_isReadonly" /* ReactiveFlags.IS_READONLY */) {
return isReadonly;
}
// 如果訪問的是 __v_isShallow 屬性,那麼返回 shallow 的值
else if (key === "__v_isShallow" /* ReactiveFlags.IS_SHALLOW */) {
return shallow;
}
// 如果訪問的是 __v_raw 屬性,並且有一堆條件滿足,那麼返回 target
else if (key === "__v_raw" /* ReactiveFlags.RAW */ &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap).get(target)) {
return target;
}
// ...
};
這一段程式碼是為了處理一些特殊的屬性,這些都是Vue
內部定義好的,就是上面提到過的列舉值,用於判斷是否是reactive
、readonly
、shallow
等等。
這一段程式碼對於我們理解原始碼並不重要,重要的是下面一段:
function get(target, key, receiver) {
// ...
// target 是否是陣列
const targetIsArray = isArray(target);
// 如果不是隻讀的
if (!isReadonly) {
// 如果是陣列,並且訪問的是陣列的一些方法,那麼返回對應的方法
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver);
}
// 如果訪問的是 hasOwnProperty 方法,那麼返回 hasOwnProperty 方法
if (key === 'hasOwnProperty') {
return hasOwnProperty;
}
}
// 獲取 target 的 key 屬性值
const res = Reflect.get(target, key, receiver);
// 如果是內建的 Symbol,或者是不可追蹤的 key,那麼直接返回 res
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res;
}
// 如果不是隻讀的,那麼進行依賴收集
if (!isReadonly) {
track(target, "get" /* TrackOpTypes.GET */, key);
}
// 如果是淺的,那麼直接返回 res
if (shallow) {
return res;
}
// 如果 res 是 ref,對返回的值進行解包
if (isRef(res)) {
// 對於陣列和整數型別的 key,不進行解包
return targetIsArray && isIntegerKey(key) ? res : res.value;
}
// 如果 res 是物件,遞迴代理
if (isObject(res)) {
// 將返回的值也轉換為代理。我們在這裡進行 isObject 檢查,以避免無效的值警告。
// 還需要延遲訪問 readonly 和 reactive,以避免迴圈依賴。
return isReadonly ? readonly(res) : reactive(res);
}
// 返回 res
return res;
};
這一段還是太多了,但是其實每段程式碼都是為了完成一個獨立的需求,我們再來拆解一下:
- 對陣列的方法訪問處理
function get(target, key, receiver) {
// ...
// target 是否是陣列
const targetIsArray = isArray(target);
// 如果不是隻讀的
if (!isReadonly) {
// 如果是陣列,並且訪問的是陣列的一些方法,那麼返回對應的方法
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver);
}
// 如果訪問的是 hasOwnProperty 方法,那麼返回 hasOwnProperty 方法
if (key === 'hasOwnProperty') {
return hasOwnProperty;
}
}
// ...
};
這一段程式碼是為了處理陣列的一些方法,比如push
、pop
等等,如果我們在呼叫這些方法的時候,就會進入這一段程式碼,然後返回對應的方法,例如:
const arr = reactive([1, 2, 3]);
arr.push(4);
這些方法都在arrayInstrumentations
中,這次不做重點分析,後面會專門講解。
- 獲取返回值,返回值的特別對待
function get(target, key, receiver) {
// ...
// 獲取 target 的 key 屬性值
const res = Reflect.get(target, key, receiver);
// ...
};
走到這裡,就需要獲取target
的key
屬性值了,這裡使用了Reflect.get
;
這個方法是ES6
中新增的,用於訪問物件的屬性,和target[key]
是等價的,但是Reflect.get
可以傳入receiver
,這個引數是用來繫結this
的;
這是為瞭解決Proxy
的this
指向問題,這裡不做過多的解釋,後面會專門講解,Reflect
不瞭解的看:MDN Reflect
- 特殊屬性的不進行依賴收集
function get(target, key, receiver) {
// ...
// 如果是內建的 Symbol,或者是不可追蹤的 key,那麼直接返回 res
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res;
}
// ...
};
這一步是為了過濾一些特殊的屬性,例如原生的Symbol
型別的屬性,如:Symbol.iterator
、Symbol.toStringTag
等等,這些屬性不需要進行依賴收集,因為它們是內建的,不會改變;
還有一些不可追蹤的屬性,如:__proto__
、__v_isRef
、__isVue
這些屬性也不需要進行依賴收集;
- 進行依賴收集
function get(target, key, receiver) {
// ...
// 如果不是隻讀的,那麼進行依賴收集
if (!isReadonly) {
track(target, "get" /* TrackOpTypes.GET */, key);
}
// ...
};
這一步是為了進行依賴收集,這裡呼叫了track
方法,這個方法在effect
中會用到,稍後會講解;
- 淺的不進行遞迴代理
function get(target, key, receiver) {
// ...
// 如果是淺的,那麼直接返回 res
if (shallow) {
return res;
}
// ...
};
這一步是為了處理shallow
的情況,如果是shallow
的,那麼就不需要進行遞迴代理了,直接返回res
即可;
- 對返回值進行解包
function get(target, key, receiver) {
// ...
// 如果 res 是 ref,對返回的值進行解包
if (isRef(res)) {
// 對於陣列和整數型別的 key,不進行解包
return targetIsArray && isIntegerKey(key) ? res : res.value;
}
// ...
};
這一步是為了處理ref
的情況,如果res
是ref
,那麼就對res
進行解包,這裡有一個判斷,如果是陣列,並且key
是整數型別,那麼就不進行解包;
- 對返回值進行代理
function get(target, key, receiver) {
// ...
// 如果 res 是物件,那麼對返回的值進行代理
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res);
}
// ...
};
如果是物件,那麼就對res
進行代理,這裡有一個判斷,如果是readonly
的,那麼就使用readonly
方法進行代理,否則就使用reactive
方法進行代理;
最後就是返回res
了,這裡就是Vue3
的get
方法的全部內容了,其實拆分下來就容易理解多了,下面我們來看看Vue3
的set
方法;
set
const set$1 = /*#__PURE__*/ createSetter();
function createSetter(shallow = false) {
// 閉包返回一個 set 方法
return function set(target, key, value, receiver) {
// 獲取舊值
let oldValue = target[key];
// 如果舊值是隻讀的,並且是 ref,並且新值不是 ref,那麼直接返回 false,代表設定失敗
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false;
}
// 如果不是淺的
if (!shallow) {
// 如果新值不是淺的,並且不是隻讀的
if (!isShallow(value) && !isReadonly(value)) {
// 獲取舊值的原始值
oldValue = toRaw(oldValue);
// 獲取新值的原始值
value = toRaw(value);
}
// 如果目標物件不是陣列,並且舊值是 ref,並且新值不是 ref,那麼設定舊值的 value 為新值,並且返回 true,代表設定成功
// ref 的值是在 value 屬性上的,這裡判斷了舊值的代理型別,所以設定到了舊值的 value 上
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value;
return true;
}
}
// 如果是陣列,並且 key 是整數型別
const hadKey = isArray(target) && isIntegerKey(key)
// 如果 key 小於陣列的長度,那麼就是有這個 key
? Number(key) < target.length
// 如果不是陣列,那麼就是普通物件,直接判斷是否有這個 key
: hasOwn(target, key);
// 透過 Reflect.set 設定值
const result = Reflect.set(target, key, value, receiver);
// 如果目標物件是原始資料的原型鏈中的某個元素,則不會觸發依賴收集
if (target === toRaw(receiver)) {
// 如果沒有這個 key,那麼就是新增了一個屬性,觸發 add 事件
if (!hadKey) {
trigger(target, "add" /* TriggerOpTypes.ADD */, key, value);
}
// 如果有這個 key,那麼就是修改了一個屬性,觸發 set 事件
else if (hasChanged(value, oldValue)) {
trigger(target, "set" /* TriggerOpTypes.SET */, key, value, oldValue);
}
}
// 返回結果,這個結果為 boolean 型別,代表是否設定成功
// 只是代理相關,,和業務無關,必須要返回是否設定成功的結果
return result;
};
}
set
方法的實現其實整體要比get
方法的實現要複雜一些,雖然程式碼比get
要少一些,不過整體梳理下來,大體分為下面幾個步驟:
- 獲取舊值
function set(target, key, value, receiver) {
// 獲取舊值
let oldValue = target[key];
// ...
};
這裡的舊值就是target[key]
的值,舊值在Vue3
中有很多用處,會貫穿整個流程,這裡先不展開講,後面會講到;
- 判斷舊值是否是隻讀的
function set(target, key, value, receiver) {
// ...
// 如果舊值是隻讀的,並且是 ref,並且新值不是 ref,那麼直接返回 false,代表設定失敗
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false;
}
// ...
};
只讀的ref
是不能被修改的,所以這裡就直接返回false
了,代表設定失敗;
但是這裡需要有很多的條件,首先舊值必須是隻讀的,其次舊值必須是ref
,最後新值不能是ref
,如下面的例子:
const refObj = ref({
a: 1,
b: 2
});
const readonlyObj = readonly(refObj);
const obj = reactive({
readonlyObj
})
obj.readonlyObj = 10;
console.log(obj.readonlyObj); // 設定失敗
obj.readonlyObj = ref(10);
console.log(obj.readonlyObj); // 設定成功
很奇怪的判定,個人的知識儲備量還不夠,沒想明白為什麼要有這麼樣一個的判定才會設定失敗。
- 判斷是否是淺的
function set(target, key, value, receiver) {
// ...
// 如果不是淺的
if (!shallow) {
// ...
}
// ...
};
在判斷是否不是淺層響應的時候,這個引數是透過閉包儲存下來的,不是淺層響應的時候,這個內部會做兩件事情:
- 獲取舊值的原始值和新值的原始值
function set(target, key, value, receiver) {
// ...
// 如果不是淺的
if (!shallow) {
// 如果新值不是淺的,並且不是隻讀的
if (!isShallow(value) && !isReadonly(value)) {
// 獲取舊值的原始值
oldValue = toRaw(oldValue);
// 獲取新值的原始值
value = toRaw(value);
}
}
// ...
};
這裡需要先判斷新值是否不是淺層響應的,並且不是隻讀的,如果是的話,那麼就不需要獲取原始值了,因為這個時候新值就是原始值了;
這裡因為如果新值是淺層響應的,那就說明這個響應式物件的元素只有一層響應式,只會關心當前物件的響應式,當前物件的元素是否是響應式的就不關心了,所以不用獲取原始值,直接覆蓋原則就可以了;
deleteProperty
function deleteProperty(target, key) {
// 當前物件是否有這個 key
const hadKey = hasOwn(target, key);
// 舊值
const oldValue = target[key];
// 透過 Reflect.deleteProperty 刪除屬性
const result = Reflect.deleteProperty(target, key);
// 如果刪除成功,並且當前物件有這個 key,那麼就觸發 delete 事件
if (result && hadKey) {
trigger(target, "delete" /* TriggerOpTypes.DELETE */, key, undefined, oldValue);
}
// 返回結果,這個結果為 boolean 型別,代表是否刪除成功
return result;
}
deleteProperty
方法的實現對比get
和set
方法的實現都要簡單很多,也沒有什麼特別的地方,就是透過Reflect.deleteProperty
刪除屬性,然後透過trigger
觸發delete
事件,最後返回刪除是否成功的結果;
has
function has$1(target, key) {
// 透過 Reflect.has 判斷當前物件是否有這個 key
const result = Reflect.has(target, key);
// 如果當前物件不是 Symbol 型別,或者當前物件不是內建的 Symbol 型別,那麼就觸發 has 事件
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, "has" /* TrackOpTypes.HAS */, key);
}
// 返回結果,這個結果為 boolean 型別,代表當前物件是否有這個 key
return result;
}
has
方法的實現也是比較簡單的,就是透過Reflect.has
判斷當前物件是否有這個 key,然後透過track
觸發has
事件,最後返回是否有這個 key
的結果;
ownKeys
function ownKeys(target) {
// 直接觸發 iterate 事件
track(target, "iterate" /* TrackOpTypes.ITERATE */, isArray(target) ? 'length' : ITERATE_KEY);
// 透過 Reflect.ownKeys 獲取當前物件的所有 key
return Reflect.ownKeys(target);
}
ownKeys
方法的實現也是比較簡單的,直接觸發iterate
事件,然後透過Reflect.ownKeys
獲取當前物件的所有 key,最後返回這些 key;
注意點在於對陣列的特殊處理,如果當前物件是陣列的話,那麼就會觸發length
的iterate
事件,如果不是陣列的話,那麼就會觸發ITERATE_KEY
的iterate
事件;
這一塊的區別都是在track
方法中才會有體現,這個就是響應式的核心思路,後面會詳細講解;
effect
上面講完了reactive
方法,接下來就是effect
方法,effect
方法的作用是建立一個副作用函式,這個函式會在依賴的資料發生變化的時候執行;
依賴收集和觸發更新的過程先不要著急,等講完effect
方法之後,再來分析這個過程,先看看effect
方法的實現:
function effect(fn, options) {
// 如果 fn 物件上有 effect 屬性
if (fn.effect) {
// 那麼就將 fn 替換為 fn.effect.fn
fn = fn.effect.fn;
}
// 建立一個響應式副作用函式
const _effect = new ReactiveEffect(fn);
// 如果有配置項
if (options) {
// 將配置項合併到響應式副作用函式上
extend(_effect, options);
// 如果配置項中有 scope 屬性(該屬性的作用是指定副作用函式的作用域)
if (options.scope)
// 那麼就將 scope 屬性記錄到響應式副作用函式上(類似一個作用域鏈)
recordEffectScope(_effect, options.scope);
}
// 如果沒有配置項,或者配置項中沒有 lazy 屬性,或者配置項中的 lazy 屬性為 false
if (!options || !options.lazy) {
// 那麼就執行響應式副作用函式
_effect.run();
}
// 將 _effect.run 的 this 指向 _effect
const runner = _effect.run.bind(_effect);
// 將響應式副作用函式賦值給 runner.effect
runner.effect = _effect;
// 返回 runner
return runner;
}
其實這裡的原始碼一下並不能看明白具體想要幹嘛,而且內部的呼叫,或者說資料的指向也比較複雜;
但是梳理下來,這裡的關鍵點有兩個部分:
- 建立一個響應式副作用函式
const _effect = new ReactiveEffect(fn)
; - 返回一個
runner
函式,可以透過這個函式來執行響應式副作用函式;
ReactiveEffect
先來分析下ReactiveEffect
這個類,這個類的作用是建立一個響應式副作用函式,這個函式會在依賴的資料發生變化的時候執行;
class ReactiveEffect {
constructor(fn, scheduler = null, scope) {
// 副作用函式
this.fn = fn;
// 排程器,用於控制副作用函式何時執行
this.scheduler = scheduler;
// 標誌位,用於標識當前 ReactiveEffect 物件是否處於活動狀態
this.active = true;
// 響應式依賴項的集合
this.deps = [];
// 父級作用域
this.parent = undefined;
// 記錄當前 ReactiveEffect 物件的作用域
recordEffectScope(this, scope);
}
run() {
// ...
}
stop() {
// ...
}
}
ReactiveEffect
這個類的實現主要體現在兩個方法上,一個是run
方法,一個是stop
方法;
其他的屬性都是用來記錄一些資料的,比如fn
屬性就是用來記錄副作用函式的,scheduler
屬性就是用來記錄排程器的,active
屬性就是用來記錄當前ReactiveEffect
物件是否處於活動狀態的;
這些屬性的具體作用將在下面的分析中講解,先來看看run
方法的實現;
run
function run() {
// 如果當前 ReactiveEffect 物件不處於活動狀態,直接返回 fn 的執行結果
if (!this.active) {
return this.fn();
}
// 尋找當前 ReactiveEffect 物件的最頂層的父級作用域
let parent = activeEffect;
let lastShouldTrack = shouldTrack;
while (parent) {
if (parent === this) {
return;
}
parent = parent.parent;
}
try {
// 記錄父級作用域為當前活動的 ReactiveEffect 物件
this.parent = activeEffect;
// 將當前活動的 ReactiveEffect 物件設定為 “自己”
activeEffect = this;
// 將 shouldTrack 設定為 true (表示是否需要收集依賴)
shouldTrack = true;
// effectTrackDepth 用於標識當前的 effect 呼叫棧的深度,執行一次 effect 就會將 effectTrackDepth 加 1
trackOpBit = 1 << ++effectTrackDepth;
// 這裡是用於控制 "effect呼叫棧的深度" 在一個閾值之內
if (effectTrackDepth <= maxMarkerBits) {
// 初始依賴追蹤標記
initDepMarkers(this);
}
else {
// 清除所有的依賴追蹤標記
cleanupEffect(this);
}
// 執行副作用函式,並返回執行結果
return this.fn();
}
finally {
// 如果 effect呼叫棧的深度 沒有超過閾值
if (effectTrackDepth <= maxMarkerBits) {
// 確定最終的依賴追蹤標記
finalizeDepMarkers(this);
}
// 執行完畢會將 effectTrackDepth 減 1
trackOpBit = 1 << --effectTrackDepth;
// 執行完畢,將當前活動的 ReactiveEffect 物件設定為 “父級作用域”
activeEffect = this.parent;
// 將 shouldTrack 設定為上一個值
shouldTrack = lastShouldTrack;
// 將父級作用域設定為 undefined
this.parent = undefined;
// 延時停止,這個標誌是在 stop 方法中設定的
if (this.deferStop) {
this.stop();
}
}
}
整體梳理下來,run
方法的作用就是執行副作用函式,並且在執行副作用函式的過程中,會收集依賴;
整體的流程還是非常複雜的,但是這裡的核心思想是各種標識位的設定,以及在執行副作用函式的過程中,會收集依賴;
這裡的流程沒必要一下就全都瞭解,現在只需要記住下面這樣的流程就可以了:
stop
function stop() {
// 如果當前 活動的 ReactiveEffect 物件是 “自己”
// 延遲停止,需要執行完當前的副作用函式之後再停止
if (activeEffect === this) {
// 在 run 方法中會判斷 deferStop 的值,如果為 true,就會執行 stop 方法
this.deferStop = true;
}
// 如果當前 ReactiveEffect 物件處於活動狀態
else if (this.active) {
// 清除所有的依賴追蹤標記
cleanupEffect(this);
// 如果有 onStop 回撥函式,就執行
if (this.onStop) {
this.onStop();
}
// 將 active 設定為 false
this.active = false;
}
}
stop
方法的作用就是停止當前的ReactiveEffect
物件,停止之後,就不會再收集依賴了;
這裡的activeEffect
和this
並不是每次都相等的,因為activeEffect
會跟著呼叫棧的深度而變化,而this
則是固定的;
this.active
標識的自身是否處在活動狀態,因為巢狀的ReactiveEffect
物件,activeEffect
並不一定指向自己,而this.active
則是自身的狀態;
依賴收集
講了reactive
和effect
之後,我們就可以來講講依賴收集了;
上面講了這麼多,他們兩個好像還沒有聯絡起來,好像是相互獨立的,而他們的聯絡的紐帶就是activeEffect
;
常聽人說響應式系統在getter
中收集依賴,在setter
中觸發依賴,現在回頭看看getter
是怎麼收集依賴的;
track
現在回憶一下getter
的實現,裡面有這樣的一段程式碼:
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
// ...
// 如果不是隻讀的,就會收集依賴
if (!isReadonly) {
track(target, "get" /* TrackOpTypes.GET */, key);
}
// ...
return res;
};
}
track
方法的作用就是收集依賴,它的實現如下:
const targetMap = new WeakMap();
/**
* 收集依賴
* @param target 指向的物件
* @param type 操作型別
* @param key 指向物件的 key
*/
function track(target, type, key) {
// 如果 shouldTrack 為 false,並且 activeEffect 沒有值的話,就不會收集依賴
if (shouldTrack && activeEffect) {
// 如果 targetMap 中沒有 target,就會建立一個 Map
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// 如果 depsMap 中沒有 key,就會建立一個 Set
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = createDep()));
}
// 將當前的 ReactiveEffect 物件新增到 dep 中
const eventInfo = {
effect: activeEffect,
target,
type,
key
};
// 如果 dep 中沒有當前的 ReactiveEffect 物件,就會新增進去
trackEffects(dep, eventInfo);
}
}
在這裡我們發現了兩個老熟人,一個是shouldTrack
,一個是activeEffect
,這兩個變數都是在effect
方法中出現過的;
shouldTrack
在上面也講過,它的作用就是控制是否收集依賴,暫時不用深入;
activeEffect
就是我們剛剛講的ReactiveEffect
物件,它指向的就是當前正在執行的副作用函式;
track
方法的作用就是收集依賴,它的實現非常簡單,就是在targetMap
中記錄下target
和key
;
targetMap
是一個WeakMap
,它的鍵是target
,值是一個Map
,這個Map
的鍵是key
,值是一個Set
;
這意味著,如果我們在操作target
的key
時,就會收集依賴,這個時候,target
和key
就會被記錄到targetMap
中,用程式碼表示就是:
const obj = {
a: 1,
b: 2
};
const targetMap = new WeakMap();
// 我在操作 obj.a 的時候,就會收集依賴
obj.a;
// 這個時候,targetMap 中就會記錄下 obj 和 a
let depsMap = targetMap.get(obj);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// createDep 實現很簡單,就不在講解的程式碼裡面單獨寫出來了,具體就是一個 Set,多了兩個屬性,w 和 n
const createDep = (effects) => {
const dep = new Set(effects);
dep.w = 0; // 指向的是 watcher 物件的唯一標識
dep.n = 0; // 指向的是不同的 dep 的唯一標識
return dep;
};
let dep = depsMap.get("a");
if (!dep) {
depsMap.set("a", (dep = createDep()));
}
// dep 就是一個 Set,裡面存放的就是當前的 ReactiveEffect 物件
dep.add(activeEffect);
上面就是一個收集依賴的過程,我們可以看到,targetMap
中記錄的是target
和key
,而dep
中記錄的是ReactiveEffect
物件;
trigger
現在我們來看看trigger
方法,它的作用就是觸發依賴,它的實現如下:
/**
* 觸發依賴
* @param target 指向的物件
* @param type 操作型別
* @param key 指向物件的 key
* @param newValue 新值
* @param oldValue 舊值
* @param oldTarget 舊的 target
*/
function trigger(target, type, key, newValue, oldValue, oldTarget) {
// 獲取 targetMap 中的 depsMap
const depsMap = targetMap.get(target);
if (!depsMap) {
// never been tracked
return;
}
// 建立一個陣列,用來存放需要執行的 ReactiveEffect 物件
let deps = [];
// 如果 type 為 clear,就會將 depsMap 中的所有 ReactiveEffect 物件都新增到 deps 中
if (type === "clear" /* TriggerOpTypes.CLEAR */) {
// 執行所有的 副作用函式
deps = [...depsMap.values()];
}
// 如果 key 為 length ,並且 target 是一個陣列
else if (key === 'length' && isArray(target)) {
// 修改陣列的長度,會導致陣列的索引發生變化
// 但是隻有兩種情況,一種是陣列的長度變大,一種是陣列的長度變小
// 如果陣列的長度變大,那麼執行所有的副作用函式就可以了
// 如果陣列的長度變小,那麼就需要執行索引大於等於新陣列長度的副作用函式
const newLength = Number(newValue);
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= newLength) {
deps.push(dep);
}
});
}
// 其他情況
else {
// key 不是 undefined,就會將 depsMap 中 key 對應的 ReactiveEffect 物件新增到 deps 中
// void 0 就是 undefined
if (key !== void 0) {
deps.push(depsMap.get(key));
}
// 執行 add、delete、set 操作時,就會觸發的依賴變更
switch (type) {
// 如果 type 為 add,就會觸發的依賴變更
case "add" /* TriggerOpTypes.ADD */:
// 如果 target 不是陣列,就會觸發迭代器
if (!isArray(target)) {
// ITERATE_KEY 再上面介紹過,用來標識迭代屬性
// 例如:for...in、for...of,這個時候依賴會收集到 ITERATE_KEY 上
// 而不是收集到具體的 key 上
deps.push(depsMap.get(ITERATE_KEY));
// 如果 target 是一個 Map,就會觸發 MAP_KEY_ITERATE_KEY
if (isMap(target)) {
// MAP_KEY_ITERATE_KEY 同上面的 ITERATE_KEY 一樣
// 不同的是,它是用來標識 Map 的迭代器
// 例如:Map.prototype.keys()、Map.prototype.values()、Map.prototype.entries()
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
}
}
// 如果 key 是一個數字,就會觸發 length 依賴
else if (isIntegerKey(key)) {
// 因為陣列的索引是可以透過 arr[0] 這種方式來訪問的
// 也可以透過這種方式來修改陣列的值,所以會觸發 length 依賴
deps.push(depsMap.get('length'));
}
break;
// 如果 type 為 delete,就會觸發的依賴變更
case "delete" /* TriggerOpTypes.DELETE */:
// 如果 target 不是陣列,就會觸發迭代器,同上面的 add 操作
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY));
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
}
}
break;
// 如果 type 為 set,就會觸發的依賴變更
case "set" /* TriggerOpTypes.SET */:
// 如果 target 是一個 Map,就會觸發迭代器,同上面的 add 操作
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY));
}
break;
}
}
// 建立一個 eventInfo 物件,主要是除錯的時候會用到
const eventInfo = {
target,
type,
key,
newValue,
oldValue,
oldTarget
};
// 如果 deps 的長度為 1,就會直接執行
if (deps.length === 1) {
if (deps[0]) {
{
triggerEffects(deps[0], eventInfo);
}
}
}
else {
// 如果 deps 的長度大於 1,這個時候會組裝成一個陣列,然後再執行
// 這個時候呼叫就類似一個呼叫棧
const effects = [];
for (const dep of deps) {
if (dep) {
effects.push(...dep);
}
}
{
triggerEffects(createDep(effects), eventInfo);
}
}
}
tigger
函式的作用就是觸發依賴,當我們修改資料的時候,就會觸發依賴,然後執行依賴中的副作用函式。
在這裡的實現其實並沒有執行,主要是收集一些需要執行的副作用函式,然後在丟給triggerEffects
函式去執行。
這裡的難點在於區分不同的操作型別,然後收集不同的副作用函式,並且需要理解為什麼要這樣區分;
主要是這節寫的有點多,所以這一塊暫時不在這裡展開,後面會單獨寫一篇文章來講解。
現在我們來看看triggerEffects
函式:
function triggerEffects(dep, debuggerEventExtraInfo) {
// 如果 dep 不是陣列,就會將 dep 轉換成陣列,因為這裡的 dep 可能是一個 Set 物件
const effects = isArray(dep) ? dep : [...dep];
// 執行 computed 依賴
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo);
}
}
// 執行其他依賴
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo);
}
}
}
這裡沒什麼特殊的,就是轉換一下dep
,然後執行computed
依賴和其他依賴,主要還是在triggerEffect
函式:
function triggerEffect(effect, debuggerEventExtraInfo) {
// 如果 effect 不是 activeEffect,或者 effect 允許遞迴,就會執行
if (effect !== activeEffect || effect.allowRecurse) {
// 如果 effect.onTrigger 存在,就會執行,只有開發模式下才會執行
if (effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo));
}
// 如果 effect 是一個排程器,就會執行 scheduler
if (effect.scheduler) {
effect.scheduler();
}
// 否則直接執行 effect.run()
else {
effect.run();
}
}
}
這裡的邏輯也很簡單,但是如果結合effect
函式,就會發現這裡的實現非常的巧妙。
這裡的 effect.scheduler
和effect.run
,在我們看effect
函式的時候,就已經出現過了;
run
就是呼叫副作用函式,scheduler
是排程器,允許使用者自定義呼叫副作用函式的時機。
還是因為這一篇寫的太多了,所以這裡就不展開了,後面會單獨寫一篇文章來講解。
動手時間
上面講了那麼多,還不如自己動一下手來實現這一整套流程,這樣才能更好的理解。
首先我們梳理一下整個流程:
- 建立一個響應式物件
- 建立一個副作用函式
- 訪問響應式物件,觸發依賴收集
- 修改響應式物件,觸發依賴執行
// 1. 建立一個響應式物件
const state = reactive({
name: '田八',
age: 18
});
// 2. 建立一個副作用函式
effect(() => {
// 3. 訪問響應式物件,觸發依賴收集
console.log(state.name);
});
// 4. 修改響應式物件,觸發依賴執行
state.age = 19;
建立一個響應式物件
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
// 依賴收集
track(target, key);
return Reflect.get(target, key);
},
set(target, key, value) {
const res = Reflect.set(target, key, value);
// 依賴觸發
trigger(target, key);
return res;
}
});
}
這裡只做最簡單的實現,所以沒有做深度監聽,只是簡單的監聽了一層,並且只有get
和set
兩個鉤子,只對Object
型別的資料做了監聽。
建立一個副作用函式
let activeEffect = null;
function effect(fn) {
const _effect = new ReactiveEffect(fn);
_effect.run();
}
class ReactiveEffect {
constructor(fn) {
this.fn = fn;
this.deps = [];
}
run() {
activeEffect = this;
this.fn();
activeEffect = null;
}
}
這裡的ReactiveEffect
類,主要是用來儲存副作用函式的,然後在run
函式中,將activeEffect
設定為當前的ReactiveEffect
例項,這樣在track
函式中,就可以拿到當前的ReactiveEffect
例項。
依賴收集
const targetMap = new WeakMap();
function track(target, key) {
if (activeEffect) {
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
}
這裡的主流程和Vue3
的原始碼是一樣的,並沒有做什麼改動,確實是非常的巧妙。
依賴觸發
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => {
effect.run();
});
}
}
這裡簡化了流程,直接遍歷dep
,然後執行effect
的run
函式。
線上地址:https://codesandbox.io/s/magical-knuth-mjyh7j?file=/src/main.js
總結
這一篇文章,主要是講解了Vue3
的響應式原理,以及如何手動實現一個簡單的響應式系統。
整個響應式系統的實現,主要是圍繞的effect
函式,reactive
函式,track
函式,trigger
函式這四個函式。
每個函式都只做自己的事情,各司其職:
effect
函式:建立一個副作用函式,主要的作用是來執行副作用函式reactive
函式:建立一個響應式物件,主要的作用是來監聽物件的變化track
函式:依賴收集,主要收集的就是effect
函式trigger
函式:依賴觸發,主要的作用是來觸發track
函式收集的effect
函式
這樣的設計,讓整個響應式系統的實現變得非常的簡單,也讓整個系統的可維護性變得非常的高。
這裡的巧妙點在於依賴收集,當呼叫副作用函式時,副作用函式裡面的響應式物件在呼叫時,會觸發get
鉤子;
get
中呼叫track
函式收集activeEffect
,這個時候activeEffect
是一定存在的,並且activeEffect
中的副作用函式是一定引用了這個響應式物件的,所以這個時候就可以將這個響應式物件和activeEffect
關聯起來。
將當前的物件作為key
,將activeEffect
作為value
,儲存到targetMap
中,這樣就完成了依賴收集。
在響應式物件的set
鉤子中,呼叫trigger
函式,將targetMap
中的activeEffect
取出來,然後執行activeEffect
的run
函式,這樣就完成了依賴觸發。
今天就到了這裡,如有不對的地方,歡迎大家指正。
大家好,這裡是田八的【原始碼&庫】系列,
Vue3
的原始碼閱讀計劃,Vue3
的原始碼閱讀計劃不出意外每週一更,歡迎大家關注。如果想一起交流的話,可以點選這裡一起共同交流成長
首發在掘金,無任何引流的意思,後續文章不再強調。
系列章節: