Vue 3.0 reactive, effect 到底做了什麼事#
相信 vue-next (vue 3.0) 出來之後大家對 source code 都非常好奇,且 vue 3.0 也改為 Composition API,當然大家最好奇的應該還是 Vue 3.0 到底對於 Proxy API 用到了怎樣的境界,所以本篇主軸會在於 vue 3.0 reative 到底做了什麼事情
凡事先看 test case#
由於 vue 3.0 更換成了 Composition API,強烈建議先看完 Composition API 再來往下探討
以下圖 unit testing 的程式碼來說,可以看到 他將 { num: 0 }
設為 reactive,並且只要設定 num 的值,第四行的 () => (dummy = counter.num)
就會被 call。
it('should observe basic properties', () => {
let dummy
const counter = reactive({ num: 0 })
effect(() => (dummy = counter.num))
expect(dummy).toBe(0)
counter.num = 7
expect(dummy).toBe(7)
})
reactive 實作#
可以得知 reactive 這個 function 會回傳一個 reactive object,
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (readonlyToRaw.has(target)) {
return target
}
// target is explicitly marked as readonly by user
if (readonlyValues.has(target)) {
return readonly(target)
}
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}
createReactiveObject 當然先判斷 target 是否已經有 observe 或者本身就是 proxy。接下來就是針對 target.constructor 如果是 Set, Map, WeakMap, WeakSet 就使用 collectionHandlers ,反之為 baseHandlers ,以這個 test cast 的例子會使用 mutableHandlers。
function createReactiveObject(
target: any,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target already has corresponding Proxy
let observed = toProxy.get(target)
if (observed !== void 0) {
return observed
}
// target is already a Proxy
if (toRaw.has(target)) {
return target
}
// only a whitelist of value types can be observed.
if (!canObserve(target)) {
return target
}
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
observed = new Proxy(target, handlers)
toProxy.set(target, observed)
toRaw.set(observed, target)
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}
接著我們往下看 mutableHandlers ,依照先前給的值,mutableHandlers 勢必是要一個 proxy handlers,而依照 vue2.x 的經驗,都是 getter 在專門收集依賴,所以繼續往下看。
export const mutableHandlers: ProxyHandler<any> = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
這邊可以看到正常情況下,會跑到 track 這個 function 上,且跟以前一樣,如果偵測到 res 是 Object 的話,就會遞迴的往下對他進行 reactive 操作
function createGetter(isReadonly: boolean) {
return function get(target: any, key: string | symbol, receiver: any) {
const res = Reflect.get(target, key, receiver)
if (typeof key === 'symbol' && builtInSymbols.has(key)) {
return res
}
if (isRef(res)) {
return res.value
}
track(target, OperationTypes.GET, key)
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}
我們可以看到 track function 裡面有一個操作類似於 Vue 2.x 的 Dep.target stack, 即為 activeReactiveEffectStack,在這裡會取出最後一個 effect 進行 targetMap 的設置。而 targetMap 是一個 WeakMap
,總之就是存該 target 中的所有 deps,接著就將 effect 推入一個 deps 上。
- targetMap = target 對應 depsMap
- depsMap = key 對應 deps
export const targetMap: WeakMap<any, KeyToDepMap> = new WeakMap()
export function track(
target: any,
type: OperationTypes,
key?: string | symbol
) {
if (!shouldTrack) {
return
}
const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
if (effect) {
if (type === OperationTypes.ITERATE) {
key = ITERATE_KEY
}
let depsMap = targetMap.get(target)
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key!)
if (dep === void 0) {
depsMap.set(key!, (dep = new Set()))
}
if (!dep.has(effect)) {
dep.add(effect)
effect.deps.push(dep)
if (DEV && effect.onTrack) {
effect.onTrack({
effect,
target,
type,
key
})
}
}
}
}
但這時候可能大家會納悶,那 activeReactiveEffectStack 這個東西是從哪來的呢?
還記得我們 `effect(() => (dummy = counter.num))` 這一段 code 吧。
讓我們看一下 effect 的實作細節。可以清楚的看到在沒有 lazy 的情況底下會直接 call createReactiveEffect(fn, options) 的 return function。
也就是這一段 `run(effect as ReactiveEffect, fn, args)`
export function effect(
fn: Function,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect {
if ((fn as ReactiveEffect).isEffect) {
fn = (fn as ReactiveEffect).raw
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}
function createReactiveEffect(
fn: Function,
options: ReactiveEffectOptions
): ReactiveEffect {
const effect = function effect(...args): any {
return run(effect as ReactiveEffect, fn, args)
} as ReactiveEffect
effect.isEffect = true
effect.active = true
effect.raw = fn
effect.scheduler = options.scheduler
effect.onTrack = options.onTrack
effect.onTrigger = options.onTrigger
effect.onStop = options.onStop
effect.computed = options.computed
effect.deps = []
return effect
}
```
而 run 當中就會看到關鍵的 stack push `activeReactiveEffectStack.push(effect)`,與 `return fn(...args)` 然後當然 finally 中的 stack pop `activeReactiveEffectStack.pop()`
這裡要了解一件事情,就是 finally block 總是會執行,就算是在 return 後面
```
function run(effect: ReactiveEffect, fn: Function, args: any[]): any {
if (!effect.active) {
return fn(...args)
}
if (activeReactiveEffectStack.indexOf(effect) === -1) {
cleanup(effect)
try {
activeReactiveEffectStack.push(effect)
return fn(...args)
} finally {
activeReactiveEffectStack.pop()
}
}
}
```
好,該了解的都了解完了,讓我們回頭看看這個 test case,reactive 會怎麼完成他要做的工作
```
it('should observe basic properties', () => {
let dummy
const counter = reactive({ num: 0 })
effect(() => (dummy = counter.num))
expect(dummy).toBe(0)
counter.num = 7
expect(dummy).toBe(7)
})
```
首先可以看到,第 3 行將`{ num: 0 }`轉為 reactive。
依照前面的例子所見 counter 將會是 createReactiveObject 的回傳值,也就是
```
new Proxy({ num: 0 }, mutableHandlers)
```
這時候到第四行,我們可以知道 effect 裡面傳了一個 function 為 `() => (dummy = counter.num)`,且不為 lazy。
所以 effect 會直接 call,也就是會執行下面這行
```
const effect = function effect(...args): any {
return run(effect as ReactiveEffect, fn, args)
} as ReactiveEffect
```
當 run 跑進去後,我們知道他總共會做三件事情
1. push activeReactiveEffectStack
2. run fn
3. pop activeReactiveEffectStack
所以跑到第二步時,stack 上已經有一個 effect 的資料,且 fn 為 `dummy = counter.num`,這時候就會 call 到 counter 的 proxy getter function。
還記得會發生什麼事情嗎,就是會跑到 track function,
而如果你還記得的話 (我相信到這裏大家都忘光了),track 會將 stack 的 top effect 拿出來建立 target 某個 key 與該 effect 的對應關係。
好,到這裡已經快結束了,但少了一個東西,我們前面都沒有提到過,就是 set 會發生什麼事情
```
counter.num = 7
```
首先讓我們看一下 set handler 的 code
```
function set(
target: any,
key: string | symbol,
value: any,
receiver: any
): boolean {
value = toRaw(value)
const hadKey = hasOwn(target, key)
const oldValue = target[key]
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
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)) {
/* istanbul ignore else */
if (__DEV__) {
const extraInfo = { oldValue, newValue: value }
if (!hadKey) {
trigger(target, OperationTypes.ADD, key, extraInfo)
} else if (value !== oldValue) {
trigger(target, OperationTypes.SET, key, extraInfo)
}
} else {
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (value !== oldValue) {
trigger(target, OperationTypes.SET, key)
}
}
}
return result
}
```
一般來說當 proxy 代理一個陣列時,做基本的 push 時可能會觸發很多次的 get 與 set ( set value , set length ),如果沒有多做一些處理,很有可能會執行很多次的 trigger。
這裡作者有一些小智慧在裏頭,假如現在 push 了`'world'`進入了 `['hello']`,這時候會先做`set [1] = "world"`,此時 hadKey 為 false 這就會一路走到 `trigger(target, OperationTypes.ADD, key, extraInfo)`,接著 `set length = 2`,此時 `hadKey` 是 true 的,且 `value`是與`oldValue`相等的,所以可以完美地躲掉執行很多次 trigger 的問題
回到主題上,那 trigger 又會做什麼事情呢?
```
export function trigger(
target: any,
type: OperationTypes,
key?: string | symbol,
extraInfo?: any
) {
const depsMap = targetMap.get(target)
if (depsMap === void 0) {
// never been tracked
return
}
const effects: Set<ReactiveEffect> = new Set()
const computedRunners: Set<ReactiveEffect> = new Set()
if (type === OperationTypes.CLEAR) {
// collection being cleared, trigger all effects for target
depsMap.forEach(dep => {
addRunners(effects, computedRunners, dep)
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
addRunners(effects, computedRunners, depsMap.get(key))
}
// also run for iteration key on ADD | DELETE
if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
addRunners(effects, computedRunners, depsMap.get(iterationKey))
}
}
const run = (effect: ReactiveEffect) => {
scheduleRun(effect, target, type, key, extraInfo)
}
// Important: computed effects must be run first so that computed getters
// can be invalidated before any normal effects that depend on them are run.
computedRunners.forEach(run)
effects.forEach(run)
}
```
當然聰明的你應該都知道該怎麼做了,就是把 targetMap 對照表拿出來,然後對他所有的 effect 做一次 run 就好了。
當然這也只是 reactive 的皮毛而已,下一篇我會討論 computedRunners 的實作方法與 readonly 特性。