响应式系统的惰性求值
响应式系统的惰性求值
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的惰性行为有所区别:
ref
和reactive
:立即执行getter收集依赖computed
:默认惰性,只有访问.value时计算watch
和watchEffect
:watchEffect
立即执行,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
上一篇:自定义元素的支持方式
下一篇:虚拟DOM的静态标记