欲看懂这一篇还是建议先看上一篇,这一篇我们继续往下走
一、嵌套问题
🖖先看背景
- 在这段代码里面,
question1
里面嵌套了question2
,所以question1
的执行会导致question2
的执行
let temp1 ,temp2;
function question1() {console.log('question1执行')effect(question2)temp1 = proxy.foo
}
function question2() {console.log('question2执行')temp2 = proxy.bar
}
effect(question1)
- 由于
question1
中使用了proxy.foo
,question2
使用了proxy.bar
,那么我们所希望的结构应该是这样
//理想情况
data|___foo |__question1|___bar|__question2
- 但事实却是:初始化的时候触发
question1
,question1
内再触发一次question2
,当我们修改proxy.foo
后本应该再次触发question1
再触发question2
,但是却只触发了question2
🖖分析原因
- 我们只用了一个
activeEffect
来存储副作用函数,但是这样当副作用函数嵌套的时候,内层的副作用函数会覆盖activeEffect
的值。 - 所以即使
proxy.foo
是在外层副作用函数question1
读取的,但是它收集到的副作用函数也是内层的question2
,也就解释了为什么上面只执行question2
而不执行question1
//实际情况
data|___foo |__question2|___bar|__question2
🖖解决方案
- 涉及到有序的多个数据存储,嵌套函数就是最里层的最后调用,也最先执行完毕,就纯纯符合栈结构
- 那么我们可以选择使用一个
effectStack
来存储副作用函数,当副作用函数执行的时候,让它压入栈,当它执行完毕出栈 - 所以我们一共增加了四步* 新增一个
effectStack
栈* 在fn()
执行前,将effectFn
推进栈* 在fn()
执行后,将effectFn
推出栈* 将activerEffect
重新复制为栈顶元素
//用一个全局变量存储被注册的副作用函数
let activeEffect;
const effectStack = []//新增一个栈
function effect(fn) {const effectFn = () => {cleanup(effectFn);//当effectFn执行的时候,把它设置为当前激活的副作用函数activeEffect = effectFn;effectStack.push(effectFn); //新增fn();effectStack.pop(); //新增activeEffect = effectStack[effectStack.length-1];//新增}effectFn.deps = [];effectFn();
}
🖖测试
- 依旧还是上面的修改
proxy.foo
的值:proxy.foo = '123'
- 现在就达到预期情况了,执行了
question1
后再执行了question2
二、递归循环问题
🤚先看背景
当你解决了上面的问题兴致勃勃测试数据的时候,你输入了
proxy.bar++
,打开控制台一看,坏了,发现它竟然递归调用了导致了栈溢出报错
🤚分析原因
proxy.bar++
是一个自增的操作,那么它就会先获取值,再设置值- 当我们获取值的时候,就会触发
track
函数,那么该函数就会加入list
,当我们+1后设置值的时候,又会触发trigger
函数,把list
的事件取出执行,它本身都还没有执行完,就又让调用它,那么就是无限递归调用自己,导致如上图所示的栈溢出 - 本质上就是在同一个函数中先获取值,再设置值,就会导致目前这个代码进入循环状态
- 可以证实一下我们的想法,即不使用自增,而是先获取
proxy.bar
的值,再设置值,同样报出了错误;但是如果是先设置值,再获取值即无问题
function question2() {console.log('question2执行')temp2 = proxy.bar;proxy.bar = '123'
}
🤚解决方案
- 既然我们上述提到了是在同一函数中进行这种操作才有问题,那么我们自然是希望,如果在赋值的时候,发现是触发的副作用函数刚好是当前执行函数的时候就不执行它
- 触发的副作用函数我们可以在
forEach
循环里面寻找 - 当前执行函数刚好就是
activerEffect
- 那么就说明我们的
effectsToRun
不能全盘照收从list取出来的副作用集合,而是需要由一个剔除操作
function trigger(target,key) {const desMap = list.get(target);if(!desMap) return;//从desMap中取中存放副作用事件的Set结构const effects = desMap.get(key);//不要全盘接受const effectsToRun = new Set();//剔除操作effects && effects.forEach(effectFn => {//如果不相等就加进去,如果相等就不处理if(effectFn !== activeEffect) {effectsToRun.add(effectFn)}})effectsToRun.forEach(effectFn=> effectFn());
}
🤚测试
- 我们直接拿上面分析原因的那段代码进行测试,发现现在已经解决了递归调用问题,即正常调用
三、扩充调度
🤚背景
- 当我们问题解决后,自然就想着能不能给他加上一些功能
- 例如,能不能加上一个调度器,可以用它来控制副作用函数的执行顺序,甚至控制该副作用函数的调用次数
🤚实现方案
- 要让它可调度,其实就是将它的执行交给我们调度函数去解决,那么只需要走三步:* 传入* 挂载* 使用
- 传入:我们选择以
options
作为形参,赋初始值{}
,options
内可以放入scheduler
(调度函数)
为什么不是直接传入scheduler呢:因为这个函数不会只有调度这个功能配置功能,所以我们希望传入的结构应该是
//options对象 { //调度函数 scheduler(fn) { } }
- 挂载:人家都传入来了,你总得用吧,难不成丢那里自己就会调用了(Bushi
function effect(fn, options = {} ) {
.......//这里都不用修改effectFn.deps = [];//看这里看这里,把它挂载上去即可effectFn.options = options;effectFn();
}
- 使用:首先
options
中可能有传入scheduler
,也可能没有传入,那么我们首先就要给它加个判断,其次调度函数影响的是调用过程,那么我们就先去找到trigger
函数,再去找到调用它的地方,也就是在forEach
那个遍历调用的地方,然后把副作用函数的执行交给调换函数去处理
// effectsToRun.forEach(effectFn=> effectFn())----旧代码,我们不用了//使用schedulereffectsToRun.forEach(effectFn => {//判断if(effectFn.options.scheduler) {//有则使用effectFn.options.scheduler(effectFn)} else {//没有则则直接调用effectFn()}});
🤚测试
- 比如我希望这个副作用函数能够一秒钟执行一次* 在
scheduler
里面使用了setInterval
每隔一秒就去执行show
函数
function show() {document.body.innerText = `现在的数字为${proxy.num}`proxy.num++;
}
effect(show,{scheduler(fn) {setInterval(fn,1000)}
})
proxy.num++
- 结果如图
四、继续扩展
再比如我期望我能够去控制它的执行次数,比如说
Vue.js
中连续多次修改响应式数据但是只触发一次更新的效果
👋实现方案
- 先定义一个
jobQueue
集合,用来存放不重复的副作用函数 - 通过
isFlushing
(表示是否正在刷新),控制是否是否去执行jobQueue
内的任务 - 通过
p.then
将函数添加到微任务队列,在微任务队列中遍历执行jobQueue
内的任务,执行完将isFlushing
置为false
const jobQueue = new Set();
const p = Promise.resolve();
let isFlushing = false;
function flushJob() {if(isFlushing) return;isFlushing = true;p.then(()=>{jobQueue.forEach(job=>job())}).finally(()=>{isFlushing = false})
}
👋使用
- 副作用函数为
dosomething
,打印proxy.num
- 传入调度函数
- 让
proxy.num
递增十次
function doSomething() {console.log(proxy.num);
}
effect(doSomething,{scheduler(fn) {jobQueue.add(fn);flushJob()}
})
for(let i = 0;i<10;i++) {proxy.num++;
}
- 只打印出了
👋分析过程
- 每一次
proxy.num
递增,就会进去trigger
函数里面找到scheduler
,然后jobQueue
把该函数推入集合;由于我们jobQueue
设置的Set
结构,所以你无论推进入同一副作用函数doSomething
几次其实里面都只有一个* 图为截取部分过程中打印的jobQueue
- 推进去之后就调用
flushJob
函数,一旦它开始执行,isFulshing
标志就会被设置为true
,也就是说无论你调用多少次,在一个周期内都只会执行一次,实际上它在一个事件循环中只会执行一次* 图为过程中打印isFulshing
,可以知道只有第一次是可以进入执行的,后面都被return
回来了 - 当微任务队列开始执行的时候,就会拿出
jobQueue
中的函数依次执行,但是我们前面已经说了只有一个函数,辣就是说只去执行一次,此时proxy.num
的值已经递增到11了,所以就打印出了11
最后
整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。
有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享
部分文档展示:
文章篇幅有限,后面的内容就不一一展示了
有需要的小伙伴,可以点下方卡片免费领取