第三章、computed设计思路--(读书笔记)

183 阅读6分钟

vuejs中有很井段的computed、watch用法,那么具体设计思路是什么,今天我们继续探索vuejs一书,看看书中会有什么答案,题外话,本系列属于读书自我感受心得,建议看完前面的部分在进入本期的读书,有助于降低理解难度

1、简易版的computed

  在前面的章节我们实现了一个‘toy’级别的响应式系统,那么基于这个响应式系统,我们如何实现一个cumputed计算属性;
首先我们调整一下effect副作用函数,让其不在第一时间触发,方便我们后续处理

function effect(fn, option = {}) {
  const effectFn = () => {
    clearEffectDeps(effectFn);
    activeEffect = effectFn;
    // 将当前副作用effectFn放入effectStack中
    effectStack.push(effectFn);
    // 执行副租用函数
    const res = fn()
    // 出栈
    effectStack.pop();
    // 将activeEffect指向上一个effect
    activeEffect = effectStack[effectStack.length - 1];
    return res
  }
  effectFn.deps = []
  // 增加调度任务模块
  effectFn.option = option;
  if(!option.lazy) {
    effectFn()
  }
  return effectFn
}

  这样当我们配置lazy = true,我们可以在effect首次执行时候就不会触发副作用函数,具体如下:

const effectFn = effect(() => {
  return obj.a + obj.b
}, {
  lazy: false
})

effectFn()

  那么接下来我们实现一下一个简易版的computed计算属性,基本思路就是,生成一个副作用函数,当我们在获取值的时候触发并执行拿到结果,代码如下:

const data = {bar: 1, foo: 1}

const obj = new Proxy(data, {/* 参考之前的代码 */})

function computed(getter) {
  const effectFn = effect(getter, {
    // 保证副作用不会第一时间被执行
    lazy: true
  })
  const obj = {
    // 获取值触发
    get value() {
      return effectFn();
    }
  }
  return obj
}

const numbers = computed(() => {
  return obj.bar + obj.foo
})

console.log(numbers.value) // 2
console.log(numbers.value) // 2

  上面基本能达到我们想要的computed计算,但是有一点,我们多次获取值,会发现computed会进行多次计算,用过vue的同学都知道vue中computed会缓存值,原始关联的数据没有发生变化,computed不会计算,只能拿缓存的值去使用,所以我们还需要实现缓存值的功能;

