前言 这次讲讲computed
这个API
computed
computed
在Vue2也有体现,基本上是一个缓存比较大的计算量的值的一种方法,并且能在依赖的值发生变化的时候自动的计算更新后的值。
在我们前面的翻译API的文章中知道了Vue3中computed
接收一个getter函数,返回了一个ref
的对象。
可以用一个例子来简单的看下Vue3中computed
的使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 const {reactive, effect, computed} = VueReactivity ;const o = { name : "lwf" }; const r = reactive (o);const c = computed (() => r.name + " --- computed" );effect (() => { console .log (r.name ); });
这时候我们尝试改变r.name
的值,然后再输出c.value
的值
在更新了响应式对象r
的name
属性之后,它的依赖,也就是我们写的effect便执行了,然后再打印c.value
,发现它的值已经发生了改变。
接下来,我们就来探究这个API是如何做到这种效果的。
首先,老套路,找到它的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 export function computed<T>( getterOrOptions : ComputedGetter <T> | WritableComputedOptions <T> ) { let getter : ComputedGetter <T> let setter : ComputedSetter <T> if (isFunction (getterOrOptions)) { getter = getterOrOptions setter = __DEV__ ? () => { console .warn ('Write operation failed: computed value is readonly' ) } : NOOP } else { getter = getterOrOptions.get setter = getterOrOptions.set } let dirty = true let value : T let computed : ComputedRef <T> const runner = effect (getter, { lazy : true , computed : true , scheduler : () => { if (!dirty) { dirty = true trigger (computed, TriggerOpTypes .SET , 'value' ) } } }) computed = { _isRef : true , effect : runner, get value () { if (dirty) { value = runner () dirty = false } track (computed, TrackOpTypes .GET , 'value' ) return value }, set value (newValue : T ) { setter (newValue) } } as any return computed }
这个函数很长,但总体上可以分成三个部分
匹配参数
建立依赖
返回构建的computed
对象(也就是ref
对象)
匹配参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 export function computed<T>( getterOrOptions : ComputedGetter <T> | WritableComputedOptions <T> ) { let getter : ComputedGetter <T> let setter : ComputedSetter <T> if (isFunction (getterOrOptions)) { getter = getterOrOptions setter = __DEV__ ? () => { console .warn ('Write operation failed: computed value is readonly' ) } : NOOP } else { getter = getterOrOptions.get setter = getterOrOptions.set } }
这里判断了传进来的参数是不是一个函数,如果是,那么这个函数就作为一个getter(此时就是只读的),如果不是,那就分别取get
和set
属性做为getter和setter。
一般而言,使用computed
时,都是直接传递一个函数进去,也就是上面我举的例子一样,基本上用不到setter。
建立effect(依赖) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export function computed<T>( getterOrOptions : ComputedGetter <T> | WritableComputedOptions <T> ) { let dirty = true let value : T let computed : ComputedRef <T> const runner = effect (getter, { lazy : true , computed : true , scheduler : () => { if (!dirty) { dirty = true trigger (computed, TriggerOpTypes .SET , 'value' ) } } }) }
这里构建了一个effect,把传进来的getter传进了effect
这个API,通过之前reactive
API的学习,我们知道,
effect
传进一个函数,把这个函数和这个函数内部使用的响应式变量建立关联,当这些响应式变量发生改变时,就重新执行这个传进来的函数。
但是这个构建的effect和我们之前似乎有点不同,额外的传入了一个参数。
首先是lazy
,这个参数的意思是,不马上执行(也就是自动)这个函数来建立依赖,而是通过返回的函数来手动的来收集依赖。
这里的lazy
在effect
的源码中非常简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 export function effect<T = any >( fn : () => T, options : ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect <T> { if (isEffect (fn)) { fn = fn.raw } const effect = createReactiveEffect (fn, options) if (!options.lazy ) { effect () } return effect }
可以简单的理解,lazy
为假,就自动帮你执行了这个在函数内部创建出来的effect,也就是自动的收集依赖。如果为真,直接返回,可以由用户自己执行来改变收集依赖的时机。
第二个参数为computed
,这个参数上面有一个注释,翻译过来为:给这个effect添加一个标记,以至于它可以在trigger时,优先的执行。
第三个参数为scheduler
,传入的为一个函数。
这里我们可以回到前面看trigger
函数的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 export function trigger ( target : object , type : TriggerOpTypes , key?: unknown , newValue?: unknown , oldValue?: unknown , oldTarget?: Map <unknown , unknown > | Set <unknown > ) { const effects = new Set <ReactiveEffect >() const computedRunners = new Set <ReactiveEffect >() const add = (effectsToAdd : Set <ReactiveEffect > | undefined ) => { if (effectsToAdd) { effectsToAdd.forEach (effect => { if (effect !== activeEffect || !shouldTrack) { if (effect.options .computed ) { computedRunners.add (effect) } else { effects.add (effect) } } else { } }) } } const run = (effect : ReactiveEffect ) => { if (__DEV__ && effect.options .onTrigger ) { effect.options .onTrigger ({ effect, target, key, type , newValue, oldValue, oldTarget }) } if (effect.options .scheduler ) { effect.options .scheduler (effect) } else { effect () } } computedRunners.forEach (run) effects.forEach (run) }
具体的重点都写在代码的注释中了,简单讲就是分effect为两类,一类是一般effect,一类是计算effect,当一个响应式对象发生变化时,先执行它的计算effect,再执行它的一般effect。
为啥要先执行计算effect呢?这个我们最后再说。
返回构建的computed
对象(也就是ref
对象) 我们再看最后一段代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 export function computed<T>( options : WritableComputedOptions <T> ): WritableComputedRef <T> export function computed<T>( getterOrOptions : ComputedGetter <T> | WritableComputedOptions <T> ) { computed = { _isRef : true , effect : runner, get value () { if (dirty) { value = runner () dirty = false } track (computed, TrackOpTypes .GET , 'value' ) return value }, set value (newValue : T ) { setter (newValue) } } as any return computed }
最后就是构建一个ref对象,和一般的ref对象不同的是,计算ref有一个effect
的属性,暴露了通过参数生成的effect对象,可以让我们停止这个effect。
这个构建的ref对象和我们之前通过ref
构建的ref对象基本很像,但是在getter中出现了一个判断。判断了dirty
的值来执行runner
也就是effect。
那这个dirty
是干什么用的呢?我们可以用开头举的例子。
1 2 3 4 5 6 7 8 9 10 11 const {reactive, effect, computed} = VueReactivity ;const o = { name : "lwf" }; const r = reactive (o);const c = computed (() => r.name + " --- computed" );effect (() => { console .log (r.name ); });
当我们打印c.value
时,这是触发了value
的getter,也就是到了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 export function computed<T>( options : WritableComputedOptions <T> ): WritableComputedRef <T> export function computed<T>( getterOrOptions : ComputedGetter <T> | WritableComputedOptions <T> ) { computed = { get value () { if (dirty) { value = runner () dirty = false } track (computed, TrackOpTypes .GET , 'value' ) return value }, } as any }
接下来我们判断了dirty
,dirty
初始化是true
的,所以会执行if块。
这时候执行了runner
,也就是响应式对象r
会收集到传入computed
的函数。
runner
的返回值也就是传入函数的返回值,赋给了value
变量,然后置dirty
变量为false
,接着便是收集依赖,返回value
变量了。
到这,我们就获得了computed对象的value
属性了
如果现在我们再一次的运行c.value
,依然会调用这个属性的getter,但是由于dirty
变量为false
,直接返回了value
对象。
聪明的人可能明白了,dirty
是用来判断依赖到的响应式对象是否发生改变的。
没改变的话,直接返回缓存的值,也就避免了多次的计算。
ok,这时,我执行了r.name = 'index'
,也就是会触发响应式对象r
的所有依赖effect执行。
这时,响应式对象r
的依赖中是有我们传入computed
的函数所构成的effect的。
在trigger
函数中,有一个run
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 export function trigger ( ) { const run = (effect : ReactiveEffect ) => { if (effect.options .scheduler ) { effect.options .scheduler (effect) } else { effect () } } computedRunners.forEach (run) effects.forEach (run) }
这个run
函数就是执行我们的effect的,其中判断了effect.options.scheduler
,为真就传入effect对象并执行,也就是effect.options.scheduler(effect)
,为假就直接执行effect对象。
还记得我们之前通过传入函数生成一个计算effect时传入的额外参数吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 export function computed<T>( getterOrOptions : ComputedGetter <T> | WritableComputedOptions <T> ) { const runner = effect (getter, { scheduler : () => { if (!dirty) { dirty = true trigger (computed, TriggerOpTypes .SET , 'value' ) } } }) }
这里判断了dirty
变量,如果为false
,就置为true
并触发这个计算ref的依赖,dirty
为假的意思就是此时计算ref所依赖的响应式对象已经发生改变了。
前面我们已经打印了c.value
的值了,这时的dirty
就是false
了,进入了if的代码块。置为dirty
为true
,触发了计算ref的依赖。
如果一直没有取value
属性的值,那么就没有必要去触发计算ref的依赖。在第一次获取计算ref的值(.value
)之前,它的值都是不确定的(也不能说不确定,就是会根据它所依赖响应式对象的最新值来进行计算并返回)。
这时我们再次打印c.value
,由于响应式对象r
的改变使得dirty
变为了true
,就又要再执行一次runner
来获取最新的值。
最后,为什么要先执行响应式对象的计算effect呢?
很简单,我们用下面的代码来理解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const {reactive, effect, computed} = VueReactivity ;const o = { name : "lwf" }; const r = reactive (o);const c = computed (() => r.name + " --- computed" );effect (() => { console .log (r.name ); console .log (c.value ); });
这时运行r.name = 'index'
的话,这时会打印两次effect
1 2 3 4 5 6 // r对象改变触发了effect 'index' 'index --- computed' // c对象改变触发了effect 'index' 'index --- computed'
如果你没有先执行计算effect的话,此时对于该计算ref的dirty
还是false
,也就是没有通知到计算ref对象此时依赖的响应式对象发生了变化,使用到了旧的值。也就是打印了
1 2 3 4 5 6 7 // r对象改变触发了effect 'index' // c的getter中dirty还是为false,使用到了旧的值。 'lwf --- computed' // c对象改变触发了effect 'index' 'index --- computed'
至此,computed
API的基本流程基本上就讲完了。不得不说实现上确实很巧妙,用到了很多的闭包。
后记 还差最后一个readonly
API了,这个和reactive
差不多,这几天应该就可以写出来了,如果我写的有错,希望可以指出,非常感谢!~