Red Huang

Red Huang

Vue 3.0 reactive, effect 到底做了什麼事

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 特性。
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。