深圳幻海软件技术有限公司 欢迎您!

Vue.js设计与实现之六-computed计算属性的实现

2023-02-28

1、写在前面在前面文章介绍了effect的实现,可以用于注册副作用函数,同时允许一些选项参数options,可以指定调度器去控制副作用函数的执行时机和次数等。还有用于追踪和收集依赖的track函数,以及用于触发副作用函数重新执行的trigger函数,结合这些我们可以实现一个计算属性--compute

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
})
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

就这样,通过设置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
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

在上面代码片段中,在effect函数中先判断了是否需要懒执行,对此会判断options.lazy的值为true时,则将effectFn副作用函数作为参数返回到effect。这样,用户在调用执行effect函数时,可以通过返回值去拿到对应的effectFn函数,这样可以手动执行该函数。

const effectFn = effect(()=>{
  console.log(state.name);
},{
  //指定lazy选项,这样函数不会立即执行
  lazy: true
});
//手动执行副作用函数
effectFn();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

但是仅仅实现手动执行副作用函数,对于我们的使用意义并不大,如果将返回到effect的副作用函数作为getter,那么通过这个取值函数就能获取返回任何值。

const effectFn = effect(
()=>state.name + state.age,
{
  //指定lazy选项,这样函数不会立即执行
  lazy: true
});

//手动执行副作用函数,可以获取到返回的值
const value = effectFn();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

这样就可以实现在调用的时候,手动执行获取到各种想要得到的值。在effect函数内部只需要做出些改变,只需要在执行副作用函数时将副作用的值返回即可:

// effect用于注册副作用函数
function effect(fn,options={}){
    const effectFn = ()=>{
        // 调用函数完成清理遗留副作用函数
        cleanupEffect(effectFn)
        // 当调用effect注册副作用函数时,将副作用函数fn赋值给activeEffect
        activeEffect = effectFn;
        // 在副作用函数执行前压栈
        effectStack.push(effectFn)
        // 执行副作用函数,将执行结果存储到resconst 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
    
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

现在,我们已经实现了能够进行懒执行的副作用函数,能够拿到执行返回的结果,做后续的处理。

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;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

在上面代码中,只是粗略做了懒计算处理,只有在真正对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);
  • 1.
  • 2.
  • 3.
  • 4.

运行结果:

之所以发生这种情况,多次读取sumRes.value的值时,每次访问都会重新调用effectFn重新计算。

带有缓存的computed

为了解决前面获取不到上次计算值的问题,需要在实现computed函数时,添加对计算值的缓存操作。其实实现很简单,就是添加两个变量value和dirty,value用于缓存上次计算的值,dirty则标识是否需要重新计算。

function computed(getter){
  let value;
  let dirty = true;
  const effectFn = effect(
  getter,
  {
    //指定lazy选项,这样函数不会立即执行
    lazy: true,
    //在调度器重置dirtytrue
    scheduler(){
      dirty = true
    }
  });
  const state = {
    //当对value进行读取操作时,执行effectFn并将结果进行返回
    get value(){
      //只有当dirty标识为true值时,才会将计算值进行缓存,下一次访问直接使用缓存的值
      if(dirty){
        value = effectFn();
        dirty = false
      }
      return value
    }
  }
  return state;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

在上面代码中,初始化设置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);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

执行结果为:

但是,在当前设计的计算属性在另一个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,
        //在调度器重置dirtytrue
        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;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.

写一段简单的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);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

执行结果:

根据上面的实现demo可以分析出对应的计算属性的响应联系图:

计算属性的响应联系

4、写在最后

计算属性computed其实是一个懒执行的副作用函数,可以通过lazy选项使得副作用函数可以懒执行,被标记为懒执行的副作用函数可以通过手动执行。在读取计算属性的值时,可以手动执行副作用函数,在依赖的响应式数据发生变化时,通过scheduler将dirty标记设置为true,即为脏数据,在下次读取计算属性的值,就会重新计算得到真正的值。