Vue2 響應式原理
學過 Vue2 的話應該知道響應式原理是由 Object.defineProperty 對數據進行劫持,再加上訂閱發佈,實現數據的響應的。
Object.defineProperty 存在以下幾個方面的缺點。
初始化的時候需要遍歷對象的所有屬性進行劫持,如果對象存在嵌套還需要進行遞歸。導致初始化的時候需要消耗一些資源用於遞歸遍歷。
從上面可以推導出 Vue2 對於新增、刪減對象屬性是無法進行劫持,需要通過 Vue.set、Vue.delete 進行操作。
每個調用者會生成一個 Watcher,造成內存佔用。
無法劫持 Set、Map 對象。
Vue3 響應式原理
針對以上問題,Vue3 改用了 ES6 原生的 Proxy 對數據進行代理。
Proxy 基本用法如下:
const reactive = (target) => {
return new Proxy(target, {
get(target, key) {
console.log("get: ", key);
// return Reflect.get(target, key);
return target[key];
},
set(target, key, value) {
console.log("set: ", key, " = ", value);
// Reflect.set(target, key, value);
target[key] = value;
return value;
},
});
};
var a = reactive({ count: 1 });
console.log(a.count);
a.count = 2;
console.log(a.count);
// log 輸出
// get: count
// 1
// set: count = 2
// get: count
// 2
如此便可檢測到數據的變化。接下來只需在 get 進行收集依賴,set 通知依賴更新。
接下來還需藉助 effect、track 和 trigger 方法。
effect 函數傳入一個回調函數,回調函數會立即執行,並自動與響應式數據建立依賴關係。
track 在 proxy get 中執行,建立依賴關係。
trigger 響應式數據發生變化時,根據依賴關係找到對應函數進行執行。
代碼實現如下:
const reactive = (target) => {
return new Proxy(target, {
get(target, key) {
console.log("[proxy get] ", key);
track(target, key);
// return Reflect.get(target, key);
return target[key];
},
set(target, key, value) {
console.log("[proxy set] ", key, " = ", value);
// Reflect.set(target, key, value);
target[key] = value;
trigger(target, key);
return value;
},
});
};
// 用於存放 effect 傳入的 fn,便於 track 時找到對應 fn
const effectStack = [];
// 用於保存 響應式對象 和 fn 的關係
// {
// target: {
// key: [fn, fn];
// }
// }
const targetMap = {};
const track = (target, key) => {
let depsMap = targetMap[target];
if (!depsMap) {
targetMap[target] = depsMap = {};
}
let dep = depsMap[key];
if (!dep) {
depsMap[key] = dep = [];
}
// 建立依賴關係
const activeEffect = effectStack[effectStack.length - 1];
dep.push(activeEffect);
};
const trigger = (target, key) => {
const depsMap = targetMap[target];
if (!depsMap) return;
const deps = depsMap[key];
// 根據依賴關係,找出 fn 並重新執行
deps.map(fn => {
fn();
});
};
const effect = (fn) => {
try {
effectStack.push(fn);
fn();
} catch (error) {
effectStack.pop(fn);
}
};
var a = reactive({ count: 1 });
effect(() => {
console.log("[effect] ", a.count);
});
a.count = 2;
// log 輸出
// [proxy get] count
// [effect] 1
// [proxy set] count = 2
// [proxy get] count
// [effect] 2
以上代碼並不是 Vue3 的源碼,而是 Vue3 響應式的原理,相比起 Vue2 要更加簡單。
執行順序爲
- 調用 reactive 代理響應式對象;
- 調用 effect ,會將 fn 保存至 effectStack,在執行 fn 時會觸發 Proxy 的 get;
- 從 Proxy 的 get 觸發 track,將數據與 fn 建立關係;
- 修改響應式數據,觸發 Proxy 的 set;
- 從 Proxy 的 set 觸發 trigger,從而找出對應的 fn 並執行。
弄清楚原理再去看源碼會簡單很多,下面我們一起去看下源碼。
Vue3 響應式源碼
Vue3 的響應式是一個獨立的模塊,不依賴框架,甚至可以在 React、Angular 中使用。
reactive 函數位於 packages/reactivity/src/reactive.ts
// packages/reactivity/src/reactive.ts
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// ...
const proxy = new Proxy(
target,
// 對 Set、Map 的集合使用 collectionHandlers(mutableCollectionHandlers)
// 普通對象使用 baseHandlers(mutableHandlers)
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
// ...
return proxy
}
接下來看下 mutableHandlers
// packages/reactivity/src/baseHandlers.ts
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
看下 get 和 set
// packages/reactivity/src/baseHandlers.ts
const get = /*#__PURE__*/ 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 (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
if (!isReadonly) {
// 調用 track 建立依賴關係
track(target, TrackOpTypes.GET, key)
}
// ...
return res
}
}
// packages/reactivity/src/baseHandlers.ts
const set = /*#__PURE__*/ createSetter()
// ...
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]
// ...
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if (!hadKey) {
// 調用 trigger 通知依賴重新執行
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 調用 trigger 通知依賴重新執行
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
接下來再看下 track
// packages/reactivity/src/effect.ts
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!isTracking()) {
return
}
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// ...
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
}
}
上半部分與我們自己實現的邏輯很類似,先找出 dep 如果不存在則創建,只不過 Vue 使用的是 Map 和 Set(createDep 返回值爲 Set)。
然後是 trackEffects,關鍵代碼就是 dep 和 activeEffect 互相保存,我們的做法只是將 activeEffect 存入 dep 。
接下來看看 set 中調用的 trigger。
// packages/reactivity/src/effect.ts
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
// 沒有被 track 收集到,直接返回
return
}
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
// 清空依賴,需要觸發與 target 關聯的所有 effect
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
// 修改數組的 length 時對應的處理
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
// 修改、新增、刪除屬性時執行
if (key !== void 0) {
deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
// 往 deps 中添加迭代器屬性的 effect
switch (type) {
// ...
}
}
// 以上操作則是爲了取出 deps (targetMap[target][key])
// 下面的操作則是將 deps 中的 effect 取出並執行
// 開發時還會傳入 eventInfo
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])
}
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
}
// 執行 effect
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
}
trigger 函數看似很長,其實可以簡化成我們的例子進行理解,無非就是取出對應的 deps ,遍歷出 deps 中的 effect 並執行。
接下來就該看看 effect 函數的實現了。
// packages/reactivity/src/effect.ts
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
// 調用 ReactiveEffect 對進行封裝
const _effect = new ReactiveEffect(fn)
// ...
// 判斷是否有 options.lazy
// lazy 爲 true 不會立即執行
if (!options || !options.lazy) {
_effect.run()
}
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}
export class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
// can be attached after creation
computed?: boolean
allowRecurse?: boolean
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope | null
) {
recordEffectScope(this, scope)
}
run() {
if (!this.active) {
return this.fn()
}
if (!effectStack.includes(this)) {
try {
// 執行時將當前的 effect 存入 effectStack
// 並賦值給 activeEffect
// 在 track 時獲取
effectStack.push((activeEffect = this))
enableTracking()
// ...
return this.fn()
} finally {
// ...
resetTracking()
effectStack.pop()
const n = effectStack.length
// 從 effectStack 繼續取出上一個的 activeEffect 繼續執行
activeEffect = n > 0 ? effectStack[n - 1] : undefined
}
}
}
stop() {
// ...
}
}
我們在使用 effect 時,會將我們傳入的函數經過 ReactiveEffect 封裝,如果我們沒傳入 { lazy: true }
則會立即執行 run 函數。
run 函數就是先賦值 activeEffect 並存入 effectStack,然後執行我們傳入的回調函數。
執行回調函數的過程會觸發 Proxy 的 get,get 又會觸發 track 進行依賴收集。
執行完成後將 activeEffect 從 effectStack pop出去,並取出上一個 activeEffect 繼續執行。
爲什麼要用 effectStack ?
假如我們在 effect 中使用了 computed,Vue 需要先執行計算出 computed。
computed 內部也會調用 ReactiveEffect,所以需要將 computed 的 effect 存入 effectStack ,當 computed 計算完成之後,則從 effectStack pop 出去,繼續執行我們的 effect。
如此便完成依賴收集,當響應式數據發生變化時則會觸發 trigger,重新執行我們在 effect 中傳入的回調函數。
修改響應式數據爲什麼頁面會自動更新?還記得上篇文章<深入 Vue3 源碼,學習初始化流程>介紹的 setupRenderEffect
嗎?
這個方法也是利用了 ReactiveEffect,在 mount 的時候會觸發 setupRenderEffect
執行進而觸發 patch
。 patch
的過程中會使用響應式數據,從而建立依賴關係,當響應式數據發生變化時會重新執行 setupRenderEffect
,後面就進入 diff 了,下篇文章在詳細展開 diff。
結語
以上便是 Vue3 的響應式原理,只要瞭解了原理,能用自己的語言清晰的描述出來,面試肯定能增加成功率。
好了,這篇文章就水到這裏吧。如有錯誤的地方,希望還能在評論區指出,感謝!
下篇文章將解析 Vue3 的 diff 算法,如果有興趣的話別忘了關注我呀,我們一起學習、進步。