实现Vue3的Computed(附源码)
想要了解八股文中Computed
和Watch
的区别?
分别实现一下Computed
和Watch
就知道了
一、回顾
- 在第一篇的时候我们主要是明确了数据结构,用
WeakMap
来存放全局被代理的对象,用Map
来存放对象每一个属性的副作用函数集合,用Set
来存放副作用集合 - 第二篇我们主要是处理了可能会出现的例如
effect
嵌套调用,以及同一个effect
函数内出现先获取后赋值的操作导致的递归的情况,最后还扩展了调度函数 - 调度函数是作为
options
的属性从而作为effect
函数的参数
effect(fn,options = {})
二、分析实现
- 要实现
Computed
,首先你要了解它有什么要的功能:Computed
是基于它的依赖缓存,只有相关依赖发生改变时才会重新取值 - 那么我们就是需要去实现它,就需要将这个过程就一定会有
- 暂缓执行(依赖没改变的时候不执行)
- 执行(依赖变化则执行)
三、暂缓执行
👉暂缓
- 我们上一篇说可以传入
options
去控制它执行过程中的操作,那么我们就要利用起来 - 可以选择传入
lazy: true
,表示该函数暂缓执行 - 那么我们就去
effect
函数中找到执行的地方,给它加上一个判断: 如果lazy
为true
的话就不执行它
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
,在里面设置获取obj
的value
的时候,再去执行这个被暂缓的函数
//传入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;
});
- 我们传入一个函数,里面使用了被代理的对象的
foo
和bar
,然后 立即打印getSum
和 时隔2分钟后打印getSum.value
console.log(getSum)
setTimeout(()=>{
console.log(getSum.value)
},2000)
- 可以看到暂缓执行的效果已经达成
- 上述的操作只是实现了在我们想要它执行的地方执行而已,而不是因为依赖改变而执行
- 要因依赖改变而执行,肯定需要加一个判断,在依赖改变的时候调用,在没改变的时候使用缓存
四、依赖改变
👉缓存值
- 一般我们要缓存值的话,就需要有一个标识符来标识当前是否需要更新,有一个字段用来存储缓存值,
- 这里我们设置了
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.foo
和obj.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)
- 打印结果
五、图示总结
computed
的结果依赖于proxy
对象的值- 当
proxy
改变的时候会触发调度函数,将isNeedChange
修改为true
- 当
isNeedChange
为true
的时候,会执行包装过传入Computed
中的函数的effectFn
,执行完将结果赋值为缓存value
,重新将isNeedChange
修改为false
- 当
isNeedChange
为false
的时候,就直接使用缓存value
- 返回缓存
value
六、实现代码
- 已在本文中实现的代码打上了注释,可以自己测试数据
七、源码
computed
源码地址:core/computed.ts at main · vuejs/core · GitHub- 这里主要是对传入的参数进行判断格式化,然后生成
ComputedRefImpl
实例,并返回
- 这里主要是对传入的参数进行判断格式化,然后生成
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
}
ComputedRefImpl
源码地址: core/computed.ts at main · vuejs/core · GitHub- 这里的思路跟我们实现的思路其实就差不多了
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的实现明天再继续撒🤡
转载自:https://juejin.cn/post/7150163859062554631