手写computed
回顾computed的用法
首先回顾一下 computed 的基本用法:
const state = reactive({
a: 1,
b: 2
})
const sum = computed(() => {
return state.a + state.b
})
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed({
get() {
return firstName.value + ' ' + lastName.value
},
set(newValue) {
;[firstName.value, lastName.value] = newValue.split(' ')
}
})
实现computed方法
1.参数归一化
首先第一步,我们需要对参数进行归一化,如下所示:
function normalizeParameter(getterOrOptions) {
let getter, setter;
if (typeof getterOrOptions === "function") {
getter = getterOrOptions;
setter = () => {
console.warn(`Computed property was assigned to but it has no setter.`);
};
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
return { getter, setter };
}
上面的方法就是对传入 computed 的参数进行归一化,无论是传递的函数还是对象,统一都转换为对象。
2.建立依赖关系
接下啦就是建立依赖关系,如何建立呢?
无外乎就是将传入的 getter 函数运行一遍,getter 函数内部的响应式数据和 getter 产生关联:
// value 用于记录计算属性的值,dirty 用于标识是否需要重新计算
let value,
dirty = true;
// 将 getter 传入 effect,getter 里面的响应式属性就会和 getter 建立依赖关系
const effetcFn = effect(getter, {
lazy: true,
});
这里的 value 用于缓存计算的值,dirty 用于标记数据是否过期,一开始标记为过期方便一开始执行一次计算到最新的值。
lazy 选项标记为 true,因为计算属性只有在访问的之后,才会进行计算。
3. 返回一个对象
接下来向外部返回一个对象:
let value;
//建立一个返回对象
const obj = {
get value() {
value = effectFn();
return value;
},
set value(newVal) {
setter(newVal);
},
};
return obj;
该对象有一个 value 访问器属性,当访问 value 值的时候,会根据当前是否为脏值来决定是否重新计算。
优化细节
1.计算属性缓存
我们都知道vue的计算属性是有缓存的,即依赖不改变,compued就不会再次运行;
//但是我们现在实现的是不论依赖是否修改,都会进行运行。
const state = reactive({
a: 1,
b: 2,
});
const sum = computed(() => {
console.log("运行了计算属性");
return state.a + state.b;
});
console.log(sum.value);//("运行了计算属性"); 3
console.log(sum.value);//("运行了计算属性"); 3
console.log(sum.value);//("运行了计算属性"); 3
console.log(sum.value);//("运行了计算属性"); 3
这个时候,我们只需要将计算属性的值进行一下缓存即可
export default function computed(getterOrOptions) {
//...
let value;
let dirty = true; //+++ 设置是否是脏数据,用来判断是否重新计算
const obj = {
get value() {
//+++ 设置计算属性缓存
if (dirty) {
value = effectFn();
dirty = false;
}
return value;
},
//...
};
return obj;
}
但是仍旧会出现问题:
console.log(sum.value);//3
state.a = 20;//修改了a的值
console.log(sum.value);//打印出来依旧是3
原因就是我们在执行完value = effectFn();
后将dirty
设置为了false
,我们只需要在effect函数的scheduler
的配置项中将dirty修改为true即可
// 建立响应式数据和computed之间的依赖关系
const effectFn = effect(getter, {
lazy: true,
//scheduler是在实现effect函数的时候预留的配置项,作用是将要执行的依赖函数作为参数返回给用户,让用户自行处理,而不自己再自动执行trigger
scheduler() {
dirty = true;//+++
},
});
此时结果就正常了。
2. 处理渲染函数的依赖
目前为止,我们的计算属性工作一切正常,但是这种情况,某一个函数依赖计算属性的值,例如渲染函数。那么此时计算属性值的变化,应该也会让渲染函数重新执行才对。例如:
const state = reactive({
a: 1,
b: 2,
});
const sum = computed(() => {
console.log("computed");
return state.a + state.b;
});
effect(() => {
// 假设这个是渲染函数,依赖了 sum 这个计算属性
console.log("render", sum.value);
});
state.a++//当响应式数据修改了,依赖computed的渲染函数也应该重新执行
执行结果如下:
computed
render 3
computed
可以看到 computed 倒是重新执行了,但是渲染函数并没有重新执行。
怎么办呢?很简单,内部让渲染函数和计算属性的值建立依赖关系即可。
const obj = {
// 外部获取计算属性的值
get value() {
//+++ 相当于计算属性的 value 值和渲染函数之间建立了联系
track(obj, TrackOpTypes.GET, "value");
// ...
},
// ...
};
return obj;
首先在获取依赖属性的值的时候,我们进行依次依赖收集,这样因为渲染函数里面用到了计算属性,因此计算属性 value 值就会和渲染函数产生依赖关系。
const effetcFn = effect(getter, {
lazy: true,
scheduler() {
dirty = true;
//+++ 派发更新,执行和 value 相关的函数,也就是渲染函数。
trigger(obj, TriggerOpTypes.SET, "value");
},
});
接下来添加配置项 scheduler,之后无论是 state.a 的变化,还是 state.b 的变化,都会进入到 scheduler。进入scheduler就意味着响应式数据变化了,派发更新重新执行记录的依赖函数。而在 scheduler 中,手动派发和 value 相关的更新即可。
此时,依赖cpmputed的渲染函数也会进行更新了。
完整代码
// 实现computed函数
import { effect } from "../../effect/effect.js";
import track from "../../effect/track.js";
import trigger from "../../effect/trigger.js";
import { TriggerOpTypes, TrackOpTypes } from "../../utils/index.js";
/**
* 参数归一化
* @param param
* @returns {object} 归一化后的参数对象
*/
function normalizeParam(param) {
/* 思路:用户传递的可能是配置对象或者一个getter函数
我们需要统一一下用户的参数格式
*/
let getter, setter;
if (typeof param === "function") {
getter = param;
setter = () => {
console.warn("computed setter is not defined");
};
} else if (typeof param === "object") {
getter = param.get;
setter = param.set;
}
return { getter, setter };
}
/**
* 计算属性computed API
* @param getterOrOptions getter函数或者配置对象
* @returns {object} 计算属性结果 {value}
*/
export default function computed(getterOrOptions) {
// 先进行参数归一化
const { getter, setter } = normalizeParam(getterOrOptions);
let value;
let dirty = true; //脏数据,用来判断是否重新计算
// 建立响应式数据和computed之间的依赖关系
const effectFn = effect(getter, {
lazy: true,
scheduler() {
dirty = true;
trigger(obj, TriggerOpTypes.SET, "value");
},
});
//返回对象
const obj = {
get value() {
track(obj, TrackOpTypes.GET, "value");
// 设置计算属性缓存
if (dirty) {
value = effectFn();
dirty = false;
}
return value;
},
set value(newVal) {
setter(newVal);
},
};
return obj;
}
-EOF-