Red Huang

Red Huang

Vue 3.0 のリアクティブ、エフェクトは一体何をしたのか

Vue 3.0 の reactive と effect は一体何をしたのか#


vue-next (vue 3.0) が登場した後、皆さんはソースコードに非常に興味を持っていると思いますし、vue 3.0 は Composition API に変更されました。もちろん、皆さんが最も興味を持っているのは、Vue 3.0 が Proxy API をどのように活用しているのかということです。したがって、本記事の主な焦点は、vue 3.0 の reactive が一体何をしているのかということです。

まずは test case を見てみましょう#

vue 3.0 は Composition API に変更されたため、Composition API を先に確認することを強くお勧めします。
以下の図はユニットテストのコードで、{ num: 0 } を reactive として設定し、num の値を設定するだけで、4 行目の () => (dummy = counter.num) が呼び出されることがわかります。

 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 という関数は、reactive object を返すことがわかります。

 export function reactive(target: object) {
  // 読み取り専用のプロキシを観察しようとした場合、読み取り専用バージョンを返す。
  if (readonlyToRaw.has(target)) {
    return target
  }
  // ユーザーによって target が明示的に読み取り専用としてマークされている
  if (readonlyValues.has(target)) {
    return readonly(target)
  }
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers,
    mutableCollectionHandlers
  )
} 

createReactiveObject は、まず target がすでに observe されているか、またはプロキシであるかを判断します。次に、target.constructor が Set, Map, WeakMap, WeakSet の場合は collectionHandlers を使用し、それ以外の場合は baseHandlers を使用します。このテストケースの例では 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 はすでに対応する Proxy を持っています
  let observed = toProxy.get(target)
  if (observed !== void 0) {
    return observed
  }
  // target はすでに Proxy です
  if (toRaw.has(target)) {
    return target
  }
  // 観察できる値のタイプのホワイトリストのみが観察可能です。
  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 はプロキシハンドラである必要があります。vue2.x の経験に基づいて、getter が依存関係を収集するために特化しているため、さらに見ていきます。

 export const mutableHandlers: ProxyHandler<any> = {
  get: createGetter(false),
  set,
  deleteProperty,
  has,
  ownKeys
} 

ここでは、通常の状況では track という関数が呼び出され、以前と同様に、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
        ? // 読み取り専用と reactive に遅延アクセスする必要がある
          // 循環依存を避けるため
          readonly(res)
        : reactive(res)
      : res
  }
} 

track 関数の中には、Vue 2.x の Dep.target スタックに似た操作があり、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)) のコードを思い出してください。
effect の実装の詳細を見てみると、遅延がない場合は createReactiveEffect (fn, options) の返り値を直接呼び出すことがわかります。
つまり、この部分 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 の中では、重要なスタックプッシュ activeReactiveEffectStack.push(effect)return fn(...args) があり、もちろん finally の中でスタックポップ activeReactiveEffectStack.pop() があります。

ここで理解しておくべきことは、finally ブロックは常に実行されるということです。たとえ 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()
    }
  }
} 

さて、理解すべきことはすべて理解しました。テストケースを振り返って、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) 

この時点で 4 行目に進むと、effect に関数 () => (dummy = counter.num) が渡され、lazy ではないことがわかります。

したがって、effect は直接呼び出され、次の行が実行されます。

 const effect = function effect(...args): any {
    return run(effect as ReactiveEffect, fn, args)
  } as ReactiveEffect 

run が実行されると、3 つのことが行われます。

  1. activeReactiveEffectStack にプッシュ
  2. fn を実行
  3. activeReactiveEffectStack からポップ

したがって、2 番目のステップに進むと、スタックにはすでに 1 つの effect のデータがあり、fn は dummy = counter.num です。この時点で counter のプロキシ getter 関数が呼び出されます。
何が起こるか覚えていますか?track 関数が呼び出され、
もし覚えているなら(ここまで来ると皆さんは忘れてしまったと思いますが)、track はスタックのトップ effect を取り出して、target の特定の key とその effect の対応関係を確立します。

さて、ここまで来たらほぼ終わりですが、もう一つの重要なことがあります。前述の通り、set が何をするのかを見てみましょう。

 counter.num = 7 

まず、set ハンドラのコードを見てみましょう。

 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)
  // 元のプロトタイプチェーンの上にあるターゲットの場合はトリガーしない
  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
} 

一般的に、プロキシが配列を代理する場合、基本的な push を行うと、多くの get と set(set value, set length)がトリガーされる可能性があります。
何も追加の処理を行わなければ、トリガーが何度も実行される可能性があります。
ここで著者は少しの知恵を使っています。たとえば、['hello']'world' を push した場合、最初に set [1] = "world" を行い、この時点で hadKey は false になります。これにより、trigger(target, OperationTypes.ADD, key, extraInfo) に進み、次に set length = 2 を行うと、hadKey は true になり、value は oldValue と等しいため、多くのトリガーを回避することができます。

さて、話を戻すと、trigger は何をするのでしょうか?

 export function trigger(
  target: any,
  type: OperationTypes,
  key?: string | symbol,
  extraInfo?: any
) {
  const depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    // まだ追跡されていない
    return
  }
  const effects: Set<ReactiveEffect> = new Set()
  const computedRunners: Set<ReactiveEffect> = new Set()
  if (type === OperationTypes.CLEAR) {
    // コレクションがクリアされている場合、ターゲットのすべての effect をトリガー
    depsMap.forEach(dep => {
      addRunners(effects, computedRunners, dep)
    })
  } else {
    // SET | ADD | DELETE のために実行をスケジュール
    if (key !== void 0) {
      addRunners(effects, computedRunners, depsMap.get(key))
    }
    // 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)
  }
  // 重要: computed effects は最初に実行され、computed getters
  // は通常の effects が実行される前に無効化される必要があります。
  computedRunners.forEach(run)
  effects.forEach(run)
} 

もちろん、賢いあなたはどうすればよいか知っているでしょう。targetMap の対応表を取り出し、すべての effect に対して run を実行すればよいのです。

これは reactive の表面的な部分に過ぎません。次回は computedRunners の実装方法と readonly 特性について議論します。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。