Vue.js---计算属性computed和lazy

4.6 计算属性computed和lazy

懒执行的effect:一般的effect一下子就执行了,但是懒加载effect是等需要的时候才会执行

这时我们通过在options中添加lazy属性来达到目的

  function effect (fn , options = {}) {
    const effectFn = () => {
    // 调用clearup函数完成清除工作
    clearUp(effectFn);
    activeEffect = effectFn; 
    // 在调用副作用函数之前将副作用函数压入栈中
    effectStack.push(effectFn);
    fn();
      // 执行完之后抛出,把activeEffect还原成原来的值
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
    }
    // 将options挂在到fn上
    effectFn.options = options;
    // deps用来存储所有与该副作用函数相关联的依赖集合
    effectFn.deps = [];
    // 只有非lazy的时候才会执行副作用函数
    if(!options.lazy){
         effectFn(); 
    }
    return effectFn
  }

那什么时候才执行这个函数呢?

因为我们前面返回了effectFn作为effect函数的返回值,那我们就可以手动调用该副作用函数了

 const effectFn =  effect(
      () => {
      console.log(obj.foo);
    } ,
    // 添加lazy
           {
     lazy : true
    })
    // 手动执行副作用函数
    effectFn();
    obj.foo++;
    obj.foo++;

现在我们要实现假设传递给effect函数的是getter函数,可以返回getter函数的值

  const effectFn =  effect(
      () => {
      obj.foo + obj.bar
    } ,
    // 添加lazy
           {
     lazy : true
    })
    // 手动执行副作用函数
    const value = effectFn();
    console.log(value);

我们希望输出的是 obj.foo + obj.bar的值

实现computed函数

    // computed函数
    // 接收getter作为参数
    function computed (getter) {
      // 将gettwe作为副作用函数,创建一个lazy effect
      const effectFn = effect( getter, {
        lazy : true
      })
    const obj = {
      // 对象的value是一个访问器属性
      // 只有当读取value的值时,才会执行effctFn并将其作为结果返回
      get value(){
        return effectFn()
      }
    }
      // 返回一个对象
      return obj;
    }
    // 开始使用计算属性
     const sumRes = computed(() => obj.foo + obj.bar)
     console.log(sumRes);//3
    

但是有一个bug。多次访问会导致effectFn多次计算

解决—对值进行缓存

    function computed (getter) {
      // value用来缓存
      let value
      // dirty标志,标识是否需要重新计算
      let dirty = true
      // 将gettwe作为副作用函数,创建一个lazy effect
      const effectFn = effect( getter, {
        lazy : true
      })
    const obj = {
      // 对象的value是一个访问器属性
      // 只有当读取value的值时,才会执行effctFn并将其作为结果返回
      get value(){
        // 只有true时才可以进行计算
        if(dirty){
          value =  effectFn()
          // 将dirty设置为false,下一次直接使用缓存到value当中的值
          dirty = false;
        }
        return value
      }
    }
      // 返回一个对象
      return obj;
    }

无论我们执行了多少次sumRes都不会重新执行啦,直接取value里面的值

但是我们如果修改obj.foo的值,我们发现并没有响应的修改最后的sumRes

  get value(){
        // 只有true时才可以进行计算
        if(dirty){
          value =  effectFn()
          // 将dirty设置为false,下一次直接使用缓存到value当中的值
          dirty = false;
        }
        return value
      }

dirty = false;这便是原因

解决:当值发生变化的时候,改变dirty的值就可以啦,这时我们就要使用scheduler选项

    // computed函数
    // 接收getter作为参数
    function computed (getter) {
      // value用来缓存
      let value
      // dirty标志,标识是否需要重新计算
      let dirty = true
      // 将gettwe作为副作用函数,创建一个lazy effect
      const effectFn = effect( getter, {
        lazy : true,
        scheduler(){
          dirty = true
        }
      })
    const obj = {
      // 对象的value是一个访问器属性
      // 只有当读取value的值时,才会执行effctFn并将其作为结果返回
      get value(){
        // 只有true时才可以进行计算
        if(dirty){
          value =  effectFn()
          // 将dirty设置为false,下一次直接使用缓存到value当中的值
          dirty = false;
        }
        return value
      }
    }
      // 返回一个对象
      return obj;
    }
    // 开始使用计算属性
     const sumRes = computed(() => obj.foo + obj.bar)
     console.log(sumRes.value);//4
    obj.foo++
        console.log(sumRes.value);//5

我们的计算属性已经趋于完美了,但是还有一个问题

   effect(() => {
      console.log(sumRes.value)
    })
    obj.foo++

我们希望的是在完成 obj.foo++之后是可以重新渲染的,但是我们发现并没有

其实这是一个典型的effect嵌套,一个计算属性的内部有effect,并且它是懒执行的,只有当真正读取计算属性的值才会执行。

  • 例如,假设我们有一个计算属性 fullName,它依赖于响应式数据 firstNamelastName。只有当我们读取 fullName 的值时,才会执行计算 fullNameeffect

但是getter里面访问的响应式数据只会把computed内部的effect收集为依赖。

而当把计算属性用于另一个effect时,就会发生effect嵌套,外层的effect不会被内层的effect所收集

  • 例如,假设我们有一个 effect 函数 updateUI,它读取 fullName。这时,updateUIeffectfullNameeffect 就会嵌套在一起。

解决—当读取计算属性的值时,我们可以手动调用track函数进行追踪,当计算属性发生变化的时候,手动调用trigger触发响应

  • 当读取计算属性的值时,手动调用 track 函数,将当前 effect(如 updateUI)添加到计算属性的依赖列表中。
  • 这样,当计算属性的依赖发生变化时,可以触发当前 effect
  • 当计算属性的值发生变化时,手动调用 trigger 函数,通知所有依赖该计算属性的 effect 进行更新。
  • 这样,外层的 effect(如 updateUI)会在计算属性(如 fullName)的依赖发生变化时被触发。

