likes
comments
collection
share

实现Vue3的Computed(附源码)

作者站长头像
站长
· 阅读数 19

想要了解八股文中ComputedWatch的区别? 分别实现一下ComputedWatch就知道了

一、回顾

  • 在第一篇的时候我们主要是明确了数据结构,用WeakMap来存放全局被代理的对象,用Map来存放对象每一个属性的副作用函数集合,用Set来存放副作用集合 实现Vue3的Computed(附源码)
  • 第二篇我们主要是处理了可能会出现的例如effect嵌套调用,以及同一个effect函数内出现先获取后赋值的操作导致的递归的情况,最后还扩展了调度函数
  • 调度函数是作为options的属性从而作为effect函数的参数
effect(fn,options = {})

二、分析实现

  • 要实现Computed,首先你要了解它有什么要的功能: Computed是基于它的依赖缓存,只有相关依赖发生改变时才会重新取值
  • 那么我们就是需要去实现它,就需要将这个过程就一定会有
    • 暂缓执行(依赖没改变的时候不执行)
    • 执行(依赖变化则执行)

三、暂缓执行

👉暂缓

  • 我们上一篇说可以传入options去控制它执行过程中的操作,那么我们就要利用起来
  • 可以选择传入lazy: true,表示该函数暂缓执行
  • 那么我们就去effect函数中找到执行的地方,给它加上一个判断: 如果lazytrue的话就不执行它
function effect(fn, options = {} ) {
     ...............
  effectFn.options = options;
  //加上判断,如果lazy为true就不执行
  if(!options.lazy)  {
    effectFn();
  }
}
  • 解决了这个问题之后我们就紧接着面临一个新的问题: 既然把现在不执行它,那么什么时候,怎么才能去执行它
  • 要解决这个问题首先我们要拿到对应的副函数,才能在我们想要调用它的地方去调用它;于是我们给effect函数加上返回值
function effect(fn, options = {} ) {
     ...............
  if(!options.lazy)  {
    effectFn();
  }
  //将该副作用函数effectFn返回
  return effectFn; 
}
  • 但是effectFn其实是包装后的副作用函数,我们主要本意是通过effectFn得到真正的副作用函数fn的执行结果,所以我们需要在effectFn内部更改一下,让它能够返回真正副作用函数fn函数的执行结果
  const effectFn = () => {
    cleanup(effectFn);
    activeEffect = effectFn;
    effectStack.push(effectFn);
    const res = fn();  //用res存储fn()函数调用的结果
    effectStack.pop();
    activeEffect = effectStack[effectStack.length-1];
    return res //返回该结果
  }

处理完能够暂缓执行后其实我们就可以来写能够实现暂缓执行Computed函数了

👉执行

  • 将需要暂缓执行的函数传入computed函数中,然后作为effect的副作用函数,传入配置lazy:true,在里面设置获取objvalue的时候,再去执行这个被暂缓的函数
//传入getters函数
function computed(getter) {
  const effectFn = effect(getter, {
    lazy:true
  })
  const obj = {
    //当你获取value的时候,才会去计算
    get value() {
      return effectFn()
    }
  }
  return obj
}

👉测试

  • 为了测试是否获取value才进行执行,我在传入的函数中间加了一下打印
const getSum = computed(()=> {
  console.log('该函数被执行')
  return proxy.foo + proxy.bar;
});
  • 我们传入一个函数,里面使用了被代理的对象foobar,然后 立即打印getSum时隔2分钟后打印getSum.value
console.log(getSum)
setTimeout(()=>{
  console.log(getSum.value)
},2000)
  • 可以看到暂缓执行的效果已经达成 实现Vue3的Computed(附源码)
  • 上述的操作只是实现了在我们想要它执行的地方执行而已而不是因为依赖改变而执行
  • 要因依赖改变而执行,肯定需要加一个判断,在依赖改变的时候调用,在没改变的时候使用缓存

四、依赖改变

👉缓存值

  • 一般我们要缓存值的话,就需要有一个标识符来标识当前是否需要更新,有一个字段用来存储缓存值,
  • 这里我们设置了value来存储值,设置了isNeedChange来判断
function computed(getter) {
  let value; //存储被缓存的值
  let isNeedChange = true; //标识符用来判断
  const effectFn = effect(getter, {
    lazy:true
  })
  const obj = {
    get value() {
      if(isNeedChange) {
        //将值缓存
        value =  effectFn();
        isNeedChange = false
      } 
      return value 
    } 
  }
  return obj
}

👉依赖修改

  • 这里我们只解决了如何缓存,但是没有解决依赖改变时重新触发问题
  • 也就是说, 如果该函数依赖的值变化了,我需要重新执行去获取而不是继续使用缓存
  • 也就是说,当obj.foo或者obj.bar的值发生变化了,我需要将isNeedLength置为true
  • 这个时候就用到了我们上一篇讲的调度函数,obj.fooobj.bar改变的时候,会调用trigger函数,发现有scheduler函数,则调用scheduler函数将isNeedChange修改为true
  const effectFn = effect(getter, {
    lazy:true,
    scheduler() {
      isNeedChange = true;
    }
  })

👉测试

  • 传入的函数和之前一样
const getSum = computed(()=> {
  console.log('该函数被执行')
  return proxy.foo + proxy.bar;
});
  • 这里对他进行了多次打印
console.log('----这里是第一次获取,所以会执行----');
console.log(getSum.value);
console.log('----使用缓存值-------------')
console.log(getSum.value);
console.log(getSum.value);
console.log('---修改proxy.foo----')
proxy.foo = 10;
console.log('---发现重新执行函数----')
console.log(getSum.value)
  • 打印结果 实现Vue3的Computed(附源码)

五、图示总结

  • computed的结果依赖于proxy对象的值
  • proxy改变的时候会触发调度函数,将isNeedChange修改为true
  • isNeedChangetrue的时候,会执行包装过传入Computed中的函数的effectFn,执行完将结果赋值为缓存value,重新将isNeedChange修改为false
  • isNeedChangefalse的时候,就直接使用缓存value
  • 返回缓存value 实现Vue3的Computed(附源码)

六、实现代码

  • 已在本文中实现的代码打上了注释,可以自己测试数据

七、源码

export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
//因为computed可以传入函数和对象两种,所以这里做一下判断将它格式化
  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
 //生成了ComputedRefImpl的一个实例,下面附有源码
  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
    .....
 //并将他返回
  return cRef as any
}                                                                
export class ComputedRefImpl<T> {
    ......
  constructor(
    getter: ComputedGetter<T>,
    .....
  ) {
  //创建effect
    this.effect = new ReactiveEffect(getter, () => {
    //调度器,在依赖的数据改变的时候则会执行,就是我们上述说的scheduler
    //这里如果dirty为false的时候,会把它修改为true,使其能够重新调用effect.run()
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    this.effect.computed = this
    this.effect.active = this._cacheable = !isSSR
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }
  //在读取value的时候才会执行
  get value() {
    const self = toRaw(this)
    trackRefValue(self)
    //dirty,跟我们上述实现的isNeedChange发挥的作用是一样的
    if (self._dirty || !self._cacheable) {
    //如果dirty为true, 则需要重新执行函数,并将dirty置为false
      self._dirty = false
      //value则用来存放缓存数据
      self._value = self.effect.run()!
    }
    //返回value
    return self._value
  }
  ......
}

Watch的实现明天再继续撒🤡