注: 為了直觀的看到 Vue3 的實現邏輯, 本文移除了邊緣情況處理、相容處理、DEV環境的特殊邏輯等, 只保留了核心邏輯
vue-next/reactivity 實現了 Vue3 的響應性, reactivity 提供了以下介面:
export {
ref, // 代理基本型別
shallowRef, // ref 的淺代理模式
isRef, // 判斷一個值是否是 ref
toRef, // 把響應式物件的某個 key 轉為 ref
toRefs, // 把響應式物件的所有 key 轉為 ref
unref, // 返回 ref.value 屬性
proxyRefs,
customRef, // 自行實現 ref
triggerRef, // 觸發 customRef
Ref, // 型別宣告
ToRefs, // 型別宣告
UnwrapRef, // 型別宣告
ShallowUnwrapRef, // 型別宣告
RefUnwrapBailTypes // 型別宣告
} from './ref'
export {
reactive, // 生成響應式物件
readonly, // 生成只讀物件
isReactive, // 判斷值是否是響應式物件
isReadonly, // 判斷值是否是隻讀物件
isProxy, // 判斷值是否是 proxy
shallowReactive, // 生成淺響應式物件
shallowReadonly, // 生成淺只讀物件
markRaw, // 讓資料不可被代理
toRaw, // 獲取代理物件的原始物件
ReactiveFlags, // 型別宣告
DeepReadonly // 型別宣告
} from './reactive'
export {
computed, // 計算屬性
ComputedRef, // 型別宣告
WritableComputedRef, // 型別宣告
WritableComputedOptions, // 型別宣告
ComputedGetter, // 型別宣告
ComputedSetter // 型別宣告
} from './computed'
export {
effect, // 定義副作用函式, 返回 effect 本身, 稱為 runner
stop, // 停止 runner
track, // 收集 effect 到 Vue3 內部的 targetMap 變數
trigger, // 執行 targetMap 變數儲存的 effects
enableTracking, // 開始依賴收集
pauseTracking, // 停止依賴收集
resetTracking, // 重置依賴收集狀態
ITERATE_KEY, // 固定引數
ReactiveEffect, // 型別宣告
ReactiveEffectOptions, // 型別宣告
DebuggerEvent // 型別宣告
} from './effect'
export {
TrackOpTypes, // track 方法的 type 引數的列舉值
TriggerOpTypes // trigger 方法的 type 引數的列舉值
} from './operations'
一、名詞解釋
-
target: 普通的 JS 物件
-
reactive:
@vue/reactivity
提供的函式, 接收一個物件, 並返回一個 代理物件, 即響應式物件 -
shallowReactive:
@vue/reactivity
提供的函式, 用來定義淺響應物件 -
readonly:
@vue/reactivity
提供的函式, 用來定義只讀物件 -
shallowReadonly:
@vue/reactivity
提供的函式, 用來定義淺只讀物件 -
handlers: Proxy 物件暴露的鉤子函式, 有
get()
、set()
、deleteProperty()
、ownKeys()
等, 可以參考MDN -
targetMap:
@vue/reactivity
內部變數, 儲存了所有依賴 -
effect:
@vue/reactivit
提供的函式, 用於定義副作用,effect(fn, options)
的引數就是副作用函式 -
watchEffect:
@vue/runtime-core
提供的函式, 基於 effect 實現 -
track:
@vue/reactivity
內部函式, 用於收集依賴 -
trigger:
@vue/reactivity
內部函式, 用於消費依賴 -
scheduler: effect 的排程器, 允許使用者自行實現
二、Vue3 實現響應式的思路
先看下邊的流程簡圖, 圖中 Vue 程式碼的功能是: 每隔一秒在 id
為 Box
的 div
中輸出當前時間
在開始梳理 Vue3 實現響應式的步驟之前, 要先簡單理解 effect
, effect
是響應式系統的核心, 而響應式系統又是 Vue3 的核心
上圖中從 track
到 targetMap
的黃色箭頭, 和從 targetMap
到 trigger
的白色箭頭, 就是 effect
函式要處理的環節
effect
函式的語法為:
effect(fn, options)
effect
接收兩個引數, 第一個必填引數 fn
是副作用函式
第二個選填 options
的引數定義如下:
export interface ReactiveEffectOptions {
lazy?: boolean // 是否延遲觸發 effect
scheduler?: (job: ReactiveEffect) => void // 排程函式
onTrack?: (event: DebuggerEvent) => void // 追蹤時觸發
onTrigger?: (event: DebuggerEvent) => void // 觸發回撥時觸發
onStop?: () => void // 停止監聽時觸發
allowRecurse?: boolean // 是否允許遞迴
}
下邊從流程圖中左上角的 Vue 程式碼開始
第 1 步
通過 reactive
方法將 target
物件轉為響應式物件, reactive
方法的實現方法如下:
import { mutableHandlers } from './baseHandlers'
import { mutableCollectionHandlers } from './collectionHandlers'
const reactiveMap = new WeakMap<Target, any>()
const readonlyMap = new WeakMap<Target, any>()
export function reactive(target: object) {
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers
)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
const targetType = getTargetType(target) // 先忽略, 上邊例子中, targetType 的值為: 1
const proxy = new Proxy(
target,
targetType === 2 ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
reactive
方法攜帶 target
物件和 mutableHandlers
、mutableCollectionHandlers
呼叫 createReactiveObject
方法, 這兩個 handers 先忽略
createReactiveObject
方法通過 reactiveMap
變數快取了一份響應式物件, reactiveMap
和 readonlyMap
變數是檔案內部的變數, 相當於檔案級別的閉包變數
其中 targetType 有三種列舉值: 0 代表不合法, 1 代表普通物件, 2 代表集合, 圖中例子中, targetType
的值為 1, 對於 { text: '' }
這個普通物件傳進 reactive()
方法時, 使用 baseHandlers
提供的 mutableHandlers
最後呼叫 Proxy 方法將 target 轉為響應式物件, 其中 "響應" 體現在 handers 裡, 可以這樣理解: reactive = Proxy (target, handlers)
第 2 步
mutableHandlers
負責掛載 get
、set
、deleteProperty
、has
、ownKeys
這五個方法到響應式物件上
其中 get
、has
、ownKeys
負責收集依賴, set
和 deleteProperty
負責消費依賴
響應式物件的 get
、has
和 ownKeys
方法被觸發時, 會呼叫 createGetter
方法, createGetter
的實現如下:
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
const res = Reflect.get(target, key, receiver)
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
當 { text: '' }
這個普通JS物件傳到 createGetter
時, key 的值為: text
, res 的值為: String
型別, 如果 res 的值為 Object
型別則會遞迴呼叫, 將 res 轉為響應式物件
createGetter
方法的目的是觸發 track
方法, 對應本文的第 3 步
響應式物件的 set
和 deleteProperty
方法被觸發時, 會呼叫 createSetter
方法, createSetter
的實現如下:
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
const oldValue = (target as any)[key]
const result = Reflect.set(target, key, value, receiver)
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
return result
}
}
createSetter
方法的目的是觸發 trigger
方法, 對應本文的第 4 步
第 3 步
這一步是整個響應式系統最關鍵的一步, 即我們常說的依賴收集, 依賴收集的概念很簡單, 就是把 響應式資料 和 副作用函式 建立聯絡
文章一開始流程圖的例子中, 就是把 target
物件和 document.getElementById("Box").innerText = date.text;
這個副作用函式建立關聯, 這個 "關聯" 指的就是上邊提到的 targetMap
變數, 後邊會詳細描述一下 targetMap
物件的結構
第 2 步介紹了 createGetter
方法的核心是呼叫 track
方法, track
方法由 @/vue/reativity/src/effect.ts
提供, 下面看一下 track
的實現:
const targetMap = new WeakMap<any, KeyToDepMap>()
// target: { text: '' }
// type: get
// key: text
export function track(target: object, type: TrackOpTypes, key: unknown) {
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)
}
}
從 track
方法我們能看到 targetMap
這個閉包變數上儲存了所有的 effect
, 換句話說是把能影響到 target
的副作用函式收集到 targetMap
變數中
targetMap 是個 WeakMap, WeakMap 和 Map 的區別在於 WeakMap 的鍵只能是物件, 用 WeakMap 而不用 Map 是因為 Proxy 物件不能代理普通資料型別
targetMap 的結構:
const targetMap = {
[target]: {
[key1]: [effect1, effect2, effect3, ...],
[key2]: [effect1, effect2, effect3, ...]
}
}
{ text: '' }
這個target 傳進來時, targetMap 的結構是:
// 上邊例子中用來在 id 為 Box 的 div 中輸出當前時間的副作用函式
const effect = () => {
document.getElementById("Box").innerText = date.text;
};
const target = {
"{ text: '' }": {
"text": [effect]
}
}
舉三個例子, 來分析一下 targetMap 的結構, 第一個例子是多個 target 情況:
<script>
import { effect, reactive } from "@vue/reactivity";
const target1 = { language: "JavaScript"};
const target2 = { language: "Go"};
const target3 = { language: "Python"};
const r1 = reactive(target1);
const r2 = reactive(target2);
const r3 = reactive(target3);
// effect1
effect(() => {
console.log(r1.language);
});
// effect2
effect(() => {
console.log(r2.language);
});
// effect3
effect(() => {
console.log(r3.language);
});
// effect4
effect(() => {
console.log(r1.language);
console.log(r2.language);
console.log(r3.language);
});
</script>
這種情況下 targetMap 的構成是:
const effect1 = () => {
console.log(r1.language);
};
const effect2 = () => {
console.log(r2.language);
};
const effect3 = () => {
console.log(r3.language);
};
const effect4 = () => {
console.log(r1.language);
console.log(r2.language);
console.log(r3.language);
};
const targetMap = {
'{"language":"JavaScript"}': {
"language": [effect1, effect4]
},
'{"language":"Go"}': {
"language": [effect2, effect4]
},
'{"language":"Python"}': {
"language": [effect3, effect4]
}
}
第二個例子是單個 target 多個屬性時:
import { effect, reactive } from "@vue/reactivity";
const target = { name: "rmlzy", age: "27", email: "rmlzy@outlook.com"};
const user = reactive(target);
effect(() => {
console.log(user.name);
console.log(user.age);
console.log(user.email);
});
這種情況下 targetMap 的構成是:
const effect = () => {
console.log(user.name);
console.log(user.age);
console.log(user.email);
};
const targetMap = {
'{"name":"rmlzy","age":"27","email":"rmlzy@outlook.com"}': {
"name": [effect],
"age": [effect],
"email": [effect]
}
}
第三個例子是多維物件時:
import { effect, reactive } from "@vue/reactivity";
const target = {
name: "rmlzy",
skills: {
frontend: ["JS", "TS"],
backend: ["Node", "Python", "Go"]
}
};
const user = reactive(target);
// effect1
effect(() => {
console.log(user.name);
});
// effect2
effect(() => {
console.log(user.skills);
});
// effect3
effect(() => {
console.log(user.skills.frontend);
});
// effect4
effect(() => {
console.log(user.skills.frontend[0]);
});
這種情況下 targetMap 的構成是:
const effect1 = () => {
console.log(user.name);
};
const effect2 = () => {
console.log(user.skills);
};
const effect3 = () => {
console.log(user.skills.frontend);
};
const effect4 = () => {
console.log(user.skills.frontend[0]);
};
const targetMap = {
'{"name":"rmlzy","skills":{"frontend":["JS","TS"],"backend":["Node","Python","Go"]}}': {
"name": [effect1],
"skills": [effect2, effect3, effect4]
},
'{"frontend":["JS","TS"],"backend":["Node","Python","Go"]}': {
"frontend": [effect3, effect4]
}
}
第 4 步
第 3 步的目的是收集依賴, 這一步的目的是消費依賴
這裡要注意, 只有當 target 代理物件的 get
方法被觸發時, 才會真正執行 track
, 換句話說, 沒有地方需要 get
target 物件時, target 沒有依賴, 也就沒有收集依賴一說
下邊的例子中只是把 target 轉換為了響應式物件, 並沒有觸發依賴收集, targetMap 是空的
const target = {"text": ""};
const date = reactive(target);
effect(() => {
date.text = new Date().toString();
});
第 2 步介紹了 createSetter
方法的核心是呼叫 trigger
方法, trigger
方法由 @/vue/reativity/src/effect.ts
提供, 下面看一下 trigger
的實現:
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
const effects = new Set<ReactiveEffect>()
if (isMap(target)) {
effects.add(depsMap.get(ITERATE_KEY))
}
const run = (effect: ReactiveEffect) => {
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
effects.forEach(run)
}
trigger 的實現很簡單, 先把 target 相關的 effect 彙總到 effects 陣列中, 然後呼叫 effects.forEach(run)
執行所有的副作用函式
再回顧一下 effect 方法的定義: effect(fn, options)
, 其中 options 有個可選屬性叫 scheduler
, 從上邊 run
函式也可以看到 scheduler
的作用是讓使用者自定義如何執行副作用函式
第 5 步
又回到了本文最開始講的 effect, effect 函式的實現如下:
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}
effect 的核心是呼叫 createReactiveEffect
方法
可以看到 options.lazy
預設為 false
會直接執行 effect, 當設定為 true
時, 會返回 effect 由使用者手動觸發
createReactiveEffect
函式的實現如下:
const effectStack: ReactiveEffect[] = []
let activeEffect: ReactiveEffect | undefined
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn()
}
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++
effect.allowRecurse = !!options.allowRecurse
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
首先定義了 effect 是個普通的 function
, 先看後邊 effect 函式掛載的屬性:
effect.id = uid++ // 自增ID, 每個 effect 唯一的ID
effect.allowRecurse = !!options.allowRecurse // 是否允許遞迴
effect._isEffect = true // 特殊標記
effect.active = true // 啟用狀態
effect.deps = [] // 依賴陣列
effect.raw = fn // 快取一份使用者傳入的副作用函式
effect.options = options // 快取一份使用者傳入的配置
isEffect
函式用來判斷值是否是 effect, 就是根據上邊 _isEffect
變數判斷的, isEffect
函式實現如下:
function isEffect(fn) {
return fn && fn._isEffect === true;
}
再來看 effect 的核心邏輯:
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
effectStack
用陣列實現棧,activeEffect
是當前生效的 effect
先執行 cleanup(effect)
:
function cleanup(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
cleanup
的目的是清空 effect.deps
, deps
是持有該 effect 的依賴陣列, deps
的結構如下
清除完依賴後, 開始重新收集依賴, 把當前 effect 追加到 effectStack, 將 activeEffect 設定為當前的 effect, 然後呼叫 fn 並且返回 fn() 的結果
第 4 步提過到: "只有當 target 代理物件的 get
方法被觸發時, 才會真正執行 track
", 至此才是真正的觸發了 target
代理物件的 get
方法, 執行了track
方法然後收集到了依賴
等到 fn
執行結束, finally 階段, 把當前的 effect 彈出, 恢復 effectStack 和 activeEffect, Vue3 整個響應式的流程到此結束
三、知識點
activeEffect 的作用
我的理解是為了暴露給 onTrack
方法, 來整體看一下 activeEffect 出現的地方:
let activeEffect;
function effect(fn, options = EMPTY_OBJ) {
const effect = createReactiveEffect(fn, options);
return effect;
}
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect() {
// 省略部分程式碼 ...
try {
activeEffect = effect;
return fn();
}
finally {
activeEffect = effectStack[effectStack.length - 1];
}
};
// 省略部分程式碼 ...
return effect;
}
function track(target, type, key) {
if (activeEffect === undefined) {
return;
}
let dep = targetMap.get(target).get(key); // dep 是儲存 effect 的 Set 陣列
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
if (activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
});
}
}
}
-
在
fn
執行前,activeEffect
被賦值為當前effect
-
在
fn
執行時的依賴收集階段, 獲取 targetMap 中的 dep (儲存 effect 的 Set 陣列), 並暴露給options.onTrack
介面
effect 和 stop
@vue/reactivity
提供了 stop 函式, effect
可以被 stop 函式終止
const obj = reactive({ foo: 0 });
const runner = effect(() => {
console.log(obj.foo);
});
// effect 被執行一次, 輸出 0
// obj.foo 被賦值一次, effect 被執行一次, 輸出 1
obj.foo ++;
// 停止 effect
stop(runner);
// effect 不會被觸發, 無輸出
obj.foo ++;
watchEffect 和 effect
watchEffect
來自@vue/runtime-core
,effect
來自@vue/reactivity
watchEffect
基於effect
實現watchEffect
會維護與元件例項的關係, 如果元件被解除安裝,watchEffect
會被stop
, 而effect
不會被stop
watchEffect 和 invalidate
watchEffect
接收的副作用函式, 會攜帶一個 onInvalidate
的回撥函式作為引數, 這個回撥函式會在副作用無效時執行
watchEffect(async (onInvalidate) => {
let valid = true;
onInvalidate(() => {
valid = false;
});
const data = await fetch(obj.foo);
if (valid) {
// 獲取到 data
} else {
// 丟棄
}
});
ref
JS資料型別:
- 基本型別: String、Number、Boolean、Null、Undefined、Symbol
- 引用資料型別: Object、Array、Function
因為 Proxy 只能代理物件, reactive
函式的核心又是 Proxy, 所以 reactive 不能代理基本型別
對於基本型別需要用 ref 函式將基本型別轉為物件:
class RefImpl<T> {
private _value: T
public readonly __v_isRef = true
constructor(private _rawValue: T, public readonly _shallow = false) {
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
}
其中 __v_isRef
引數用來標誌當前值是 ref 型別, isRef
的實現如下:
export function isRef(r: any): r is Ref {
return Boolean(r && r.__v_isRef === true)
}
這樣做有個缺點, 需要多取一層 .value
:
const myRef = ref(0);
effect(() => {
console.log(myRef.value);
});
myRef.value = 1;
這也是 Vue ref 語法糖提案的原因, 可以參考 如何評價 Vue 的 ref 語法糖提案?
reactive 和 shallowReactive
shallowReactive
用來定義淺響應資料, 深層次的物件值是非響應式的:
const target = {
foo: {
bar: 1
}
};
const obj = shallowReactive(target);
effect(() => {
console.log(obj.foo.bar);
});
obj.foo.bar = 2; // 無效, reactive 則有效
obj.foo = { bar: 2 }; // 有效
readonly 和 shallowReadonly
類似 shallowReactive
, 深層次的物件值是可以被修改的
markRaw 和 toRaw
markRaw 的作用是讓資料不可被代理, 所有攜帶 __v_skip
屬性, 並且值為 true
的資料都會被跳過:
export function markRaw<T extends object>(value: T): T {
def(value, ReactiveFlags.SKIP, true)
return value
}
toRaw 的作用是獲取代理物件的原始物件:
const obj = {};
const reactiveProxy = reactive(obj);
console.log(toRaw(reactiveProxy) === obj); // true
computed
const myRef = ref(0);
const myRefComputed = computed(() => {
return myRef.value * 2;
});
effect(() => {
console.log(myRef.value * 2);
});
當 myRef
值變化時, computed 會執行一次, effect 會執行一次
當 myRef
值未變化時, computed 不會執行, effect 依舊會執行
如果你有問題歡迎留言和我交流, 閱讀原文