1、写在前面
在前面文章介绍了effect的实现,可以用于注册副作用函数,同时允许一些选项参数options,可以指定调度器去控制副作用函数的执行时机和次数等。还有用于追踪和收集依赖的track函数,以及用于触发副作用函数重新执行的trigger函数,结合这些我们可以实现一个计算属性--computed。
2、懒执行的effect
在研究计算属性的实现之前,需要先去了解下懒执行的effect(lazy的effect)。在当前设计的effect函数中,它会在调用时立即执行传递过来的副作用函数。但是事实上,希望在某些场景并不希望它立即执行,而是在需要的时候才执行,前面了解到想要改变effect的执行可以在options参数中设置。
const data = {
name:"pingping",
age:18,
flag:true
}
const state = new Proxy(data,{
/*...*/
})
effect(()=>{
console.log(state.name);
},{
//指定lazy选项,这样函数不会立即执行
lazy: true
})
就这样,通过设置options选项,去修改effect函数的实现逻辑,当options.lazy为true时不会立即执行副作用函数:
// effect用于注册副作用函数
function effect(fn,options={}){
const effectFn = ()=>{
// 调用函数完成清理遗留副作用函数
cleanupEffect(effectFn)
// 当调用effect注册副作用函数时,将副作用函数fn赋值给activeEffect
activeEffect = effectFn;
// 在副作用函数执行前压栈
effectStack.push(effectFn)
// 执行副作用函数
fn();
// 执行完毕后出栈
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
// 将options挂载到effectFn函数上
effectFn.options = options
//deps是用于存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [];
// 只有非lazy的时候才执行
if(!options.lazy){
// 执行副作用函数effectFn
effectFn()
}
//否则返回副作用函数
return effectFn
}
在上面代码片段中,在effect函数中先判断了是否需要懒执行,对此会判断options.lazy的值为true时,则将effectFn副作用函数作为参数返回到effect。这样,用户在调用执行effect函数时,可以通过返回值去拿到对应的effectFn函数,这样可以手动执行该函数。
const effectFn = effect(()=>{
console.log(state.name);
},{
//指定lazy选项,这样函数不会立即执行
lazy: true
});
//手动执行副作用函数
effectFn();
但是仅仅实现手动执行副作用函数,对于我们的使用意义并不大,如果将返回到effect的副作用函数作为getter,那么通过这个取值函数就能获取返回任何值。
const effectFn = effect(
()=>state.name + state.age,
{
//指定lazy选项,这样函数不会立即执行
lazy: true
});
//手动执行副作用函数,可以获取到返回的值
const value = effectFn();
这样就可以实现在调用的时候,手动执行获取到各种想要得到的值。在effect函数内部只需要做出些改变,只需要在执行副作用函数时将副作用的值返回即可:
// effect用于注册副作用函数
function effect(fn,options={}){
const effectFn = ()=>{
// 调用函数完成清理遗留副作用函数
cleanupEffect(effectFn)
// 当调用effect注册副作用函数时,将副作用函数fn赋值给activeEffect
activeEffect = effectFn;
// 在副作用函数执行前压栈
effectStack.push(effectFn)
// 执行副作用函数,将执行结果存储到res中
const res = fn();
// 执行完毕后出栈
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
// 将res作为effectFn的返回值
return res
}
// 将options挂载到effectFn函数上
effectFn.options = options
//deps是用于存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [];
// 只有非lazy的时候才执行
if(!options.lazy){
// 执行副作用函数effectFn
effectFn()
}
//否则返回副作用函数
return effectFn
}
现在,我们已经实现了能够进行懒执行的副作用函数,能够拿到执行返回的结果,做后续的处理。
3、computed属性
懒计算的computed属性
其实,基于前面的设计和代码实现,大概有了computed属性函数的实现雏形,就是接收一个getter函数作为副作用函数,用于创建一个懒执行的effect。computed函数的执行会返回包含一个访问器属性的对象,只有在读取value值的时候才会去执行effectFn并返回结果。
function computed(getter){
const effectFn = effect(
getter,
{
//指定lazy选项,这样函数不会立即执行
lazy: true
});
const state = {
//当对value进行读取操作时,执行effectFn并将结果进行返回
get value(){
return effectFn();
}
}
return state;
}
在上面代码中,只是粗略做了懒计算处理,只有在真正对sumRes.value的值进行读取操作时,才会去进行计算并得到值。但是在进行多次读取sumRes.value的值,每次访问计算得到的值都是相同的,并不符合我们需要使用上次计算值的要求。『计算属性需要有缓存机制,这样就可以使用到上次计算的结果。』
const sumRes = computed(()=>state.name + state.age);
console.log("hello", sumRes.value);
console.log("hello", sumRes.value);
console.log("hello", sumRes.value);
运行结果:
之所以发生这种情况,多次读取sumRes.value的值时,每次访问都会重新调用effectFn重新计算。
带有缓存的computed
为了解决前面获取不到上次计算值的问题,需要在实现computed函数时,添加对计算值的缓存操作。其实实现很简单,就是添加两个变量value和dirty,value用于缓存上次计算的值,dirty则标识是否需要重新计算。
function computed(getter){
let value;
let dirty = true;
const effectFn = effect(
getter,
{
//指定lazy选项,这样函数不会立即执行
lazy: true,
//在调度器重置dirty为true
scheduler(){
dirty = true
}
});
const state = {
//当对value进行读取操作时,执行effectFn并将结果进行返回
get value(){
//只有当dirty标识为true值时,才会将计算值进行缓存,下一次访问直接使用缓存的值
if(dirty){
value = effectFn();
dirty = false
}
return value
}
}
return state;
}
在上面代码中,初始化设置dirty为true,这样就会把计算值进行缓存,下次进行同样computed计算操作时,就会直接使用缓存的值,而非每次重新计算。同时,在computed函数的effect中添加scheduler属性,在函数内部将dirty的值重置为true,在下次访问sumRes.value时重新调用effectFn的计算值。
const sumRes = computed(()=>state.name + state.age);
console.log("hello", sumRes.value);
console.log("hello", sumRes.value);
console.log("hello", sumRes.value);
state.age++;
console.log("hello", sumRes.value);
执行结果为:
但是,在当前设计的计算属性在另一个effect函数中读取时,修改响应数据state上的属性值并不会触发副作用函数的重新渲染。其实根本原因就是这里存在一个effect嵌套问题,computed内部是effect函数实现的,而在effect中读取computed的值相当于对effect进行了嵌套,外层的effect不会被内层effect的响应式数据收集。
当然,问题很简单,解决方法同样很简单。只需要在读取计算属性值的时候,手动调用track函数进行追踪,当计算属性依赖的响应式数据发生变化时,手动调用trigger函数触发响应:
function computed(getter){
let value;
let dirty = true;
const effectFn = effect(
getter,
{
//指定lazy选项,这样函数不会立即执行
lazy: true,
//在调度器重置dirty为true
scheduler(){
dirty = true
trigger(state, "value")
}
}
);
const state = {
//当对value进行读取操作时,执行effectFn并将结果进行返回
get value(){
//只有当dirty标识为true值时,才会将计算值进行缓存,下一次访问直接使用缓存的值
if(dirty){
value = effectFn();
dirty = false
}
// 对value进行取值操作时,手动调用track函数进行追踪
track(state, "value")
return value
}
}
return state;
}
写一段简单的demo进行实验:
const sumRes = computed(()=>state.name + state.age);
console.log("hello", sumRes.value);
console.log("hello", sumRes.value);
console.log("hello", sumRes.value);
effect(()=>{
console.log(sumRes.value);
})
state.age++
console.log("hello", sumRes.value);
执行结果:
根据上面的实现demo可以分析出对应的计算属性的响应联系图:
计算属性的响应联系
4、写在最后
计算属性computed其实是一个懒执行的副作用函数,可以通过lazy选项使得副作用函数可以懒执行,被标记为懒执行的副作用函数可以通过手动执行。在读取计算属性的值时,可以手动执行副作用函数,在依赖的响应式数据发生变化时,通过scheduler将dirty标记设置为true,即为脏数据,在下次读取计算属性的值,就会重新计算得到真正的值。