这个问题应该先要做一个前提补充,我们知道当数据在同步变化的时候,页面订阅的响应操作为什么不会与数据变化完全对应,而是在所有的数据变化操作做完之后,页面才会得到响应,完成页面渲染。
从一个例子体验一下异步渲染机制。
importVuefrom'Vue'newVue({el:'#app',template:'
然而由于Vue内部的异步渲染机制,实际上页面只会渲染一次,把第一次的赋值所带来的的响应与第二次的赋值所带来的的响应进行一次合并,将最终的val只做一次页面渲染,而且页面是在执行所有的同步代码执行完之后才能得到渲染。
在上述例子里的while阻塞代码之后,页面才会得到渲染,就像在熟悉的setTimeout里的回调函数的执行一样,这就是的异步渲染。熟悉React的同学,应该很快能想到多次执行setState函数时,页面render的渲染触发,实际上与上面所说的Vue的异步渲染有异曲同工之妙。
我们可以从用户和性能两个角度来探讨这个问题。
从用户体验角度,从上面例子里便也可以看出,实际上我们的页面只需要展示第二次的值变化,第一次只是一个中间值,如果渲染后给用户展示,页面会有闪烁效果,反而会造成不好的用户体验。
从性能角度,例子里最终的需要展示的数据其实就是第二次给val赋的值,如果第一次赋值也需要页面渲染则意味着在第二次最终的结果渲染之前页面还需要渲染一次无用的渲染,无疑增加了性能的消耗。
对于浏览器来说,在数据变化下,无论是引起的重绘渲染还是重排渲染,都有可能会在性能消耗之下造成低效的页面性能,甚至造成加载卡顿问题。
异步渲染和熟悉的节流函数最终目的是一致的,将多次数据变化所引起的响应变化收集后合并成一次页面渲染,从而更合理的利用机器资源,提升性能与用户体验。
先总结一下原理,在Vue中异步渲染实际上是在数据每次变化时,将其所要引起页面变化的部分都放到一个异步API的回调函数里,直到同步代码执行完之后,异步回调开始执行,最终将同步代码里所有的需要渲染变化的部分合并起来,最终执行一次渲染操作。
拿上面例子来说,当val第一次赋值时,页面会渲染出对应的文字,但是实际这个渲染变化会暂存,val第二次赋值时,再次暂存将要引起的变化,这些变化操作会被丢到异步API,Promise.then的回调函数中,等到所有同步代码执行完后,then函数的回调函数得到执行,然后将遍历存储着数据变化的全局数组,将所有数组里数据确定先后优先级,最终合并成一套需要展示到页面上的数据,执行页面渲染操作操作。
异步队列执行后,存储页面变化的全局数组得到遍历执行,执行的时候会进行一些筛查操作,将重复操作过的数据进行处理,实际就是先赋值的丢弃不渲染,最终按照优先级最终组合成一套数据渲染。
这里触发渲染的异步API优先考虑Promise,其次MutationObserver,如果没有MutationObserver的话,会考虑setImmediate,没有setImmediate的话最后考虑是setTimeout。
接下来在源码层面梳理一下的Vue的异步渲染过程。
接下来从源码角度一步一分析一下:
1、当我们使用this.val='343'赋值的时候,val属性所绑定的Object.defineProperty的setter函数触发,setter函数将所订阅的notify函数触发执行。
defineReactive(){...set:functionreactiveSetter(newVal){...dep.notify();...}...}2、notify函数中,将所有的订阅组件watcher中的update方法执行一遍。
Dep.prototype.notify=functionnotify(){//拷贝所有组件的watchervarsubs=this.subs.slice();...for(vari=0,l=subs.length;i Watcher.prototype.update=functionupdate(){if(this.lazy){this.dirty=true;}elseif(this.sync){this.run();}else{queueWatcher(this);}};4、queueWatcher函数里,会先将组件的watcher存进全局数组变量queue里。 默认情况下config.async是true,直接进入nextTick的函数执行,nextTick是一个浏览器异步API实现的方法,它的回调函数是flushSchedulerQueue函数。 functionqueueWatcher(watcher){...//在全局队列里存储将要响应的变化update函数queue.push(watcher);...//当async配置是false的时候,页面更新是同步的if(!config.async){flushSchedulerQueue();return}//将页面更新函数放进异步API里执行,同步代码执行完开始执行更新页面函数nextTick(flushSchedulerQueue);}5、nextTick函数的执行后,传入的flushSchedulerQueue函数又一次push进callbacks全局数组里,pending在初始情况下是false,这时候将触发timerFunc。 functionnextTick(cb,ctx){var_resolve;callbacks.push(function(){if(cb){try{cb.call(ctx);}catch(e){handleError(e,ctx,'nextTick');}}elseif(_resolve){_resolve(ctx);}});if(!pending){pending=true;timerFunc();}//$flow-disable-lineif(!cb&&typeofPromise!=='undefined'){returnnewPromise(function(resolve){_resolve=resolve;})}}6、timerFunc函数是由浏览器的Promise、MutationObserver、setImmediate、setTimeout这些异步API实现的,异步API的回调函数是flushCallbacks函数。 //将nextTick里push进去的flushSchedulerQueue函数进行for循环依次调用functionflushCallbacks(){pending=false;varcopies=callbacks.slice(0);callbacks.length=0;for(vari=0;i functionflushSchedulerQueue(){varwatcher,id;//安装id从小到大开始排序,越小的越前触发的updatequeue.sort(function(a,b){returna.id-b.id;});//queue是全局数组,它在queueWatcher函数里,每次update触发的时候将当时的watcher,push进去for(index=0;index Watcher.prototype.run=functionrun(){if(this.active){varvalue=this.get();...}};10、get函数中,将实例watcher对象push到全局数组中,开始调用实例的getter方法,执行完毕后,将watcher对象从全局数组弹出,并且清除已经渲染过的依赖实例。 Watcher.prototype.get=functionget(){pushTarget(this);//将实例push到全局数组targetStackvarvm=this.vm;value=this.getter.call(vm,vm);...}11、实例的getter方法实际是在实例化的时候传入的函数,也就是下面vm的真正更新函数_update。 function(){vm._update(vm._render(),hydrating);};12、实例的_update函数执行后,将会把两次的虚拟节点传入传入vm的patch方法执行渲染操作。 Vue.prototype._update=function(vnode,hydrating){varvm=this;...varprevVnode=vm._vnode;vm._vnode=vnode;if(!prevVnode){//initialrendervm.$el=vm.__patch__(vm.$el,vnode,hydrating,false/*removeOnly*/);}else{//updatesvm.$el=vm.__patch__(prevVnode,vnode);}...};四、nextTick实现原理首先nextTick并不是浏览器本身提供的一个异步API,而是Vue中使用由浏览器本身提供的原生异步API封装而成的一个异步封装方法,上面第5第6段是它的实现源码。它对于浏览器异步API的选用规则如下,Promise存在取由Promise.then,不存在Promise则取MutationObserver,MutationObserver不存在setImmediate,setImmediate不存在最后取setTimeout来实现。 从上面的取用规则也可以看出来,nextTick即有可能是微任务,也有可能是宏任务,从优先去Promise和MutationObserver可以看出nextTick优先微任务,其次是setImmediate和setTimeout宏任务。 对于微任务与宏任务的区别这里不深入,只要记得同步代码执行完毕之后,优先执行微任务,其次才会执行宏任务。 1、Vue.config.async=false 当然是可以的,在第四段源码里,我们能看到如下一段,当config里的async的值为为false的情况下,并没有将flushSchedulerQueue加到nextTick里,而是直接执行了flushSchedulerQueue,就相当于把本次data里的值变化时,页面做了同步渲染。 functionqueueWatcher(watcher){...//在全局队列里存储将要响应的变化update函数queue.push(watcher);...//当async配置是false的时候,页面更新是同步的if(!config.async){flushSchedulerQueue();return}//将页面更新函数放进异步API里执行,同步代码执行完开始执行更新页面函数nextTick(flushSchedulerQueue);}在我们的开发代码里,只需要加入下一句即可让你的页面渲染同步进行。 importVuefrom'Vue'Vue.config.async=false2、this._watcher.sync=true 在Watch的update方法执行源码里,可以看到当this.sync为true时,这时候的渲染也是同步的。 Watcher.prototype.update=functionupdate(){if(this.lazy){this.dirty=true;}elseif(this.sync){this.run();}else{queueWatcher(this);}};在开发代码中,需要将本次watcher的sync属性修改为true,对于watcher的sync属性变化只需要在需要同步渲染的数据变化操作前执行this._watcher.sync=true,这时候则会同步执行页面渲染动作。 像下面的写法中,页面会渲染出val为1,而不会渲染出2,最终渲染的结果是3,但是官网未推荐该用法,请慎用。 newVue({el:'#app',sync:true,template:' 异步渲染机制我们其实都知道,也很容易碰到,但是可能并不清楚这个概念及其原理。正好今天看到一个大厂面试题,提到异步渲染机制,好奇是个什么概念,发现其实也不是什么新东西,我们在写代码时也经常遇到,也是个很基础的东西吧,只是不大清楚这个概念,所以找了一些资料看了下。发现这篇开篇讲的挺通俗易懂,中间源码讲的也挺清晰的,所以转载过来共同学习。