Red Huang

Red Huang

Vue 3.0 リアクティブ、コンピューテッド ソースコード分析

===

Vue 3.0 reactive, computed 原始碼分析 #


このファイルでは多くのコードが使用されているため、全体のコードをトレースするために検索 (Ctrl+F) を活用してください。

Vue 3.0 では、computed の書き方も変更されており、書き方は Composition APIを参照できます。提供されている例は以下の通りです。

 const state = reactive({
  count: 0,
  double: computed(() => state.count * 2)
}) 

テストケースを使って computed が何をできるか見てみましょう。#

computed には関数を渡すことができ、その関数内の値が変更されたり、値が与えられたりすると、computed の返り値の value が更新されます。

以下の例では、computed(() => value.foo)の中でvalue.fooが使用されているため、value.foo = 1のとき、cValueの中のvalue1に更新されることがわかります。

 it('should return updated value', () => {
    const value = reactive<{ foo?: number }>({})
    const cValue = computed(() => value.foo)
    expect(cValue.value).toBe(undefined)
    value.foo = 1
    expect(cValue.value).toBe(1)
}) 

Computed#

computed の機能を理解した後、computed がどのように更新されるのか、その原始コードを追ってみましょう。

まず、computed という関数の実装を直接見てみましょう。元のテストケースに基づいて、渡されたメソッドがgetterOrOptions: (() => T) | WritableComputedOptions<T>という型に一致していることがわかります。したがって、setter functionはありません。次に、23 行目でeffectが宣言されており、この時点でlazy: trueです。

 export function computed<T>(getter: () => T): ComputedRef<T>
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: (() => T) | WritableComputedOptions<T>
): any {
  const isReadonly = isFunction(getterOrOptions)
  const getter = isReadonly
    ? (getterOrOptions as (() => T))
    : (getterOrOptions as WritableComputedOptions<T>).get
  const setter = isReadonly
    ? __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
    : (getterOrOptions as WritableComputedOptions<T>).set

  let dirty = true
  let value: T

  const runner = effect(getter, {
    lazy: true,
    // mark effect as computed so that it gets priority during trigger
    computed: true,
    scheduler: () => {
      dirty = true
    }
  }) 

前の章でeffectの実際の効果について少し触れました。コードを復習してみましょう。
options.lazytrueであれば、起動のタイミングは外部の人によって制御されます。

 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
} 

computed に戻ると、expect(cValue.value).toBe(undefined)が実行されると、getter function get value()が実行されます。この時、dirtytrueであれば、runner が起動し、effect が依存関係の収集を開始します。

依存関係の収集方法は前の章で説明しました。理論的には、push activeReactiveEffectStackrun getterpop activeReactiveEffectStackです。

この時点で、expect(cValue.value).toBe(undefined)が実行されると、valueという reactiveObject が収集され、依存関係の収集が完了します。

 const runner = effect(getter, {
    lazy: true,
    // mark effect as computed so that it gets priority during trigger
    computed: true,
    scheduler: () => {
      dirty = true
    }
  })
  return {
    [refSymbol]: true,
    // expose effect so computed can be stopped
    effect: runner,
    get value() {
      if (dirty) {
        value = runner()
        dirty = false
      }
      // When computed effects are accessed in a parent effect, the parent
      // should track all the dependencies the computed property has tracked.
      // This should also apply for chained computed properties.
      trackChildRun(runner)
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  }
} 

したがって、value.foo = 1というステートメントが実行されると、依存関係がトリガーされ、以前に説明したtrigger functionが起動します。

addRunners という関数を見てみると、effect が computed の場合、この effect が computedRunners Set に追加されます。これは、後で computedRunners が通常の effects よりも先に実行されるためです。なぜこれを行うのでしょうか?

一般的な effect が computedObject の情報を使用する必要がある場合、まず computedObject の dirty を true にする必要があります。つまり、scheduler functionを優先的に実行し、effect が後で実行されるときに computedObject の情報が最新であることを保証します。

 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 = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  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)
}

function addRunners(
  effects: Set<ReactiveEffect>,
  computedRunners: Set<ReactiveEffect>,
  effectsToAdd: Set<ReactiveEffect> | undefined
) {
  if (effectsToAdd !== void 0) {
    effectsToAdd.forEach(effect => {
      if (effect.computed) {
        computedRunners.add(effect)
      } else {
        effects.add(effect)
      }
    })
  }
} 

話がそれましたが、ここで追跡が完了した結果、value.foo = 1が実行されると、cValue の dirty が true になります。次に、expect(cValue.value).toBe(1)が実行されると、cValue getter functionがトリガーされ、最新の値が更新されて返されます。

このテストケースはここで終了します。

Computed Chained#

しかし、computed chained の場合はどうでしょうか?

