前言
前一篇《Vue 3 Pre-Alpha / vue-next 源码学习之 vue/reactivity/effect.ts》中,我们学习了用于响应数据读写变化的方法(在 vue 3 中会把用户传入的响应方法/响应函数再次封装而成为 effect 或者叫 ReactiveEffect),现在我们来介绍一种依赖于一种依赖于 effect 的扩展于 Ref 的响应式数据类型 computed(ComputedRef),相信用过 vue 的人都大概会猜到 computed 应该于 vue 1.x / vue 2. x 中的 computed 有很大的关联。没错,用法与功能大概一样。
源码分析
注意带【】符号的为关键代码,关键代码能体现作者的最关键最基础的实现思路
import { effect, ReactiveEffect, effectStack } from './effect' // couputed 依赖于 effect 下面可以看到
import { Ref, UnwrapRef } from './ref' // couputed 其实是 extends 于 Ref 下面可以看到
import { isFunction, NOOP } from '@vue/shared' // 工具函数,判断是不是函数与返回一个空对象
export interface ComputedRef<T> extends WritableComputedRef<T> { // 构造函数 computed 返回为 ComputedRef / WritableComputedRef / any
readonly value: UnwrapRef<T> // 比普通的 Ref 多了一个 readonly 的 value 值为解套后的值
}
export interface WritableComputedRef<T> extends Ref<T> { // 比普通的 Ref 多了一个 readonly 的 effect 的 Ref
readonly effect: ReactiveEffect<T>
}
export type ComputedGetter<T> = () => T // 带返回值的 getter
export type ComputedSetter<T> = (v: T) => void // 不带返回值的 setter
export interface WritableComputedOptions<T> { // computed 的参数接口函数定义
get: ComputedGetter<T>
set: ComputedSetter<T>
}
export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
): any {
const isReadonly = isFunction(getterOrOptions)
const getter = isReadonly
? (getterOrOptions as ComputedGetter<T>) // 只传一个 getter
: (getterOrOptions as WritableComputedOptions<T>).get // 同时传了 getter 及 setter
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, { // 使用effect把 getter 封装成 ReactiveEffect,reactive / Ref 对象与 effect 是通过 targetMap 来建立绑定关系的
lazy: true, // 异步触发执行
// mark effect as computed so that it gets priority during trigger
computed: true, // 标记为 true,优先级提升,优化触发
scheduler: () => { // 把 dirty 设为 true,开始时设置数据为变更状态
dirty = true
}
})
return { // 【computed 的关键代码】 返回一个比普通的 Ref 多了一个 readonly 的 effect 的 Ref,主要也是通过 get set 实现
_isRef: 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) // 如果有父 effect 与之关联,所有的 parentRunner 也要被 track
return value
},
set value(newValue: T) {
setter(newValue)
}
}
}
function trackChildRun(childRunner: ReactiveEffect) {
if (effectStack.length === 0) {
return
}
const parentRunner = effectStack[effectStack.length - 1]
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)
}
}
}
总结
通过源码我们可以知道,computed 扩展于 Ref,也是一种 Ref,跟 Ref 一样主要也是通过 get set 附加上带 getter 函数的 effect 来实现监听响应式的能力。下面通过两个测试用例来验证:
it('should return updated value or not', () => {
const value = reactive<{ foo?: number }>({})
const cValue = computed(() => value.foo)
expect(cValue.value).toBe(undefined)
value.foo = 1
expect(cValue.value).toBe(1) // 值跟着变化,因为响应变化的能力来源于 reactive
const n = ref(1)
const plusOne = computed(() => n.value + 1)
expect(plusOne.value).toBe(2)
n.value = 2
expect(plusOne.value).toBe(3) // 值跟着变化,因为响应变化的能力来源于 ref
const plainObject = { foo: 0 }
const cPOValue = computed(() => plainObject.foo)
expect(cPOValue.value).toBe(0)
plainObject.foo = 1
expect(cPOValue.value).toBe(0) // 值不变,因为 computed 的不是响应式的 Ref / reactive,而是 plain Object
})