Vue3的computed是基于effect实现的。
computed实现思路
- 用户使用计算属性时写法与defineProperty相似
- 计算属性具有缓存的功能,如果值不变,多次取值只会执行一次get
- 可以用一个标识符来实现缓存,如果值有变化就重新执行get,没有变化就不需要重新执行get
- 所以计算属性可以是一个effect,当依赖的属性变化了就更新标识
effect方法在前面的学习中已经实现,所以在本次代码书写中可以对effect方法进行优化,公共逻辑提取,即可在computed中直接使用。
例子
packages/reactivity/dist/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./reactivity.global.js"></script>
<!-- <script src="../../../node_modules/@vue/reactivity/dist/reactivity.global.js"></script> -->
<script>
const app = document.getElementById('app');
const { effect, reactive, computed } = VueReactivity;
const state = reactive({ name: 'bowen', age: 18 });
const text = computed(() => {
return `名字:${state.name},年龄:${state.age}。`;
});
effect(() => {
app.innerHTML = text.value;
});
setTimeout(() => {
state.age = 24;
}, 1000);
</script>
</body>
</html>
新增的shared方法
packages/shared/src/index.ts
export const isObject = (val: unknown): val is Record<any, any> =>
val !== null && typeof val === 'object';
export const isFunction = (val: unknown): val is Function => typeof val === 'function';
export const isArray = Array.isArray;
export const assign = Object.assign;
export const isString = (val: unknown): val is string => typeof val === 'string';
effect的优化
将之前书写的track函数中effect收集逻辑提取出来命名为trackEffects
export function trackEffects(dep) {
if (activeEffect) {
let shouldTrack = !dep.has(activeEffect);
if (shouldTrack) {
dep.add(activeEffect);
// effect记录属性,清理时用
activeEffect.deps.push(dep);
}
}
}
export function track(target, type, key) {
if (!activeEffect) {
return;
}
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
trackEffects(dep);
}
将之前书写的trigger函数中的调度器以及渲染函数的执行逻辑提取出来命名为triggerEffects
export function triggerEffects(effects) {
effects = [...effects];
effects.forEach((effect) => {
// 屏蔽掉相同的调用
if (effect !== activeEffect) {
if (effect.scheduler) {
// 如果传入了调度器,执行调度器
effect.scheduler();
} else {
effect.run();
}
}
});
}
export function trigger(target, type, key, value, oldValue) {
const depsMap = targetMap.get(target);
if (!depsMap) {
// 触发的值不在模板中使用
return;
}
// 获取属性对应的key
let effects = depsMap.get(key);
if (effects) {
triggerEffects(effects);
}
}
ComputedRefImpl类
packages/reactivity/src/computed.ts
export class ComputedRefImpl {
public effect;
// 脏值,默认是脏的
public _dirty = true;
public readonly __v_isRef = true;
public readonly __v_isReadonly: boolean = false;
private _value;
public dep = new Set();
constructor(public getter, private readonly _setter) {
// getter放入effect中后,将会收集依赖属性
this.effect = new ReactiveEffect(getter, () => {
// 当依赖属性变化的时候,会执行这个调度函数
if (!this._dirty) {
this._dirty = true;
// 触发更新
triggerEffects(this.dep);
}
});
}
// 相当于Object.defineProperty属性访问器
get value() {
// 依赖收集
trackEffects(this.dep);
if (this._dirty) {
this._dirty = false;
this._value = this.effect.run();
}
return this._value;
}
set value(newValue) {
this._setter(newValue);
}
}
- ComputedRefImpl在使用中会传入getter和setter
- 会生成一个effect,进行依赖的属性收集工作,并且将getter放入effect中,生成effect时会传入一个调度函数,当依赖属性变化的时候,会执行这个调度函数出发更新
- ComputedRefImpl有get和set,相当于Object.defineProperty属性访问器
- get时进行依赖收集
- set时直接执行传入的setter
- _dirty用来标记value是否变化
computed函数
packages/reactivity/src/computed.ts
export const computed = (getterOrOptions) => {
let getter = undefined;
let setter = undefined;
// 是否只传入了一个函数
const onlyGetter = isFunction(getterOrOptions);
// 如果传了一个参数,那么为getter赋值,并且没有setter
if (onlyGetter) {
getter = getterOrOptions;
setter = () => {
console.warn('Write operation failed: computed value is readonly');
};
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
const cRef = new ComputedRefImpl(getter, setter);
return cRef;
};
使用computed时参数可以是一个函数或者一个包含get和set的对象,当是一个函数时即为getter,并且没有setter