2、带有值缓存版本的computed

  那么我们分析一下该如何实现值的缓存呢,很简单我们可以分析出来两个方向:

  • 1、我们需要第一个变量,保存上次请求的值(value
  • 2、我们需要一个标志位(dirty)来去判定是否启用缓存值:

  接下来,我们是实现一个基础缓存版本的computed,代码如下:

function computed(getter) {
  // 保存值
  let value;
  // 是否启用缓存
  let dirty = true;
  const effectFn = effect(getter, {
    // 保证副作用不会第一时间被执行
    lazy: true
  })
  const obj = {
    get value() {
      if (dirty) {
        value = effectFn();
        dirty = false;
      }
      return value
    }
  }
  return obj
}

  调整后的computed看似可以满足我们只计算一次,缓存值的诉求,但是仔细一思考,当我们obj中的值发生变化,computed也不会重新计算新值给我们,这是一个致命的缺陷,所以我们应当保证缓存值的情况下,也不能够失去对于依赖值发生变化的相应。

  如此,思路就来了,我们就需要让dirty能够调度执行修改其值,这就用到了我们上一讲提到的scheduler(调度器),具体如下可以实现一个简单的调度器,帮助我们处理computed多次重复计算的问题:

 // computed
 function computed(getter) {
   // 保存值
   let value;
   // 是否启用缓存
   let dirty = true;

   const effectFn = effect(getter, {
     // 保证副作用不会第一时间被执行
     lazy: true,
     scheduler() {
       dirty = true
     }
   })
   const obj = {
     get value() {
       console.log('get value', dirty)
       if (dirty) {
         value = effectFn();
         dirty = false;
       }
       return value
     }
   }
   return obj
 }

3、解决effect副作用中不生效问题

  上面实现的computed基本可以满足值缓存,依赖值变化,触发调度器调整值,但是当我们在effect中调用计算属性值会,我们发现他并不会随着依赖数据变化而变化,具体一下:

const data = {bar: 1, foo: 1}
const obj = new Proxy(data, {/* 参考之前的代码 */}) 

const sumRes = computed(() => {
   return obj.bar + obj.foo
 })

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

 obj.bar++;
// 2

理论上我们需要的是3,实际上我们只得到了2;那么我们分析一下为什么不会更新值:

  • 1、sumRes生成一个computed计算属性,遇到第一个effect执行console.log(sumRes.value),计算值,并且将value值缓存起来,当然这块在细拆分就是effect(() => { effect(...) })嵌套结构;
  • 2、当我们执行obj.bar++ 他只会执行 bar这个key值对应的副作用函数,也就是外层的effect副作用函数,但是当执行到sumRes.value时,由于第一次执行我们将dirty设置为false,本次没有触发computed内部的effectFn,这样就不发执行调度器scheduler,所以此处仍然拿到的是缓存的value值,所以是2;

  针对上面的问题,我们应该如何改进?其实思路也很简单,既然原因是无法触发computed中的effectFn,那么我们是不是让obj也能通过tack和trigger方式进行监听,触发值调度器等相关操作,那么我们就对computed进行改写:具体如下:

function computed(getter) {
  // 保存值
  let value;
  // 是否启用缓存
  let dirty = true;

  const effectFn = effect(getter, {
    // 保证副作用不会第一时间被执行
    lazy: true,
    scheduler() {
      if(!dirty) {
        dirty = true;
        trigger(obj, 'value');
      }
    }
  })
  const obj = {
    get value() {
      console.log('get value', dirty)
      if (dirty) {
        value = effectFn();
        dirty = false;
      }
      tack(obj, 'value');
      return value
    }
  }
  return obj
}

  这样我们就完成了一个穷人版的computed,后面附上完整代码:

// 创建调度任务容器
let workers = new Set();
// 创建一个微任务队列
const p = Promise.resolve();
// 是否标识正在刷新队列
let isFlushing = false;
// 副作用函数
let activeEffect;
let effectStack = [];

// 刷新队列
function flushWorkers() {
  if (isFlushing) return
  isFlushing = true
  p.then(() => {
    console.log(workers, 'workers')
    workers.forEach(w => w())
  }).finally(() => {
    isFlushing = false
  })
}

function effect(fn, option = {}) {
  const effectFn = () => {
    clearEffectDeps(effectFn);
    activeEffect = effectFn;
    // 将当前副作用effectFn放入effectStack中
    effectStack.push(effectFn);
    // 执行副作用函数,拿到返回值
    const res = fn()
    // 出栈
    effectStack.pop();
    // 将activeEffect指向上一个effect
    activeEffect = effectStack[effectStack.length - 1];
    // 将发回值作为结果抛出去
    return res
  }
  effectFn.deps = [];
  // 增加调度任务模块
  effectFn.option = option;
  if(!option.lazy) {
    effectFn()
  }
  return effectFn
}
// 每次清除副作用函数列表里面的关联关系
function clearEffectDeps(effectFn) {
  effectFn.deps.forEach(i => {
    i.delete(effectFn)
  })
  effectFn.deps.length = 0
}
// 数据准备
const data = { bar: 1, foo: 1 };
// 响应式函数容器
const bucket = new WeakMap();
const obj = new Proxy(data, {
  get(target, key) {
    tack(target, key);
    return target[key];
  },
  set(target, key, value) {
    target[key] = value;
    trigger(target, key);
    return true;
  }
});
// 向bucket里面注入副作用函数
function tack(target, key) {
  // 没有acticeEffect
  if (!activeEffect) {
    return;
  }
  // 判断下面有没有对应的对象相关的内容
  let depsMap = bucket.get(target);
  if (!depsMap) {
    depsMap = new Map();
    bucket.set(target, depsMap);
  }
  // 对应key值的内容
  let deps = depsMap.get(key);
  if (!deps) {
    deps = new Set();
    depsMap.set(key, deps);
  }
  deps.add(activeEffect);
  // 将对应key值的副作用函数相关信息放入副作用函数
  activeEffect.deps.push(deps);
}
// 副作用触发函数
function trigger(target, key) {
  let depsMap = bucket.get(target);
  if (!depsMap) {
    return;
  }
  let deps = depsMap.get(key);
  const effectsRun = new Set();
  deps && deps.forEach(i => {
    if (i !== activeEffect) {
      effectsRun.add(i);
    }
  })
  effectsRun.forEach(fn => {
    // 执行前,判断是否有调度器
    if (fn.option.scheduler) {
      fn.option.scheduler(fn);
    } else {
      fn();
    }
  });
}
// computed
function computed(getter) {
  // 保存值
  let value;
  // 是否启用缓存
  let dirty = true;

  const effectFn = effect(getter, {
    // 保证副作用不会第一时间被执行
    lazy: true,
    scheduler() {
      if(!dirty) {
        dirty = true;
        trigger(obj, 'value');
      }
    }
  })
  const obj = {
    get value() {
      console.log('get value', dirty)
      if (dirty) {
        value = effectFn();
        dirty = false;
      }
      tack(obj, 'value');
      return value
    }
  }
  return obj
}

const sumRes = computed(() => {
  return obj.bar + obj.foo
})

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

obj.bar++;

最后希望大家多多支持,我会努力笔耕不辍,更新更多的前端知识给大家~~