computed 作用
根据一些依赖的响应式数据计算出新值并返回。当依赖发生变化时,计算属性可以自动重新计算获取新值
computed 函数的具体实现
计算属性在源码中就是通过 computed 函数实现的
js
function computed(getterOrOptions) {
// getter 函数
let getter
// setter 函数
let setter
// 标准化参数
if (isFunction(getterOrOptions)) {
// 表面传入的是 getter 函数,不能修改计算属性的值
getter = getterOrOptions
setter = (process.env.NODE_ENV !== 'production')
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
}
else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
// 数据是否脏的
let dirty = true
// 计算结果
let value
let computed
// 创建副作用函数
const runner = effect(getter, {
// 延时执行
lazy: true,
// 标记这是一个 computed effect 用于在 trigger 阶段的优先级排序
computed: true,
// 调度执行的实现
scheduler: () => {
if (!dirty) {
dirty = true
// 派发通知,通知运行访问该计算属性的 activeEffect
trigger(computed, "set" /* SET */, 'value')
}
}
})
// 创建 computed 对象
computed = {
__v_isRef: true,
// 暴露 effect 对象以便计算属性可以停止计算
effect: runner,
get value() {
// 计算属性的 getter
if (dirty) {
// 只有数据为脏的时候才会重新计算
value = runner()
dirty = false
}
// 依赖收集,收集运行访问该计算属性的 activeEffect
track(computed, "get" /* GET */, 'value')
return value
},
set value(newValue) {
// 计算属性的 setter
setter(newValue)
}
}
return computed
}
计算属性的整体运行流程
computed 内部两个重要的变量:
- dirty 表示一个计算属性的值是否是"脏的",用来判断需不需要重新计算
- value 表示计算属性每次计算后的结果。
看以下示例:
js
<template>
<div>
{{ plusOne }}
</div>
<button @click="plus">plus</button>
</template>
<script>
import { ref, computed } from 'vue'
export default {
setup() {
const count = ref(0)
const plusOne = computed(() => {
return count.value + 1
})
function plus() {
count.value++
}
return {
plusOne,
plus
}
}
}
</script>
特别注意这是两个依赖收集过程:
- 对于 plusOne 来说,它收集的依赖是组件副作用渲染函数
- 对于 count 来说,它收集的依赖是 plusOne 内部的 runner 函数
注意,count 的值变化时,不会直接调用 runner 函数,而是把 runner 作为参数去执行 scheduler 函数。 这里需要回顾一下 trigger 函数内部对于 effect 函数的执行方式:
js
const run = (effect) => {
// 调度执行
if (effect.options.scheduler) {
effect.options.scheduler(effect)
}
else {
// 直接运行
effect()
}
}
如果通过 effect 创建副作用函数时,options 中有传入 scheduler,则执行 scheduler 函数(上面 runner 函数就是传入了 scheduler) runner 函数中传入的 scheduler 中,并没有对计算属性求新值,而是:
- 将 dirty 设置为 true
- 执行 trigger,去通知执行 plusOne 依赖的组件渲染副作用函数,从而触发组件重新渲染
组件重新渲染后,再次访问 plusOne,触发 getter,此时 dirty 为 true,所以执行 runner,求得新值 count.value + 1 这就是虽然组件没有直接访问 count,但是当我们修改 count 的值的时候,组件仍然会重新渲染的原因。
通过以上分析,我们可以得到 computed 计算属性的两个特点:
- 延时计算,只有当我们访问计算属性的时候,它才会真正运行 computed getter 函数计算;
- 缓存,它的内部会缓存上次的计算结果 value,而且只有 dirty 为 true 时才会重新计算。如果访问计算属性时 dirty 为 false,那么直接返回这个 value。
和单纯使用普通函数相比,计算属性的优势是 :只要依赖不变化,就可以使用缓存的 value 而不用每次在渲染组件的时候都执行函数去计算,这是典型的空间换时间的优化思想。
嵌套的计算属性
看以下示例:
js
const count = ref(0)
const plusOne = computed(() => {
return count.value + 1
})
const plusTwo = computed(() => {
return plusOne.value + 1
})
console.log(plusTwo.value)
流程与上面同理。 依赖收集:
- 对于 plusOne 来说,它收集的依赖是 plusTwo 内部的 runner 函数;
- 对于 count 来说,它收集的依赖是 plusOne 内部的 runner 函数。
整体过程:
- 修改 count 的值时,它会派发通知先运行 plusOne 内部的 scheduler 函数,把 plusOne 内部的 dirty 变为 true
- 然后再次派发通知,运行 plusTwo 内部的 scheduler 函数,把 plusTwo 内部的 dirty 设置为 true。
- 当我们再次访问 plusTwo 的值时,发现 dirty 为 true,就会执行 plusTwo 的 runner 函数去执行 plusOne.value + 1,进而执行 plusOne 的 runner 函数即 count.value + 1 + 1,求得最终新值 2。
得益于 computed 这种巧妙的设计,无论嵌套多少层计算属性都可以正常工作。
计算属性的执行顺序
计算属性内部创建副作用函数的时候会配置 computed 为 true,标识这是一个 computed 的 effect,用于在 trigger 阶段的优先级排序。 trigger 阶段执行 effect 的过程:
js
const add = (effectsToAdd) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || !shouldTrack) {
if (effect.options.computed) {
computedRunners.add(effect)
}
else {
effects.add(effect)
}
}
})
}
}
const run = (effect) => {
if (effect.options.scheduler) {
effect.options.scheduler(effect)
}
else {
effect()
}
}
computedRunners.forEach(run)
effects.forEach(run)
由上述代码得知,会先执行 computed 的 effect,然后执行普通的 effect 为何要如此设计?因为考虑到下面这种特殊场景(同时读取 plusOne 和 count):
js
import { ref, computed } from 'vue'
import { effect } from '@vue/reactivity'
const count = ref(0)
const plusOne = computed(() => {
return count.value + 1
})
effect(() => {
console.log(plusOne.value + count.value)
})
function plus() {
count.value++
}
plus()
plus++ 之后,运行后的结果:
1
3
3
如果不先执行 computed effect 而先执行普通 effect 的话,则输出的是 1 2 3 这里是因为 computed 的 effect 并不会立即执行 runner,而是先将 dirty 设置为 true,然后触发 computed effect 执行重新读取 plusOne 的值时才更新。如果先执行普通 effect,则第二次输出的时候 dirty 还为 false,此时读取 plusOne 时读不到最新值,所以会输出 2。 这就是设计 computed 的 effect 先执行的原因。