阿里云主机折上折
  • 微信号
Current Site:Index > The lazy evaluation of a reactive system

The lazy evaluation of a reactive system

Author:Chuan Chen 阅读数:35777人阅读 分类: Vue.js

Lazy Evaluation in the Reactive System

Vue3's reactive system uses Proxy to achieve automatic dependency tracking and update triggering. The lazy evaluation mechanism is a key design for performance optimization, ensuring computations are executed only when truly needed. This mechanism is primarily reflected in the implementation of computed properties (computed) and side effects (effect).

Lazy Nature of Computed Properties

Computed properties are lazy by default, meaning they are only calculated when actually accessed. Vue3 internally implements this feature through the ComputedRefImpl class:

class ComputedRefImpl<T> {
  private _value!: T
  private _dirty = true
  
  constructor(
    private getter: ComputedGetter<T>,
    private setter: ComputedSetter<T>
  ) {
    effect(() => {
      if (this._dirty) {
        this._value = this.getter()
        this._dirty = false
      }
    }, {
      lazy: true,
      scheduler: () => {
        if (!this._dirty) {
          this._dirty = true
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')
        }
      }
    })
  }
  
  get value() {
    if (this._dirty) {
      this._value = this.getter()
      this._dirty = false
    }
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }
}

The key lies in the _dirty flag, which controls whether recalculation is needed. When dependencies change, it only marks _dirty as true without immediately recalculating. For example:

const count = ref(0)
const double = computed(() => count.value * 2)

console.log(double.value) // Calculation executed, outputs 0
count.value++ // Only marks dirty, no calculation
console.log(double.value) // Calculation executed, outputs 2

Scheduling Control of Side Effect Functions

Vue3's reactive system controls the execution timing of side effects through the scheduler parameter. In component update scenarios, side effects triggered by multiple state changes are batched:

const queue: (Effect | ReactiveEffect)[] = []
let isFlushing = false

function queueJob(job: Effect | ReactiveEffect) {
  if (!queue.includes(job)) {
    queue.push(job)
    queueFlush()
  }
}

function queueFlush() {
  if (!isFlushing) {
    isFlushing = true
    Promise.resolve().then(flushJobs)
  }
}

function flushJobs() {
  try {
    for (let i = 0; i < queue.length; i++) {
      queue[i]()
    }
  } finally {
    isFlushing = false
    queue.length = 0
  }
}

This design ensures that when multiple reactive data changes occur consecutively, side effects are executed only once:

const state = reactive({ count: 0 })

effect(() => {
  console.log(state.count)
}, {
  scheduler: job => queueJob(job)
})

state.count++ // Triggered but not immediately executed
state.count++ // Triggered again
// Only prints 2 once

Lazy Handling of Dependency Collection

Vue3 also adopts a lazy strategy for dependency collection. Dependencies are established only when properties are actually accessed within side effect functions:

function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    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 = new Set()))
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}

This design ensures that changes to unused data do not trigger unnecessary updates. For example:

const obj = reactive({ a: 1, b: 2 })

effect(() => {
  // Only accesses property 'a'
  console.log(obj.a)
})

obj.b = 3 // Does not trigger side effect execution

Lazy Optimization Under Conditional Branches

Vue3's reactive system intelligently handles dependency changes in conditional branches. When branch conditions change, the system automatically adjusts dependency collection:

const showA = ref(true)
const a = ref('a')
const b = ref('b')

effect(() => {
  if (showA.value) {
    console.log(a.value)
  } else {
    console.log(b.value)
  }
})

// Initially depends on 'a'
showA.value = false // Automatically removes dependency on 'a', adds dependency on 'b'

This mechanism is achieved by cleaning up old dependencies before each side effect function execution:

function cleanup(effect: ReactiveEffect) {
  const { deps } = effect
  for (let i = 0; i < deps.length; i++) {
    deps[i].delete(effect)
  }
  deps.length = 0
}

function effect(fn, options) {
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect()
  }
  return effect
}

function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
    if (!effect.active) {
      return options.scheduler ? undefined : fn()
    }
    if (!effectStack.includes(effect)) {
      cleanup(effect)
      try {
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  }
  // Other property initializations...
  return effect
}

Update Optimization for Nested Components

In component tree scenarios, Vue3 leverages lazy updates to avoid unnecessary child component renders. When a parent component updates, it first checks if the child component's props have changed:

function updateComponent(n1, n2, optimized) {
  const instance = n2.component = n1.component
  if (shouldUpdateComponent(n1, n2, optimized)) {
    instance.next = n2
    instance.update()
  } else {
    n2.component = n1.component
    n2.el = n1.el
  }
}

function shouldUpdateComponent(prevVNode, nextVNode, optimized) {
  const { props: prevProps } = prevVNode
  const { props: nextProps } = nextVNode
  if (prevProps === nextProps) return false
  if (!nextProps) return true
  if (prevProps === null) return true
  
  for (const key in nextProps) {
    if (nextProps[key] !== prevProps[key]) {
      return true
    }
  }
  return false
}

This ensures that when parent component state changes but child component props remain unchanged, the child component does not re-render:

// Parent.vue
const count = ref(0)
setInterval(() => count.value++, 1000)

// Child.vue
// Even if the parent updates every second, the child won't update if props don't change

Static Hoisting in the Compilation Phase

Vue3's compiler also applies lazy thinking through static hoisting to reduce runtime overhead. For static nodes and attributes, the compiler hoists them outside the render function:

// Before compilation
<template>
  <div>
    <span class="static">Hello</span>
    <span>{{ dynamic }}</span>
  </div>
</template>

// After compilation
const _hoisted_1 = /*#__PURE__*/_createVNode("span", {
  class: "static"
}, "Hello", -1 /* HOISTED */)

function render(_ctx) {
  return (_openBlock(), _createBlock("div", null, [
    _hoisted_1,
    _createVNode("span", null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
  ]))
}

The static node _hoisted_1 is created only once during initialization and reused in subsequent updates, avoiding the overhead of repeated VNode creation.

Lazy Behavior Differences in Reactive APIs

Different reactive APIs exhibit varying lazy behaviors:

  1. ref and reactive: Immediately execute getters to collect dependencies
  2. computed: Lazy by default, calculates only when .value is accessed
  3. watch and watchEffect: watchEffect executes immediately, watch is lazy by default
const count = ref(0)

// Executes immediately
watchEffect(() => console.log(count.value))

// Lazy, requires explicit invocation
const stop = watch(count, (val) => console.log(val))
stop() // Can be manually stopped

This differential design allows developers to choose the most suitable API based on scenarios. For example, use watchEffect for immediate initialization logic and watch for precise control.

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.