当前位置: 首页 > Web前端 > HTML

Vue—关于Responsive(2.异步更新队列原理分析)

时间:2023-03-28 11:21:41 HTML

本节需要准备的知识点:EventLoop,Promise。EventLoop的介绍可以参考阮一峰老师的文章:http://www.ruanyifeng.com/blo...https://www.ruanyifeng.com/bl...关于Promise:https://developer.mozilla.org...上一节学习了Vue通过Object.defineProperty拦截数据变化的响应式原理,数据变化后会触发notify方法通知变化。本节继续沿着地图阅读。Vue收到通知后,会开启一个异步更新队列。两个问题:vue开启一个异步更新队列。为什么不是同步而是异步呢??不知道大家有没有发现,在Vue中修改data中的数据时,无论修改多少次,最终的模板只会渲染一次。这是怎么做到的?1.异步更新队列先看一个代码演示,拿上一节的代码:letx;lety;letf=(n)=>n*100;letactive;letonXChange=function(cb){活动=cb;active();};classDep{deps=newSet();//收集依赖depend(){if(active){this.deps.add(active);}}//通知依赖更新notify(){this.deps.forEach((dep)=>dep());}}letref=(initValue)=>{letvalue=initValue;让dep=newDep();returnObject.defineProperty({},"value",{get(){dep.depend();returnvalue;},set(newValue){value=newValue;dep.notify();},});};x=ref(1);onXChange(()=>{y=f(x.value);console.log('onXChange',y);});x.value=2;x.value=3;假设我们现在不仅依赖x,还依赖y,z,分别输出x,y,z到页面,我们现在依赖x,y,z这三个变量,所以我们要把名字改成要监视的onXChange函数,这意味着它可以监视更改,而不仅仅是x的更改。letx;lety;letz;x=ref(1);y=ref(2);z=ref(3);//考虑到我们会依赖很多变量,把onXChange改成watch更符合语义观察(()=>{document.write(`

x:${f(x.value)};y:${f(y.value)};z:${f(z.value)}

`)});可以看到页面上打印了这三个值现在我们修改x,y,z的值x.value=2;y.value=3;z.value=4;检查页面,结果没问题,每一次数据变化都被监听响应了。现在结果是正确的,我们的问题是什么?问题是:响应每次数据变化,每次渲染模板。如果数据改变一百或一千次怎么办?是否需要重复渲染一百次或一千次?我们都知道频繁操作dom会影响网页的性能。如果对重排重绘知识感兴趣,请阅读阮一峰老师的文章:https://www.ruanyifeng.com/bl...因此,需要保证所有的依赖都准确更新,并且首要的问题是保证不能频繁渲染。现在我们修改x.value、y.value和z.value以同步更新依赖项。有没有一种机制可以等到我修改这些值?稍后执行更新任务呢?答案是——异步。异步任务会等到同步任务被清除后才能执行。有了这个特性和我们之前的分析,我们需要:创建一个队列来存储任务创建一个将任务推入队列的方法创建一个方法承诺执行队列中的任务(用于创建微任务)按照步骤创建以下代码://创建任务队列letqueue=[];//创建添加任务的方法letqueueJob=(job)=>{//过滤添加的任务if(!queue.includes(job)){queue.push(工作);//添加任务flushJobs();//执行任务,请注意这是伪代码}};//创建一个方法来执行任务letflushJobs=()=>{letjob;//将队列中的任务一一取出分配给job执行,直到队列清空while((job=queue.shift())!==undefined){job();}};//创建一个Promise,待定那么我们需要修改notify代码,不是监听到数据变化就立即调用依赖更新,而是将依赖添加到队列中notify(){this.deps.forEach(dep=>queueJob(dep));回到页面,我们发现模板在页面渲染了3次,那么我们写的这段代码有什么用呢?异步体现在哪里?接着往下看2.NextTick原理分析虽然我们在上面的代码中开启了一个队列,并成功将任务推入队列执行,但本质上是同步推入和执行的。我们想把它做成一个异步队列那么Promise就该发挥作用了。宏任务和微任务的介绍可以参考:https://zhuanlan.zhihu.com/p/...我们创建了nextTick函数,它接收一个回调函数,返回一个status是一个fulfilledPromise,并将回调函数传递给then方法//创建一个PromiseletnextTick=(cb)=>Promise.resolve().then(cb);那么只需要在添加任务的时候调用nextTick,任务的flushJobs就会被执行函数可以传给nextTickletqueueJob=(job)=>{//过滤添加的任务if(!queue.includes(job)){queue.push(job);//添加任务nextTick(flushJobs);//推送微任务}};回到页面虽然x,y和z三个变量的值最后只在页面上渲染了一次。总结一下这段代码的执行过程:当x.value被修改时,会触发dep.notify()通知依赖更新,然后我们会开一个队列给任务(这个任务是active保存的回调函数)被发送到queueJob函数。queueJob函数判断当前任务是否已经添加。如果没有,则添加当前任务并执行nextTick(Promise),因为在Promise调用then方法时会通过then中的回调将函数压入microtask队列,所以flushJobs函数不会立即执行,而是会在所有同步任务执行完毕后执行,也就是说y和z修改值后(如果后面还有其他同步代码,则要继续等待),flushJobs函数要等到Event的下一个tick时才会执行环形。(这三个通知触发的是同一个active,所以queueJob只会往队列中添加一个任务)所以不管后面y和z的值改变了多少次,当前更新任务只执行一次,从而达到优化的目的。这正是Vue采用的解决方案——异步更新队列。官方文档描述的很清楚。我们可以通过两种方式调用nextTick:Vue.nextTick()this.$nextTick()(至于什么时候使用nextTick,不偷懒看官方文档就可以找到答案哈哈)下面源码摘录从vue2.6.11版本开始,这两个API分别挂载在initGlobalAPI函数和renderMixin函数中,都引用了nextTick函数nextTick源码如下:内部访问外部回调,回调就是前面提到的队列,然后调用nextTick队列推一个回调函数,然后判断pending(pending的作用是控制timerFunc同时只执行一次),调用timerFunc(),最后返回一个Promise(大家都用过nextTick应该知道)。我们来看看callbacks、pending、timerFunc是怎么定义的。可以看到timerFunc函数只是调用了p.then方法并将flushCallbacks函数压入microtask队列,而p是一个fulfilledPromise,这和我们自己的nextTick函数是一样的。这个flushCallbacks函数有什么作用?在flushCallbacks中,将pending重置为初始值,复制callbacks队列中的任务,清空队列,然后依次执行复制的任务,这与我们自己的flushJobs函数是一致的。看完上面的源码,我们可以总结出Vue就是干这个的,是时候提炼一下小学语文的中心思想了。监听到数据变化后,调用dep.notify()进行通知,将任务放入队列,同样只添加一次调用Promise.resolve().then(flushCallbacks),将任务执行函数推送到微任务中queue,等待所有的同步任务完成,然后执行所有的同步执行,执行渲染的flushCallbacks函数,对比一下我们自己写的代码你学会了吗?以上demo代码已经上传到github:https://github.com/Mr-Jemp/Vu...后面要学习的内容在这里:Vue—关于Responsive(三、DiffPatch原理分析)Vue—关于Responsive(四、深入学习Vue响应式源码)本文由多发博文平台OpenWrite发布!