在之前的文章图解Vue响应式原理中,我们通过9张流程图了解了Vue的渲染流程。相信大家对Vue的整个渲染流程都有一定的了解。这篇文章我们来重点介绍一下Vue异步更新原理模块。本文主要分析了Vue从更新Data到通知Watcher异步更新视图的过程,也就是下图中橙色部分。我们先回顾一下图中的几个对象:Data对象:Vue中data方法返回的对象。Dep对象:每个Data属性都会创建一个Dep来收集所有使用这个Data的Watcher对象。Watcher对象:主要用来渲染DOM。接下来,我们开始分析这个过程。Vue异步更新DOM原理很多同学都知道Vue中的数据更新是异步的,也就是说我们修改Data之后,并不能立即获取到修改后的DOM元素。例如:{{message}}
什么时候才能拿到真正的DOM元素?答:在Vue的nextTick回调中。这一点在Vue官网上有详细的介绍,但是大家有没有想过为什么Vue需要使用nextTick方法来获取最新的DOM呢?带着这个疑问,我们直接看源码。//当一个Data被更新时,会依次执行下面的代码//1.触发Data.set//2.调用dep.notify//3.Dep会遍历所有相关的Watcher,执行update方法classWatcher{//4.执行更新操作update(){queueWatcher(this);}}constqueue=[];functionqueueWatcher(watcher:Watcher){//5.将当前Watcher添加到异步队列中queue.push(watcher);//6.执行异步队列,传入回调nextTick(flushSchedulerQueue);}//更新视图的具体方法functionflushSchedulerQueue(){letwatcher,id;//排序,先渲染父节点,再渲染子节点//这样可以避免不必要的子节点渲染,比如:父节点中v-if为false的子节点,不需要渲染队列。排序((a,b)=>a.id-b.id);//遍历所有Watcher进行批量更新。for(index=0;index{cb.call(ctx);});//2.执行异步任务//该方法会根据浏览器兼容性选择不同的异步策略timerFunc();}可以看到nextTick函数非常简单,它只是将输入的flushSchedulerQueue添加到callbacks数组中,然后执行timerFunc方法。接下来我们分析一下timerFunc方法。lettimerFunc;//判断是否兼容Promiseif(typeofPromise!=="undefined"){timerFunc=()=>{Promise.resolve().then(flushCallbacks);};//判断是否兼容MutationObserver//https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver}elseif(typeofMutationObserver!=="undefined"){letcounter=1;constobserver=newMutationObserver(flushCallbacks);consttextNode=document.createTextNode(String(counter));observer.observe(textNode,{characterData:true,});timerFunc=()=>{counter=(counter+1)%2;textNode.data=String(计数器);};//判断是否兼容setImmediate//部分IE浏览器存在该方法}elseif(typeofsetImmediate!=="undefined"){//这是一个宏任务,但是比setTimeouttimerFunc=()=>{setImmediate(flushCallbacks);};}else{//如果你不知道上面的方法,使用setTimeout0timerFunc=()=>{setTimeout(flushCallbacks,0);};}//异步执行后,执行所有回调方法,即执行flushSchedulerQuueuefunctionflushCallbacks(){for(leti=0;i&l吨;副本长度;i++){回调[i]();}}可以看到,timerFunc是一个基于浏览器兼容性而创建的异步方法。执行后会调用flushSchedulerQueue方法进行具体的DOM更新分析,此时我们可以得到一个整体的流程图。接下来,我们来改进一些判断逻辑。判断has标志以避免将相同的观察者添加到队列中。判断waitingflag,让所有Watcher在一个tick内更新。判断flushingflag,处理Watcher渲染时可能产生的新Watcher。比如:触发v-if的条件,渲染新的Watcher。结合以上判断,最终的流程图如下:最后我们来分析一下为什么this.$nextTick可以拿到更新后的DOM?//我们使用this.$nextTick实际调用nextTick方法Vue.prototype.$nextTick=function(fn:Function){returnnextTick(fn,this);};可以看到调用this.$nextTick其实就是调用图中的nextTick方法执行异步队列中的回调函数。按照先到先得的原则,修改Data触发的update异步队列先执行。执行完成后,会生成一个新的DOM。接下来执行this.$nextTick的回调函数,就可以得到更新后的DOM元素。由于nextTick只是一个通过Promise、SetTimeout等方法模拟出来的异步任务,所以也可以手动执行一个异步任务,达到和这个一样的效果$nextTick。this.message="helloworld";//手动执行一个异步任务,同时获取最新的DOMPromise.resolve().then(()=>{consttextContent=document.getElementById("text").textContent;console.log(textContent==="helloworld");//true});setTimeout(()=>{consttextContent=document.getElementById("text").textContent;console.log(textContent==="你好世界");//真});思考与总结本文从源码的角度介绍了Vue的异步更新原理,我们简单回顾一下。当Vue中的Data被修改时,所有与该Data相关的Watcher都会被触发更新。首先,所有的观察者都会被添加到队列中。然后,调用nextTick方法执行异步任务。在异步任务的回调中,对Queue中的Watcher进行排序,然后进行相应的DOM更新。最后,如果你对此有什么想法,欢迎留言评论!