以下のコードは、c2 -> c1 -> valueの依存関係を示しており、computed の依存関係の構築は、常にgetter functionから収集されることを覚えておいてください。この例では、c2.valueがそれに該当します。この時、computedObject の runner が実行されます。ここでは少し複雑なので、簡単な言葉で表現します。

  1. push c2, activeReactiveEffectStack => [ c2 ]
  2. c2 run fn // which is c1.value + 1
  3. push c1, activeReactiveEffectStack => [ c2, c1 ]
  4. c1 run fn // which is value.foo
  5. value 依賴於 activeReactiveEffectStack top // 也就是 c1
  6. pop c1, activeReactiveEffectStack => [ c2 ]
  7. c1 run trackChildRun,將 activeReactiveEffectStack top 也就是 c2 依賴於 c1 的所有依賴
  8. pop c2, activeReactiveEffectStack => []

このようにして、親ノードは子ノードのすべての依存点に依存することになります。これは再帰的に有効であり、親ノードはすべての子孫ノードに依存することができ、依存関係はフラットになります。
この利点は、階層構造が多くても、依存関係のトリガーによる更新性能に影響を与えないことです。
この例では、value.foo++だけでc1c2の dirty が true に更新されるため、expect(c2.value).toBe(2)expect(c1.value).toBe(1)の順序に関係なく、両方の値が更新されます。

 it('should work when chained', () => {
    const value = reactive({ foo: 0 })
    const c1 = computed(() => value.foo)
    const c2 = computed(() => c1.value + 1)
    expect(c2.value).toBe(1)
    expect(c1.value).toBe(0)
    value.foo++
    expect(c2.value).toBe(2)
    expect(c1.value).toBe(1)
  }) 

function trackChildRun(childRunner: ReactiveEffect) {
const parentRunner =
activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
if (parentRunner) {
for (let i = 0; i < childRunner.deps.length; i++) {
const dep = childRunner.deps[i]
if (!dep.has(parentRunner)) {
dep.add(parentRunner)
parentRunner.deps.push(dep)
}
}
}
}


### [](https://hackmd.io/ZfQArpjoQ8GrGqCABYpPpQ?view#Triggered-effect-when-chained "Triggered-effect-when-chained")チェーン時のトリガー効果

次に、一般的なeffectがcomputedに依存する場合に何が起こるかについて説明します。

この例では、依存関係は`effect -> c2 -> c1 -> value`です。effectはlazyではないため、直接実行されます。以下は以前のテキストの順序で表現します。

1.  `push effect, activeReactiveEffectStack => [ effect ]`
2.  `effect run fn // which is dummy = c2.value`
3.  `push c2, activeReactiveEffectStack => [ effect, c2 ]`
4.  `c2 run fn // which is c1.value + 1`
5.  `push c1, activeReactiveEffectStack => [ effect, c2, c1 ]`
6.  `c1 run fn // which is value.foo`
7.  `value 依賴於 activeReactiveEffectStack top // 也就是 c1`
8.  `pop c1, activeReactiveEffectStack => [ effect, c2 ]`
9.  `c1 run trackChildRun,將 activeReactiveEffectStack top 也就是 c2 依賴於 c1 的所有依賴`
10.  `pop c2, activeReactiveEffectStack => [ effect ]`
11.  `c2 run trackChildRun,將 activeReactiveEffectStack top 也就是 effect 依賴於 c2 的所有依賴 (而又此時 c2 擁有 c1 的所有依賴,所以也就是 c1, c2 兩者的所有依賴`
12.  `pop effect, activeReactiveEffectStack => []`

この時点で、dummyは12行目で1になります。`getter1`と`getter2`も一度実行され、次にvalue.foo++が実行され、3つの依存者`effect`、`c2`、`c1`がトリガーされます。そしてもちろん、`computedRunners`と`effects`は集合なので、必ず一度だけ実行され、それぞれの関数がトリガーされます。このテストケースは完了しました。

it('should trigger effect when chained', () => {
const value = reactive({ foo: 0 })
const getter1 = jest.fn(() => value.foo)
const getter2 = jest.fn(() => {
return c1.value + 1
})
const c1 = computed(getter1)
const c2 = computed(getter2)

let dummy
effect(() => {
  dummy = c2.value
})
expect(dummy).toBe(1)
expect(getter1).toHaveBeenCalledTimes(1)
expect(getter2).toHaveBeenCalledTimes(1)
value.foo++
expect(dummy).toBe(2)
// should not result in duplicate calls
expect(getter1).toHaveBeenCalledTimes(2)
expect(getter2).toHaveBeenCalledTimes(2)

})


これで、computedのすべての状況がどれほど複雑で困難であるかを説明できますが、困難はまだ続きます。このシリーズの記事では、徐々にコアや仮想DOMの方に進んでいきます。
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。