1。上一篇写的主要是介绍了如何简单的实现一个响应系统,但是还有很多未知和不可控的问题,比如副作用函数的嵌套,如何避免无限递归,多个副作用函数的影响?本文将解决以下问题:分支切换嵌套效果的无限递归可调度性2、分支切换和清理分支切换页面渲染时,我们需要避免副作用函数的遗留问题。你为什么这么说?首先看下面的代码片段。副作用函数effect里面的箭头函数里面有一个三元表达式,根据state.flag的值来切换页面渲染的值。这是我们期望的分支切换。constdata={name:"pingping",age:18,flag:true};conststate=newProxy(data,{/*其他代码省略*/});//副作用函数,effect执行并渲染页面effect(()=>{console.log("render");document.body.innerHTML=state.flag?state.name:state.age;})当flag的值为初始值true时,页面渲染结果如图注:但实际上分支切换可能有遗留问题副作用函数。在上面的代码片段中,标志的初始值为真。这时候就会从响应对象状态中获取字段flag的值。这时effect函数会执行触发flag和name的read操作,sideeffect函数会和response数据建立关系。连接。当flag的初始值为true时,实际上只有Map的key值关联了flag和name以及side-effectfunction,只会收集这两个响应式data-side-effectfunction的依赖关系.当flag字段的值改为false时,会触发副作用函数effect的重新执行。逻辑上不会读取name的值,只会触发flag和age的读取操作。理想情况下,这两个应该由依赖集合收集。该字段对应的副作用函数。side-effect函数与响应数据的关系但实际上,这种变化在上面的代码中是无法实现的。修改字段flag的值后会触发副作用函数的重新执行,整个依赖关系会维持flag为true时的关系图。name字段生成的副作用函数被继承。//当设置一个不存在的属性时setTimeout(()=>{state.flag=false;},1000)如上面代码,遗留副作用函数会造成不必要的数据更新,之所以这么说是因为flag的值改为false后,会触发更新,重新执行副作用函数。这时应该没有name的依赖,即不会读取name的值。不管flag的值怎么变,应该只读age的值,不读name。上述代码实际执行效果如下图所示,页面渲染值没有变化,控制台打印显示://当设置了一个不存在的属性时setTimeout(()=>{state.flag=false;setTimeout(()=>{console.log("name的值改变了,理论上不会更新页面数据...");state.name="onechuan"})},1000)即使我们在setTimeout中继续修改name的值,页面依然呈现name初始值“pingping”,控制台显示我们修改了name的值。cleanup那么,我们应该如何解决上面副作用函数遗留下来的问题呢?其实我们只需要设置它在每次副作用函数触发执行的时候从所有关联的依赖集合中删除即可。副作用函数执行完成后,会重新建立连接,再次将副作用函数收集到依赖集合中,但是之前的副作用函数已经清理干净了。“收拾屋子,再请客。”清除副作用函数和响应数据之间的连接。我们应该如何实施上述理论呢?我们首先要确定哪些依赖集包含遗留的副作用函数,我们需要重新设计副作用函数effect。在effect函数内部定义一个effectFn函数,并在其中添加一个effectFn.deps数组,用于存放包含当前sideeffect函数的所有依赖集合。每次执行一个副作用函数之前,需要根据effectFn.deps获取依赖集,调用cleanupEffect函数清理剩余的副作用函数。//全局变量用于存放注册的副作用函数letactiveEffect;//effect用于注册副作用函数functioneffect(fn){consteffectFn=()=>{//调用函数清理遗留副作用functioncleanupEffect(effectFn)//当调用effect注册一个副作用函数,将副作用函数fn赋值给activeEffectactiveEffect=effectFn;//执行副作用函数fn();}//deps用于存放所有与副作用函数effectFn关联的依赖集合。部门=[];//执行副作用函数effectFneffectFn()}cleanupEffect函数旨在实现如下代码段,接收一个effectFn副作用函数作为参数,遍历收集依赖集的effectFn.deps数组,去除effectFn函数从依赖集中清除,最后重置effectFn.deps数组。//遗留副作用函数的清理函数functioncleanupEffect(effectFn){const{deps}=effectFn//遍历依赖集合数组for(leti=0;i{};lets=newSet([effect])s.forEach(item=>{s.delete(effect);s.add(effect)});//这会导致死亡那么我们应该如何打破这个循环呢?很简单,构造一个新的Set集合进行遍历即可。即只需要修改trigger函数中的语句即可://在set拦截函数中调用trigger函数触发变化函数trigger(target,key){//根据target从bucket中获取depMapsconstdepMaps=bucket.get(目标);//判断是否有if(!depMaps)return//根据key值获取对应的副作用函数consteffects=depMaps.get(key);//执行副作用函数//effects&&effects.forEach(fn=>fn())consteffectsToRun=newSet(effects);effectsToRun.forEach(effectFn=>effectFn());}此时有:修改age值前的页面控制台打印结果:3.嵌套效果和效果栈嵌套在实际开发效果时,我们难免会这样写效果函数嵌套,即一个效果函数在里面嵌套另一个效果函数。effect(()=>{effct(()=>{/*...*/})})如果我们的响应式系统不支持effect嵌套,会发生什么?//原始数据constdata={name:"pingping",age:18,flag:true}//代理对象conststate=newProxy(data,{/*其他代码省略*/});//全局变量lettemp1,temp2;//effectFn1嵌套effectFn2effect(()=>{console.log("executeeffectFn1");effect(()=>{console.log("executeeffectFn2");//读取state.name属性temp2ineffectFn2=state.name;})//读取effectFn1中的state.age属性temp1=state.age;})setTimeout(()=>{state.age=19},1000)在上面的代码中,简单的写一个effect嵌套demo,effectFn1嵌套在effectFn2里面,那么effectFn1的执行会导致effectFn2的执行。effectFn2中读取state.name的值,effectFn1中读取state.age的值,effectFn2的读操作优先于effectFn1的读操作。即:state|__name|__effectFn1|__age|__effectFn2这种情况下,理论上修改state.name的值只会触发effectFn2的执行,而当修改state.age的值时,会触发执行effectFn1执行并间接触发effectFn2函数的执行。但实际上,修改state.age的值的结果如下图所示,打印了3次,effectFn1只执行了一次,但是effectFn2执行了两次,并没有重新执行effectFn1的函数修改过程中。为什么会这样?这是因为我们嵌套了多个效果函数,activeEffect全局变量一次只能存放一个通过效果函数注册的副作用函数。当effect嵌套时,内层effect产生的sideeffectfunction会覆盖activeEffect的值,永远无法回到过去。“真是个没心没肺的人。”效果执行栈那么如何解决这个问题呢?想想js的事件循环机制,就知道是用一个栈数据结构来存储当前正在执行的事件。同样,我们也可以添加一个副作用函数执行栈effectStack。当当前的副作用函数执行时,入栈,执行完弹出,让activeEffect指向栈顶的副作用函数,即最近执行的副作用函数.leteffectStack=[];//effect用于注册副作用函数functioneffect(fn){consteffectFn=()=>{//调用函数完成清理剩余的副作用functioncleanupEffect(effectFn)//调用effect注册副作用函数时,将副作用函数fn赋值给activeEffectactiveEffect=effectFn;//在执行副作用函数之前压栈effectStack.push(effectFn)//执行副作用函数fn();//执行完成后出栈effectStack.pop()activeEffect=effectStack[effectStack.length-1]}//deps用于存放所有与副作用函数关联的依赖effectFn.deps=[];//执行副作用函数effectFneffectFn()}在上面的代码片段中,定义了一个effectStack数组来存放要执行的副作用函数,activeEffect始终指向当前正在执行的副作用函数。根据栈结构的先进后出原则,外层效果先入栈,内层效果后入栈顶。内层执行完成后,外层效果出栈。这样,反应式数据只会收集直接读取当前值的副作用函数作为依赖项,从而避免混淆。这样控制打印:打印结果4.避免无限递归循环。在存储当前执行的副作用函数的依赖集时,可能会出现循环执行的情况。我们还添加了一个新的Set集合来解决它。当我们对副作用函数中相同字段的值执行无限递归循环时会发生什么?//原始数据constdata={name:"pingping",age:18,flag:true}//代理对象conststate=newProxy(data,{/*其他代码省略*/});effect(()=>{state.age++;})我们看到执行结果有爆栈,内存溢出:内存溢出我们可以看到state.age++;语句中,既有state.age的读取操作,也有设置值的操作,使得之前的side-effectfunction还没有执行完,重新开始一个新的执行,从而无限递归调用自己。“我叫我自己,超越我自己”那么,我们应该如何避免堆栈溢出呢?在上一篇文章中我们知道,对state.age的值跟踪和设置值触发操作都是在同一个副作用函数activeEffect中实现的。那么只需要在trigger中加一个guardcondition:判断触发trigger的sideeffectfunction和当前正在执行的sideeffectfunction是否相同,如果相同则不触发执行,否则执行被执行。//调用set拦截函数中的trigger函数触发变化函数trigger(target,key){//根据target从bucket中获取depMapsconstdepMaps=bucket.get(target);//判断是否有if(!depMaps)return//根据key值获取对应的副作用函数consteffects=depMaps.get(key);consteffectsToRun=newSet();//执行副作用函数effects&&effects.forEach(effectFn=>{if(effectFn!==activeEffect){effectsToRun.add(effectFn)}})effectsToRun.forEach(effectFn=>effectFn());}执行时trigger触发器,比较过滤触发trigger的sideeffectfunction和当前正在执行的sideeffectfunction,避免栈内存溢出。5.调度执行首先理解可调度性的含义,即当触发器触发副作用函数重新执行时,可以自定义副作用函数执行的时机、频率和执行方式。//原始数据constdata={name:"pingping",age:18,flag:true}//代理对象conststate=newProxy(data,{/*其他代码省略*/});effect(()=>{console.log(state.age);});state.age++;console.log("runend");执行结果如果我们需要改变代码的执行顺序,得到不同的结果,我们需要提供给用户进行调度的能力,允许用户自定义调度器。//effect用于注册副作用函数functioneffect(fn,options={}){consteffectFn=()=>{//调用函数完成清理剩余的副作用functioncleanupEffect(effectFn)//调用effect注册副作用函数时,将副作用函数fn赋值给activeEffectactiveEffect=effectFn;//在执行副作用函数之前压栈effectStack.push(effectFn)//执行副作用函数fn();//执行完成后出栈effectStack.pop()activeEffect=effectStack[effectStack.length-1]}//挂载options到effectFn函数effectFn.options=options//deps用于存放所有关联的依赖副作用函数effectFn.deps=[];//executeSideeffectfunctioneffectFneffectFn()}//调用set拦截函数中的trigger函数触发改变函数trigger(target,key){//根据target从bucket中取出的depMapsconstdepMaps=bucket.get(目标);//判断有没有if(!depMaps)return//根据key值获取对应的副作用函数consteffects=depMaps.get(key);consteffectsToRun=newSet();//执行副作用函数effects&&effects.forEach(effectFn=>{if(effectFn!==activeEffect){effectsToRun.add(effectFn)}})effectsToRun.forEach(effectFn=>{//如果副作用函数中有调度器if(effectFn.options.scheduler){effectFn.options.scheduler(effectFn)}else{effectFn()}});上面的代码片段中,当trigger触发副作用函数的执行时,会先判断副作用函数中是否有调度器:如果有调度器,则直接执行调度器函数,将当前的副作用函数作为参数effectFn.options.scheduler(effectFn)如果没有调度器,则直接执行副作用函数effectFn()。effect(()=>{console.log(state.age);},{//optionsscheduler(fn){//schedulersetTimeout(fn);}});state.age++;console.log("运行结束");执行结果这样,系统设计就实现了对副作用函数执行顺序的控制。此外,我们还可以添加和控制副作用函数的执行次数。另外,我们只需要修改scheduler的代码,这里就不赘述了。6.写在最后本文主要解决的问题是:分支切换导致遗留副作用函数,可以添加一个集合来收集依赖集,在每次执行副作用函数之前清除对应的连接,执行connect后重新建立。effect嵌套的问题可以通过增加一个effectStack执行栈来解决。外层的副作用函数先入栈,内层的函数后入栈。activeEffect总是指向当前要执行的副作用函数。为避免无限递归循环,可以在trigger触发副作用函数执行前判断触发的副作用函数与当前执行的副作用函数是否相同。对于响应系统的调度,可以设置调度器来控制副作用函数执行的顺序、时机和次数。