源码阅读与调试
计算属性computed会基于其响应式以来被缓存 ,并且在依赖的响应式数据发生变化时重新计算 。
创建一个computed的测试实例;
js
const { reactive, effect, computed } = Vue
const obj = reactive({
name: '张三'
})
const computedObj = computed(() => {
return '姓名:' + obj.name
})
effect(() => {
document.querySelector('#app').innerText = computedObj.value
})
setTimeout(() => {
obj.name = '李四'
}, 2000)
reactive之前文章有详细描述,这里就不赘述了,直接来看本文的主题computed;
ts
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
// isFunction判断是不是函数
const onlyGetter = isFunction(getterOrOptions)
// onlyGetter为true
if (onlyGetter) {
getter = getterOrOptions
// 当前没有setter,所以它现在是一个空函数
setter = __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
// 返回ComputedRefImpl的实例
return cRef as any
}
第9行的getterOrOptions
此时为我们computed传入的函数,如下图所示;
接着会进入new ComputedRefImpl
;
ts
export class ComputedRefImpl<T> {
public dep?: Dep = undefined
private _value!: T
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true
// _dirty变量非常重要
public _dirty = true
public _cacheable: boolean
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean
) {
// 如果effect.run()执行就会触发getter方法,接着触发回调函数
this.effect = new ReactiveEffect(getter, () => {
// 如果this._dirty为false,执行triggerRefValue方法
// this._dirty用来控制什么时候触发依赖
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
})
}
}
第20行中传入的getter就是我们在computed中传入的函数,如下图所示:
到这里computed函数就执行完成了;接着测试实例中是在effect方法中触发了computedObj的Getter,也就是class ComputedRefImpl的get方法;
ts
export class ComputedRefImpl<T> {
get value() {
// self可以被看作this
const self = toRaw(this)
// 收集依赖
trackRefValue(self)
// _dirty为true
if (self._dirty || !self._cacheable) {
self._dirty = false
// 这里会执行computed中传入的函数
self._value = self.effect.run()!
}
// self._value为"姓名:张三"
return self._value
}
set value(newValue: T) {
this._setter(newValue)
}
}
第14行返回self._value,此时的self._value如下图所示:
到这里我们发现computed本质上也没有代理监听的机制,它也是通过get value和set value进行的触发;我们发现get value和set value中只有收集依赖没有触发依赖;触发依赖在class ComputedRefImpl的constructor中,也就是ReactiveEffect的回调函数里;这时我们去看一下ReactiveEffect都做了什么:
调度器相关逻辑,先来划重点:
我们继续调试跟踪代码,执行settimtout时,会触发Setter,也就是baseHandler文件中的createSetter方法;
createSetter方法=>trigger方法=>triggerEffects方法=>triggerEffect方法
看一下triggerEffects方法具体做了什么:
ts
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
const effects = isArray(dep) ? dep : [...dep]
for (const effect of effects) {
// 判断是否有computed
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
}
triggerEffect方法;
ts
function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
// 判断是否有scheduler
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
第2行中effect如下图所示;它里面是包含一个scheduler的;因此它会执行scheduler的逻辑;这是会再次执行class ComputedRefImpl中constructor里的传入的函数;
scheduler也就是我们测试实例中computed传入的方法;
js
const computedObj = computed(() => {
return '姓名:' + obj.name
})
进入scheduler方法,也就是我们new ReactiveEffect传入的回调函数;
ts
export class ComputedRefImpl<T> {
constructor(
) {
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
})
}
}
第5行的this.dirty是false,会去触发依赖;会再次进入triggerRefValue方法;
triggerRefValue方法=>triggerEffects方法
ts
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
const effects = isArray(dep) ? dep : [...dep]
// 第一层for循环没有触发
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
// 触发第二层for循环
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
}
此时的effect的scheduler不存在了,fn也变成了effect中传入的方法;
fn此时如下图所示:
执行完effect,页面视图也从姓名:张三 变成了姓名:李四;
触发依赖:
- 第一次settimeout中触发Setter;
- 第二次是在new ReactiveEffect的回调函数中触发;
computed的总结:
- computed的本质是一个
ComputedRefImpl
的实例; ComputedRefImpl
中通过dirty变量来控制run的执行和triggerRefValue的触发;- 想要访问计算属性的值,必须通过.value,因为它内部和ref一样是通过get value来进行实现的;
- 每次.value时都会触发trackRefValue即:收集依赖;
- 在触发依赖时,需要先触发computed的effect,再触发非computed的effect;
实现computed
实现class ComputedRefImpl
首先创建一个computed的测试实例:
js
const { reactive, effect, computed } = Vue;
const obj = reactive({
name: "张三",
});
const computedObj = computed(() => {
return "姓名:" + obj.name;
});
effect(() => {
document.querySelector("#app").innerText = computedObj.value;
});
setTimeout(() => {
obj.name = "李四";
}, 2000);
创建packages/reactivity/src/computed.ts
ts
import { isFunction } from "@vue/shared";
import { ReactiveEffect } from "./effect";
import { trackRefValue } from "./ref";
import { Dep } from "./dep";
export type ComputedGetter<T> = (...args: any[]) => T;
export function computed<T>(getterOrOptions: ComputedGetter<T>) {
let getter: ComputedGetter<T>;
const onlyGetter = isFunction(getterOrOptions);
if (onlyGetter) {
getter = getterOrOptions;
const cRef = new ComputedRefImpl(getter);
return cRef as any;
}
}
export class ComputedRefImpl<T> {
public dep?: Dep = undefined;
private _value!: T;
private readonly effect: ReactiveEffect<T>;
public _dirty = true;
constructor(getter: ComputedGetter<T>) {
this.effect = new ReactiveEffect(getter);
}
get value() {
trackRefValue(this);
return this._value;
}
}
shared/index.ts中增加:
ts
export const isFunction = (fn: unknown) => typeof fn === "function";
还有两个地方需要导出computed;
实现调度器及dirty相关逻辑
修改packages/reactivity/src/computed.ts;
ts
export class ComputedRefImpl<T> {
public dep?: Dep = undefined;
private _value!: T;
private readonly effect: ReactiveEffect<T>;
public _dirty = true;
constructor(getter: ComputedGetter<T>) {
this.effect = new ReactiveEffect(getter, () => {
console.log("schedule");
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();
}
return this._value;
}
}
修改effect.ts;
ts
export type EffectScheduler = (...args: any[]) => any;
export function triggerEffects(dep: Dep) {
// 转化成数组
const effects = isArray(dep) ? dep : [...dep];
// 依次触发依赖
for (const effect of effects) {
triggerEffect(effect);
}
}
// 触发指定依赖
export function triggerEffect(effect: ReactiveEffect) {
// 判读是否有scheduler
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
export class ReactiveEffect<T = any> {
computed?: unknown;
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null
) {}
}
此时测试实例就具有响应性了;
实现computed的缓存性
computed是有缓存性的,我们通过一个测试实例来看一下:
js
const { reactive, effect, computed } = Vue
const obj = reactive({
name: '张三'
})
const computedObj = computed(() => {
console.log('计算属性执行')
return '姓名:' + obj.name
})
effect(() => {
document.querySelector('#app').innerText = computedObj.value
document.querySelector('#app').innerText = computedObj.value
})
setTimeout(() => {
obj.name = '李四'
}, 2000)
第一次log是computed初始化,第二次是setTimeout触发了Setter;说明vue3的computed是具有缓存性,此时我们可以用刚才实现computed来做对比实验;
triggerEffects方法:
第一遍执行,打印出effects:
第二遍执行,打印出effects:
schedule如果在run之后执行,就会疯狂执行computed中的方法,形成死循环;schedule中做了什么呢?schedule中把_dirty改成了true,并且又一次triggerRefValue;所以就会造成死循环;想到这里大家应该有解决办法了吧;只要改变一下schedule的执行顺序,问题就可以解决啦!
ts
export function triggerEffects(dep: Dep) {
// 转化成数组
const effects = isArray(dep) ? dep : [...dep];
// 依次触发依赖
// 判断是否有computed
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect);
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect);
}
}
}