当前的副作用函数effect立即执行:effect(()=>{console.log(obj.foo)})有些场景不希望effect立即执行,可以添加options来添加属性:effect(()=>{console.log(obj.foo)},//options{lazy:true})这里的lazy就是上一篇介绍的调度。当options.lazy为真时,副作用函数不会立即执行:)effectStack.pop()activeEffect=effectStack[effectStack.length-1]}effectFn.options=optionsoptions.deps=[]//sideeffectfunction只有在不惰性时才执行if(!options.lazy)effectFn()//将副作用函数作为返回值返回returneffectFn}因为最后一行暴露了副作用函数,所以可以在函数外手动执行副作用函数:consteffectFn=effect(()=>{console.log(obj.foo)},//options{lazy:true})//手动执行effectFn()就是这样意义其实不是很大,但是如果能拿到返回值就好多了手动执行后传递的效果函数(fn):consteffectFn=effect(()=>obj.foo+obj.bar,//options{lazy:true})//val是传入函数constval=effectFn()的返回值,所以需要修改effect函数:functioneffect(fn,options={}){consteffectFn=()=>{cleanup(effectFn)activeEffect=effectFneffectStack.push(effectFn)//将fn的结果返回给resconstres=fn()effectStack.pop()activeEffect=effectStack[effectStack.length-1]//使用res作为TheeffectFn的返回值returnres}effectFn.options=optionseffectFn.deps=[]//只有在非延迟加载时才执行副作用函数if(!options.lazy)effectFn()//返回副作用函数为返回值returneffectFn}其实传给effect的fn才是真正的副作用函数,effectFn是对fn的重新包装,所以effectFn执行后返回fn得到的值还要加上constres=fn()和returnres是个题外话,根据前段时间发表的文章<浅谈闭包>,新加入的res变量是一个闭包,fn也是一个回调函数。当然,effectFn也是一个闭包,现在实现了惰性执行。可以获得副作用函数和副作用函数的执行结果,实现计算属性:functioncomputed(getter){//使用getter作为副作用函数consteffectFn=effect(getter,{lazy:true})constobj={//读取值时执行effectFngetvalue(){returneffectFn()}}returnobj}现在可以使用计算函数创建一个计算属性:constdata={foo:1,bar:2}constobj=newProxy(data,{/*...*/})constsumRes=computed(()=>obj.foo+obj.bar)console.log(sumRes.value)到目前为止的完整代码是://用于存储副作用函数的桶constbucket=newWeakMap()//用于存储已注册副作用的函数letactiveEffect=undefined//副作用函数堆栈consteffectStack=[]functioncleanup(effectFn){for(letitmeofeffectFn.deps){itme.delete(effectFn)}effectFn.deps.length=[]}functioneffect(fn,options={}){consteffectFn=()}=>{console.log('效果');cleanup(effectFn)activeEffect=effectFneffectStack.push(effectFn)//将fn的结果返回给resconstres=fn()effectStack.pop()activeEffect=effectStack[effectStack.length-1]//使用res作为返回effectFn的值returnres}effectFn.options=optionseffectFn.deps=[]//只执行副作用函数if(!options.lazy)effectFn()//返回副作用函数作为返回值returneffectFn}//constdata={//text:'helloworld',//ok:true//}constdata={foo:1,bar:2}constobj=newProxy(data,{//拦截读取操作get(target,key){track(target,key)//返回属性值returntarget[key]},//拦截集合操作set(target,key,newVal){//设置属性值target[key]=newValtrigger(target,key)}})functiontrack(target,key){//没有activeEffect,直接返回if(!activeEffect)returntarget[key]//根据target返回'bucket'中的depsMap,也是一个Map类型:key--->effectsletdepsMap=bucket.get(target)//如果depsMap不存在,则新建一个Map,将其与目标相关联if(!depsMap)bucket.set(target,(depsMap=newMap()))//根据key从depsMap中获取deps,是一个Set类型//存储所有相关的副作用函数tothecurrentkey:effectsletdeps=depsMap.get(key)//如果deps不存在,则创建一个新的Set并将其与键0相关联if(!deps)depsMap.set(key,(deps=newSet()))//最后将当前激活的副作用函数添加到“bucket”中=depsMap.get(key)常量effectsToRun=newSet()effects&&effects.forEach(effectFn=>{//如果trigger触发的副作用函数和当前正在执行的函数相同,则不会触发执行if(effectFn!==activeEffect){effectsToRun.add(effectFn)}})effectsToRun.forEach(effectFn=>{//如果副作用函数有调度器,调用调度器并将副作用函数作为参数传递if(effectFn.options.scheduler){effectFn.options.scheduler(effectFn)}else{//否则,直接执行副作用函数effectFn()}})}functioncomputed(getter){//使用getter作为副作用函数consteffectFn=effect(getter,{lazy:true})constobj={//读取值时effectFngetvalue(){returneffectFn()}}returnobj}constsumRes=computed(()=>obj.foo+obj.bar)console.log(sumRes.value)//3console.log(sumRes.value)//3console.log(sumRes.value)//3console.log(sumRes.value)//3在函数effectFn中打印字符串'effect'后,会发现sumRes.value已经被取了四次,函数effectFn会执行四次:所以需要在computed中添加一个value缓存:functioncomputed(getter){//value用来缓存最后一次计算的值letvalue//dirty标志,用于标识是否需要重新计算,true表示需要计算letdirty=trueconsteffectFn=effect(getter,{lazy:true})constobj={getvalue(){if(dirty){value=effectFn()//设置dirty为false,下次直接访问askvaluedirty=false}returnvalue}}returnobj}中存储的值此时只会计算一次,每次访问都不会重新执行副作用函数,不过相信你聪明如你所见问题,如果我们改变obj访问sumRes.value后,你会发现访问的值并没有改变,这里就不演示了。解决方法是当其中一个值发生变化时,将dirty重置为true。这时候,我们可以添加一个调度器(参见:执行调度):需要计算flagletdirty=trueconsteffectFn=effect(getter,{lazy:true,//添加调度器,重置脏调度器(){dirty=true}})constobj={getvalue(){if(dirty){value=effectFn()//设置dirty为false,下次直接访问value中存储的值dirty=false}returnvalue}}returnobj}现在基本完美了,但是在某些情况下有缺陷:constsumRes=computed(()=>obj.foo+obj.bar)effect(()=>{//读取副作用函数中的计算属性console.log(sumRes.value)//3})obj.bar++inobj.bar++期望是触发计算属性并重新渲染,但实际上并没有。原因是计算属性有它自己的作用并且是延迟执行的。只有当计算属性的值真正被读取时才会被执行。对于计算属性函数的getter,在其中访问的响应数据只会收集计算属性函数内部的效果作为依赖,而在上面的例子中,当计算属性用于另一个效果时会发生效果嵌套,并且外部效果不会被内部效果中的响应数据收集。我们从computed函数也可以看出重新上传了一个对象obj,手动给它赋了一个get函数,不是这样的:constdata={foo:1,bar:2}constobj=newProxy(data,{//拦截读取操作get(target,key){track(target,key)//返回属性值returntarget[key]},//拦截设置操作set(target,key,newVal){//设置属性值target[key]=newValtrigger(target,key)}})使用proxy,两者其实是完全分开的,但是使用的是同一个副作用函数解决方法很简单由于computed属性没有绑定跟踪,手动调用:functioncomputed(getter){//value用于缓存上次计算的值letvalue//dirtyflag,用于标识是否需要重新计算,trueflag需要计算letdirty=trueconsteffectFn=effect(getter,{lazy:true,//添加调度器,将dirty重置scheduler(){dirty=true//当计算属性depe的反应数据ndsonchanges,手动触发响应trigger(obj,'value')}})constobj={getvalue(){if(dirty){value=effectFn()//设置dirty为false,下次直接访问value中存储的值dirty=false}//读取value时,手动调用track函数进行跟踪track(obj,'value')返回值}}returnobj}这样就完成了!里面有很多闭包...,目前完整的闭包代码是://Bucketforstoringside-effectfunctionsconstbucket=newWeakMap()//FunctionusedtostoreregisteredsideeffectsletactiveEffect=undefined//副作用函数堆栈consteffectStack=[]functioncleanup(effectFn){for(letitmeofeffectFn.deps){itme.delete(effectFn)}effectFn。deps.length=[]}functioneffect(fn,options={}){consteffectFn=()=>{console.log('effect');cleanup(effectFn)activeEffect=effectFneffectStack.push(effectFn)//将fn的结果返回给resconstres=fn()effectStack.pop()activeEffect=effectStack[effectStack.length-1]//使用res作为返回effectFn的值returnres}effectFn.options=optionseffectFn.deps=[]//副作用函数只有在非惰性加载时才执行if(!options.lazy)effectFn()//返回副作用函数为返回值returneffectFn}//constdata={//text:'helloworld',//ok:true//}constdata={foo:1,bar:2}constobj=newProxy(data,{//拦截读取操作get(target,key){track(target,key)//返回属性valuereturntarget[key]},//拦截设置操作set(target,key,newVal){//设置属性值target[key]=newValtrigger(target,key)}})functiontrack(target,key){//没有activeEffect,直接返回if(!activeEffect)returntarget[key]//根据target从'bucket'中返回depsMap,也是一个Map类型:key--->effectsletdepsMap=bucket.get(target)//如果depsMap不存在,创建一个新的Map关联到targetif(!depsMap)bucket.set(target,(depsMap=newMap()))//然后根据depsMap获取depskey,是一个Set类型//存储所有与当前key相关的副作用函数:effectsletdeps=depsMap.get(key)//如果deps不存在,也创建一个新的Set,并关联key0if(!deps)depsMap.set(key,(deps=newSet()))//最后,将当前激活的副作用函数添加到'bucket'deps.add(activeEffect)}functiontrigger(target,key){constdepsMap=bucket.get(target)如果(!depsMap)returnconsteffects=depsMap.get(key)consteffectsToRun=newSet()effects&&effects.forEach(effectFn=>{//如果触发器触发的副作用函数与当前正在执行的函数相同如果(effectFn!==activeEeffect){effectsToRun.add(effectFn)}})effectsToRun.forEach(effectFn=>{//如果副作用函数有调度器,调用调度器并将副作用函数作为参数传递if(effectFn.options.scheduler){effectFn.options.scheduler(effectFn)}else{//否则直接执行副作用函数effectFn()}})}functioncomputed(getter){//value用于缓存上次计算的值letvalue//dirty标志,用于标识是否需要重新计算,如果为真,则需要重新计算{dirty=true//当计算依赖属性的响应数据发生变化时,手动触发响应trigger(obj,'value')}})constobj={getvalue(){if(dirty){value=effectFn()//设置dirty为false,下次直接访问value中存储的值dirty=false}//读取值时,手动调用track函数跟踪track(obj,'value')returnvalue}}returnobj}constsumRes=computed(()=>obj.foo+obj.bar)effect(()=>{//读取side中的计算属性效果函数console.log(sumRes.value)//3})obj.bar++//effect(()=>{//console.log('effectrun');//document.body.innerText=obj.ok?obj.text:'not'//})//setTimeout(()=>{//obj.ok=false//},2000)
