持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天,点击查看活动详情
上一篇我们初探了响应式系统 👉 初探响应式系统 - 掘金 (juejin.cn)
欲看懂这一篇还是建议先看上一篇,这一篇我们继续往下走
一、嵌套问题
🖖先看背景
- 在这段代码里面,
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()) ----旧代码,我们不用了
//使用scheduler
effectsToRun.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
- 到这里就把一个简易的响应式的系统完成啦
- 注:这篇是基于个人学习 《Vue.js设计与实现》 这本书过程的一种梳理和整理输出