当前位置: 首页 > 科技观察

手写Vue3响应式系统:实现Computed

时间:2023-03-17 20:06:24 科技观察

上一篇我们实现了一个基本的响应式系统,本文继续实现computed。首先我们简单回顾一下:响应式系统的核心是一个WeakMap---Map---Set数据结构。WeakMap的key是原始对象,value是响应式Map。这样,当对象被销毁时,对应的Map也会被销毁。Map的key是对象的每一个属性,value是一组依赖于对象属性的效果函数。然后使用Proxy代理对象的get方法将依赖对象属性的效果函数收集到key对应的Set中。还需要代理对象的set方法,在修改对象属性时调用key的所有效果函数。上一篇我们按照这个思路实现了一个比较完整的响应式系统,然后今天继续实现computed。computed的实现,首先我们重构之前的代码,将依赖收集和trigger依赖函数的执行分离成track和trigger函数:逻辑上依然是给对应的Set添加effect,触发effect函数的执行在对应的Set,但是拉出来就清楚多了。然后继续实施计算。computed的使用大概是这样的:constvalue=computed(()=>{returnobj.a+obj.b;});比较效果:effect(()=>{console.log(obj.a);});区别只是多了一个返回值。所以我们实现基于效果的计算,如下所示:functioncomputed(fn){constvalue=effect(fn);returnvalue}当然,当前的effect是没有返回值的,所以需要加上:只要执行effect函数之前记录返回值并返回的基础上,这个改造还是很容易的。现在computed可以返回计算出的值:但是现在当数据发生变化时,所有的effect都会被执行,像computed这样的effect不需要每次都重新执行,只需要在数据发生变化后执行即可。所以我们加了一个lazy选项,控制效果不是立即执行,而是返回函数让用户自己执行。然后在computed中使用effect的时候,添加一个lazy选项,这样effect函数就不会执行,而是返回。在computed中创建一个对象,在触发value的get的时候调用这个函数获取最新的值:测试一下:可以看到computed的返回值的value属性可以获取到计算出的值,obj已经修改的。A。之后会重新执行计算函数,再次取值时可以得到新的值。它只是多执行一次计算,因为当obj.a发生变化时,所有的效果函数都会被执行:这样每次数据变化时,都会重新执行计算函数,计算出最新的值。这个不是必须的,效果函数是否执行应该是可控的。所以我们需要给它添加一个调度函数:它可以支持传入schduler回调函数,然后在执行effect的时候,如果有scheduler,就传给它,让用户自己调度,否则效果函数将被执行。这样用户就可以自己控制effect函数的执行了:那么试试刚才的代码:可以看到obj之后并没有执行effect函数重新计算。这样可以避免在数据改变后立即执行计算函数,并允许您自己控制执行。现在还有一个问题,每次访问res.value,都需要计算一下:能不能加个缓存?只有当数据发生变化时才需要计算,否则直接取之前计算的值。当然可以,只是加个标记:调用scheduler的时候,说明数据发生了变化。这个时候dirty设置为true,然后取值的时候重新计算,然后改成false。下次取值时,直接取计算值。我们来测试一下:当我们访问计算值的value属性时,第一次会重新计算,后面直接取计算值。修改了它依赖的数据后,再次访问value属性会重新计算,后面再访问会直接重新取计算出来的值。至此,我们就完成了computed的功能。但是目前的computed实现还是有问题的,比如这段代码:letres=computed(()=>{returnobj.a+obj.b;});effect(()=>{console.日志(资源。值);});我们在一个效果函数中使用计算值,按理说如果obj.a改变了,计算值也会改变,所有的效果函数都应该被触发。但它没有:为什么?这是因为返回的计算值不是响应式对象,需要做成响应式的,即track在get的时候收集依赖,setting的时候触发依赖的执行:再试一次:现在计算值改变了效果这取决于它可以被触发。至此,我们的计算就完成了。完整代码如下:constdata={a:1,b:2}letactiveEffectconsteffectStack=[];functioneffect(fn,options={}){consteffectFn=()=>{cleanup(effectFn)activeEffect=effectFneffectStack.push(effectFn);constres=fn()effectStack.pop()activeEffect=effectStack[effectStack.length-1]returnres}effectFn.deps=[]effectFn.options=选项;if(!options.lazy){effectFn()}returneffectFn}functioncomputed(fn){letvalueletdirty=trueconsteffectFn=effect(fn,{lazy:true,scheduler(fn){if(!dirty){dirty=truetrigger(obj,'value');}}});constobj={getvalue(){if(dirty){value=effectFn()dirty=false}track(obj,'value');控制台日志(对象);返回值}}返回对象}functioncleanup(effectFn){for(leti=0;i{if(effectFn.options.scheduler){effectFn.options.scheduler(effectFn)}else{effectFn()}})}总结上一篇文章,我们实现了一个响应式的核心数据结构,依赖集合,数据更改后通知相关函数执行今天我们在此基础上实现了计算。我们修改了effect函数,让它返回传入的fn,然后在computed中执行得到计算值。我们还支持lazy和scheduler选项,lazy是让effect不立即执行传入的函数,scheduler是当数据变化触发依赖执行时回调sheduler进行调度。我们通过标记是否脏来实现缓存。当sheduler被执行时,就意味着数据已经改变了。将dirty设置为true并重新计算computed的值。否则,直接取缓存。另外计算出来的值不是响应式对象,我们需要分别调用track和trigger。这样我们就实现了一个完整的computed函数,vue3内部也有实现。