前言
前一篇《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
})