完整代码:


  <script setup>
  let activeEffect;
  // effect栈
  const effectStack = [];
  function effect (fn , options = {}) {
    const effectFn = () => {
    // 调用clearup函数完成清除工作
    clearUp(effectFn);
    activeEffect = effectFn; 
    // 在调用副作用函数之前将副作用函数压入栈中
    effectStack.push(effectFn);
    // 将effect的值存储起来
    const res =  fn();
      // 执行完之后抛出,把activeEffect还原成原来的值
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
      // 返回res
    return res;
    }
    // 将options挂在到fn上
    effectFn.options = options;
    // deps用来存储所有与该副作用函数相关联的依赖集合
    effectFn.deps = [];
    // 只有非lazy的时候才会执行副作用函数
    if(!options.lazy){
         effectFn(); 
    }
    return effectFn
  }
  
  const bucket = new WeakMap();
  const data = { foo : 2 , bar : 2 }; // 确保所有属性都已定义
  const obj = new Proxy(data, {
    get(target, key){
      track(target , key);
      return target[key];
    },
    set(target, key, newVal){
      target[key] = newVal;
      trigger(target , key , newVal);
    }
  });
  // 追踪变化
  function track(target , key){
    if(!activeEffect){
        return;
      }
      // 根据tartget取来的depsMap,它是一个map类型
      let depsMap = bucket.get(target);
      // 如果不存在
      if(!depsMap){
        // 创建一个
        bucket.set(target, (depsMap = new Map()));
      }
      // 根据key取来的deps,它是一个set类型
      let deps = depsMap.get(key);
      // 如果不存在
      if(!deps){
        // 创建一个
        depsMap.set(key, (deps = new Set()));
      }
      deps.add(activeEffect); // 添加当前活跃的副作用函数
      activeEffect.deps.push(deps);
  }
// 触发变化:处理调度逻辑
  function trigger(target, key, newVal) {
    const depsMap = bucket.get(target);
    if (!depsMap) {
      return;
    }
    const effects = depsMap.get(key);
        // 开始执行
    const effectsToRun = new Set(effects);
    effects && effects.forEach(effectFn => {
      if(activeEffect !== effectFn){
        effectsToRun.add(effectFn);
      }
    });
    effectsToRun.forEach(effectFn =>{
      if(effectFn.options.scheduler){
        effectFn.options.scheduler(effectFn);
      }else {
         effectFn();
      }
    });
    // effects && effects.forEach(fn => fn()); // 只触发与键相关的副作用函数
  }
  // 清除函数
   function clearUp (effectFn){
     // 遍历然后进行删除
     for(let i = 0 ; i < effectFn.deps.length ; i++){
       const deps = effectFn.deps[i];
       // 移除
       deps.delete(effectFn);
     }
     // 最后重置effectFn.deps数组
     effectFn.deps.length = 0;
   }
    
  //   effect(() => {
  //     console.log(obj.foo);
  //   })
  // obj.foo ++;
  //   obj.foo++;
    // 连续执行同一代码,只饭后最后一次计算的结果
    // 定义一个任务集合set:可以进行去重操作
    const jobQueue = new Set();
    // 使用promise.resolve()创建一个promise实例:将一个任务添加到微任务队列当中
    const p = Promise.resolve();
    // 开始刷新队列
    let isFlushing = false;
    function flushJob(){
      // 如果队列正在被刷新->return
      if(isFlushing){
        return;
      }
        // 没有刷新,进行刷新操作
        isFlushing = true;
        p.then(() => {
          jobQueue.forEach(job => job());
          
        }).finally(() => {
          // 结束后重置
          isFlushing = false;
        })
    }
    // computed函数
    // 接收getter作为参数
    function computed (getter) {
      // value用来缓存
      let value
      // dirty标志,标识是否需要重新计算
      let dirty = true
      // 将gettwe作为副作用函数,创建一个lazy effect
      const effectFn = effect( getter, {
        lazy : true,
        scheduler(){
          // 设置value进行trigger
          trigger(obj , 'value');
          dirty = true
        }
      })
    const obj = {
      // 对象的value是一个访问器属性
      // 只有当读取value的值时,才会执行effctFn并将其作为结果返回
      get value(){
        // 只有true时才可以进行计算
        if(dirty){
          value =  effectFn()
          // 将dirty设置为false,下一次直接使用缓存到value当中的值
          dirty = false;
        }
        // 读取value,进行追踪
        track(obj , 'value')
        return value
      }
    }
      // 返回一个对象
      return obj;
    }
    // 开始使用计算属性
     const sumRes = computed(() => obj.foo + obj.bar)
     console.log(sumRes.value);
     obj.foo++
        console.log(sumRes.value);
    effect(() => {
      console.log(sumRes.value)
    })
    obj.foo

</script>

总结:

computed属性的实现我们首先用到了懒加载effect,需要使用的时候才使用。因为我们前面返回了effectFn作为effect函数的返回值,那我们就可以手动调用该副作用函数了。接着我们实现了computed属性,我们是传入一个getter函数和懒加载属性,为了解决多次访问会导致effectFn多次计算,我们需要缓存value。但是我们如果修改obj.foo的值,我们发现并没有响应的修改最后的sumRes,那是因为dirty并没有在修改值之后被修改为true,所以我们就要使用scheduler选项,在 effectoptions 中添加 scheduler,当计算属性的依赖发生变化时,将 dirty 设为 true,以便下次读取 value 时重新计算。为了解决effect嵌套的问题,我们进行了手动追踪和触发

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值