阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 响应式系统的惰性求值

响应式系统的惰性求值

作者:陈川 阅读数:35773人阅读 分类: Vue.js

响应式系统的惰性求值

Vue3的响应式系统通过Proxy实现了数据的自动追踪依赖和触发更新。其中惰性求值机制是优化性能的关键设计,它确保计算只在真正需要时执行。这种机制主要体现在计算属性(computed)和副作用(effect)的实现中。

计算属性的惰性特性

计算属性默认是惰性的,只有在实际被访问时才会进行计算。Vue3内部通过ComputedRefImpl类实现这一特性:

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
  }
}

关键点在于_dirty标志位,它控制着是否需要重新计算。当依赖项变化时,只会标记_dirty为true,而不是立即重新计算。例如:

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

console.log(double.value) // 计算执行,输出0
count.value++ // 只标记dirty,不计算
console.log(double.value) // 计算执行,输出2

副作用函数的调度控制

Vue3的响应式系统通过scheduler参数控制副作用的执行时机。在组件更新场景中,多个状态变化触发的副作用会被批量处理:

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
  }
}

这种设计使得连续多次修改响应式数据时,副作用只会执行一次:

const state = reactive({ count: 0 })

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

state.count++ // 触发但不会立即执行
state.count++ // 再次触发
// 最终只会打印一次2

依赖收集的延迟处理

Vue3在依赖收集时也采用了惰性策略。只有在副作用函数实际访问属性时才会建立依赖关系:

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)
  }
}

这种设计使得未使用的数据变化不会触发不必要的更新。例如:

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

effect(() => {
  // 只访问a属性
  console.log(obj.a)
})

obj.b = 3 // 不会触发副作用执行

条件分支下的惰性优化

Vue3的响应式系统能智能处理条件分支中的依赖变化。当分支条件改变时,系统会自动调整依赖收集:

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)
  }
})

// 初始依赖a
showA.value = false // 自动移除a的依赖,添加b的依赖

这种机制通过每次执行副作用函数前清理旧依赖实现:

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]
      }
    }
  }
  // 其他属性初始化...
  return effect
}

嵌套组件的更新优化

在组件树场景中,Vue3利用惰性更新避免不必要的子组件渲染。父组件更新时,会先比较子组件的props是否变化:

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
}

这使得父组件状态变化但子组件props未变时,子组件不会重新渲染:

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

// Child.vue
// 即使父组件每秒更新,只要props不变子组件就不会更新

编译阶段的静态提升

Vue3的编译器也会应用惰性思想,通过静态提升减少运行时开销。对于静态节点和属性,编译器会将其提升到渲染函数外部:

// 编译前
<template>
  <div>
    <span class="static">Hello</span>
    <span>{{ dynamic }}</span>
  </div>
</template>

// 编译后
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 */)
  ]))
}

静态节点_hoisted_1只会在初始化时创建一次,后续更新时直接复用,避免重复创建VNode的开销。

响应式API的惰性差异

不同响应式API的惰性行为有所区别:

  1. refreactive:立即执行getter收集依赖
  2. computed:默认惰性,只有访问.value时计算
  3. watchwatchEffectwatchEffect立即执行,watch默认惰性
const count = ref(0)

// 立即执行
watchEffect(() => console.log(count.value))

// 惰性,需要显式调用
const stop = watch(count, (val) => console.log(val))
stop() // 可以手动停止

这种差异设计使得开发者可以根据场景选择最合适的API。例如需要立即执行的初始化逻辑使用watchEffect,需要精确控制时使用watch

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

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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