本文是专栏的第四篇,上一篇地址为vue3源码阅读与实现: 响应式系统-ref模块
computed模块
模块总览
computed
接收一个getter
函数,并返回一个计算属性ref
,当getter
函数中的响应式状态变化后,会引起getter
重新计算,通过计算属性ref
可以访问计算的结果.计算属性有以下特点:
- 缓存性: 如果
getter
中的响应式状态没有变化,多次访问计算属性ref
的值会直接使用缓存而不是重新计算 - 不推荐直接修改计算属性: 如果一定要直接修改,必须同时传递
getter
和setter
函数
有以下一些问题:
- 计算属性是一个响应式数据,所以一定会有收集依赖和触发依赖的操作.收集依赖可以在访问计算属性时进行,那触发依赖应该在什么时机进行呢?
- 计算属性的缓存是怎么实现的?
- 为什么访问计算属性需要通过
.value
?
带着这些疑问来一起debugger
debugger
使用如下测试用例:
...
<div id="app"></div>
<script>
const { reactive, effect, computed } = Myvue;
const obj = reactive({ name: "响应式" });
debugger
const computedData = computed(() => { // 关注点1: computed对getter函数进行了怎样的处理,又返回了什么值
return `这是计算数据的返回的值: ${obj.name}`;
});
effect(() => {
debugger
document.querySelector("#app").innerHTML = computedData.value; // 关注点2: 访问计算属性时,计算属性如何收集依赖
});
setTimeout(() => {
debugger
obj.name = "修改之后值"; // 关注点3: 响应式状态改变,怎么引发计算属性触发依赖的
}, 2000);
...
同样,debugger
时有以下关注点:
- 关注点1:
computed
对getter
函数进行了怎样的处理,又返回了什么值 - 关注点2: 访问计算属性时,计算属性如何收集依赖
- 关注点3: 响应式状态改变,怎么使计算属性触发依赖的
vscode
中右键open with live server
,开始调试
关注点1:计算属性的创建
-
进入第一个
debugger
,首先根据参数类型对getter
和setter
进行了初始化,然后返回了ComputedRefImpl
实例,根据这个名字是不是就知道为什么官网叫计算属性ref
了吧
-
进入
ComputedRefImpl
,该类有两个重要属性-
effect
: 存放了计算属性的回调函数,this.effect.run()
可以重新执行该回调 -
_dirty
: 表示计算属性依赖的响应式数据是否变化,如果变化了在获取计算属性时需要重新执行this.effect.run
重新计算
-
-
在其构造函数中,首先创建了
ReactiveEffect
实例effect
,这一步就把getter
保存在了effect
实例中,同时注意到还传递了第二个参数,值是一个箭头函数,箭头函数中包含一个triggerRefValue
,看名字这个箭头函数应该和触发依赖有关,
-
进入
ReactiveEffect
查看第二个参数,是一个调度器scheduler
,(很重要,计算属性触发依赖的关键)
-
触发依赖时这个调度器的执行优先级比
run
函数高,可以从triggerEffect
函数看出来,有调度器就不会执行run
函数了
- 这里先有个印象,之后触发调度器的时候再详细讲解,到此,返回了这个计算属性
ref
,computed
的初始化逻辑就结束了
总结
computed
函数主要做了一件事情:
- 创建
ComputedRefImpl
实例,并返回,在实例中创建了一个ReactiveEffect
,和以往响应式处理不同的是这次传递了一个调度器scheduler
关注点2:计算属性的依赖收集
进入第二个debugger
-
触发了计算属性的
get value
,首先进行依赖收集trackRefValue(在ref模块中讲解过)
,然后执行了this.effect.run
,
-
这里的
run
函数其实触发的就是,计算属性的参数() => { return `这是计算数据的返回的值: ${obj.name}`;}
由于函数中访问了,
obj
的name
,所以会触发obj
的依赖收集,这个过程在reactive
章节中已经详细描述,这里不再赘述,只说最终结果: 将this.effect
添加到obj
的依赖集合中 -
最后返回了
run
函数运行的结果
总结
这个过程中ComputedRefImpl
实例主要在get value中
做了2
件事情
- 收集依赖,这里收集到的是计算属性的依赖
- 执行
run
函数,触发响应式状态的依赖收集
关注点3:计算属性的依赖触发
在这里,对响应式数据进行修改,查看响应式数据的依赖触发是如何引起计算属性的依赖触发的
-
进入
debugger
,修改name
后,首先在proxy
的set
中会先触发name
属性相关依赖,也就是,在关注点2第2步收集的this.effect
-
在关注点1我们知道
this.effect
是一个包含调度器的effect
,因此触发时,会优先执行调度器逻辑,如图:
-
而在调度器
schduler
中,正是计算属性触发依赖的逻辑,因此开始触发计算属性的依赖
- 这样就实现了,响应式数据变化,触发计算属性重新计算的逻辑
总结
当响应式状态变化时,通过调度器,开启了计算属性的依赖触发,从而保证了自动执行下面的逻辑:响应式数据变化 => 计算属性变化 => 使用计算属性的地方变化
总结
在计算属性中,依赖关系是这样的: 数据变化 => 计算属性重新计算 => 使用计算属性的地方自动更新
:
- 数据变化时触发依赖,使计算属性重新计算
- 计算属性重新计算,触发计算属性的依赖,从而让使用计算属性的地方自动更新
总的来说,计算属性的实现需要解决这么个问题:
因为计算属性没有set
,因此无法像ref
一样在set value
中处理计算属性的依赖触发逻辑.那么就要找一个时机,来进行计算属性的依赖触发:
由上述第一步可知,数据变化相当于计算属性发生变化,那么在数据变化触发依赖时,进行计算属性的依赖触发即可.由reactive和ref
模块源码可知,比如fn
中访问了响应式数据那么fn
就会被作为依赖收集起来,依赖触发时会重新执行fn
,这样的依赖收集和触发逻辑显然不符合现在的期望,我们期望的是由fn
触发了响应式数据的依赖收集,但依赖触发的时候不执行fn
而是干点别的,比如去触发计算属性的依赖
.因此为了实现这个逻辑,就可以使用调度器scheduler
,在scheduler
中进行计算属性的依赖触发,因为这个函数的优先级高,有他在依赖触发的时候就不会执行run
而总是执行调度器
实现computed模块
computed函数
创建ComputedRefImpl
,在创建ReactiveEffect
实例时,需要传递第二个参数是scheduler
在packages/reactivity/src/computed.ts
,
import { isFunction } from "@vue/shared";
import { Dep } from "./deps";
import { ReactiveEffect } from "./effect";
import { trackRefValue, triggerRefValue } from "./ref";
export function computed(getterOrOptions) {
let getter;
const onlyGetter = isFunction(getterOrOptions);
if (onlyGetter) {
getter = getterOrOptions;
}
const cRef = new ComputedRefImpl(getter);
return cRef;
}
export class ComputedRefImpl<T> {
private _value!: T;
public dep?: Dep;
public readonly effect: ReactiveEffect<T>; // 重要属性,该实例保存了计算属性中的函数参数,也就是说每次需要获取计算属性的值,都要调用这个属性的run方法
public readonly __v_isRef: boolean = true;
public _dirty: boolean = true; // 重要属性,决定是否重新计算和触发计算属性的依赖
constructor(getter) {
// 传递调度器的原因: 在响应式数据触发依赖时,执行一些其他逻辑,而不是依赖的run函数.
// 这里的scheduler意思是, 响应式数据触发依赖时也触发计算属性的依赖
// 原因: 计算属性不一定有set操作所以不能在set中进行依赖触发,
// 但计算属性的依赖一定需要计算属性的值变化时触发才能完成响应式, 计算属性的值变化依赖于某些响应式数据,那么当响应式数据变化时触发依赖即可
// 当前的响应式收集依赖和触发依赖都是通过执行effect的run方法,现在的场景需要:
// 收集依赖时通过run方法, 触发依赖时执行其他方法, 于是在effect中新加一个属性, 触发依赖时, 这个属性有值, 就执行这个属性
// 因此将依赖触发的逻辑放在scheduler中,
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true;
triggerRefValue(this);
}
});
this.effect.computed = this;
}
get value() {
// 依赖收集
trackRefValue(this);
if (this._dirty) {
this._dirty = false;
this._value = this.effect.run()!;
}
// 返回_value
return this._value;
}
}
修改ReactiveEffect
为之前定义的ReactiveEffect
类新增shceduler
属性
在packages/reactivity/src/effets.ts
中
export type EffectScheduler = (...args: any[]) => any;
export class ReactiveEffect<T = any> {
...
public scheduler?: EffectScheduler | null = null; // 调度器,如果有调度器,触发依赖时会优先执行调度器
constructor(fn: () => T, scheduler?: EffectScheduler) {
...
this.scheduler = scheduler; // 和计算属性相关
}
}
总结
回到刚开始的几个问题:
-
计算属性是一个响应式数据,所以一定会有收集依赖和触发依赖的操作.收集依赖可以在访问计算属性时进行,那触发依赖应该在什么时机进行呢?
计算属性的依赖触发通过
scheduler
在响应式状态发生变化时触发 -
计算属性的缓存是怎么实现的?
每个计算属性
ref
都有一个_dirty
属性,表示响应式数据是否发生了变化,如果变化,那么在执行访问计算属性时会重新计算,否则使用缓存的值 -
为什么访问计算属性需要通过
.value
?- 计算属性实际上是一个
ComputedRefImpl
实例,和ref
一样内部通过属性访问器get value()
实现,所以需要.value
才能访问
- 计算属性实际上是一个