深入解读VUE中的异步渲染的实现


Posted in Javascript onJune 19, 2020

接下来在本文里一起看看当数据变化时,从源码层面逐步分析一下触发页面的响应动作之后,如何做渲染到页面上,展示到用户层面的。

同时也会了解在Vue中的异步方法NextTick的源码实现,看一看NextTick方法与浏览器的异步API有何联系。

注意,本文涉及的Vue源码版本为2.6.11。

什么是异步渲染?

这个问题应该先要做一个前提补充,当数据在同步变化的时候,页面订阅的响应操作为什么不会与数据变化完全对应,而是在所有的数据变化操作做完之后,页面才会得到响应,完成页面渲染。

从一个例子体验一下异步渲染机制。

import Vue from 'Vue'
new Vue({
 el: '#app',
 template: '<div>{{val}}</div>', 
 data () {  return {   val: 'init'  } },
 mounted () { 
  this.val = '我是第一次页面渲染'  // debugger  
  this.val = '我是第二次页面渲染'  
  const st = Date.now()  
  while(Date.now() - st < 3000) {} }})

上面这一段代码中,在mounted里给val属性进行了两次赋值,如果页面渲染与数据的变化完全同步的话,页面应该是在mounted里有两次渲染。

而由于Vue内部的渲染机制,实际上页面只会渲染一次,把第一次的赋值所带来的的响应与第二次的赋值所带来的的响应进行一次合并,将最终的val只做一次页面渲染。

而且页面是在执行所有的同步代码执行完后才能得到渲染,在上述例子里的while阻塞代码之后,页面才会得到渲染,就像在熟悉的setTimeout里的回调函数的执行一样,这就是的异步渲染。

熟悉React的同学,应该很快能想到多次执行setState函数时,页面render的渲染触发,实际上与上面所说的Vue的异步渲染有异曲同工之妙。

Vue为什么要异步渲染?

我们可以从用户和性能两个角度来探讨这个问题。

从用户体验角度,从上面例子里便也可以看出,实际上我们的页面只需要展示第二次的值变化,第一次只是一个中间值,如果渲染后给用户展示,页面会有闪烁效果,反而会造成不好的用户体验。

从性能角度,例子里最终的需要展示的数据其实就是第二次给val赋的值,如果第一次赋值也需要页面渲染则意味着在第二次最终的结果渲染之前页面还需要渲染一次无用的渲染,无疑增加了性能的消耗。

对于浏览器来说,在数据变化下,无论是引起的重绘渲染还是重排渲染,都有可能会在性能消耗之下造成低效的页面性能,甚至造成加载卡顿问题。

异步渲染和熟悉的节流函数最终目的是一致的,将多次数据变化所引起的响应变化收集后合并成一次页面渲染,从而更合理的利用机器资源,提升性能与用户体验。

Vue中如何实现异步渲染?

先总结一下原理,在Vue中异步渲染实际在数据每次变化时,将其所要引起页面变化的部分都放到一个异步API的回调函数里,直到同步代码执行完之后,异步回调开始执行,最终将同步代码里所有的需要渲染变化的部分合并起来,最终执行一次渲染操作。

拿上面例子来说,当val第一次赋值时,页面会渲染出对应的文字,但是实际这个渲染变化会暂存,val第二次赋值时,再次暂存将要引起的变化,这些变化操作会被丢到异步API,Promise.then的回调函数中,等到所有同步代码执行完后,then函数的回调函数得到执行,然后将遍历存储着数据变化的全局数组,将所有数组里数据确定先后优先级,最终合并成一套需要展示到页面上的数据,执行页面渲染操作操作。

异步队列执行后,存储页面变化的全局数组得到遍历执行,执行的时候会进行一些筛查操作,将重复操作过的数据进行处理,实际就是先赋值的丢弃不渲染,最终按照优先级最终组合成一套数据渲染。

这里触发渲染的异步API优先考虑Promise,其次MutationObserver,如果没有MutationObserver的话,会考虑setImmediate,没有setImmediate的话最后考虑是setTimeout。

接下来在源码层面梳理一下的Vue的异步渲染过程。

深入解读VUE中的异步渲染的实现

接下来从源码角度一步一分析一下。

1、当我们使用this.val='343'赋值的时候,val属性所绑定的Object.defineProperty的setter函数触发,setter函数将所订阅的notify函数触发执行。

defineReactive() { 
 ... set: function reactiveSetter (newVal) { 
  ...  dep.notify(); 
 ... } 
 ...}

2、notify函数中,将所有的订阅组件watcher中的update方法执行一遍。

Dep.prototype.notify = function notify () { 
 // 拷贝所有组件的watcher var subs = this.subs.slice(); 
 ... for (var i = 0, l = subs.length; i < l; i++) {
  subs[i].update(); }};

深入解读VUE中的异步渲染的实现

3、update函数得到执行后,默认情况下lazy是false,sync也是false,直接进入把所有响应变化存储进全局数组queueWatcher函数下。

Watcher.prototype.update = function update () {
 if (this.lazy) {
  this.dirty = true;
 } else if (this.sync) {
  this.run(); }
 else { 
  queueWatcher(this); }};

深入解读VUE中的异步渲染的实现

4、queueWatcher函数里,会先将组件的watcher存进全局数组变量queue里。默认情况下config.async是true,直接进入nextTick的函数执行,nextTick是一个浏览器异步API实现的方法,它的回调函数是flushSchedulerQueue函数。

function queueWatcher (watcher) { 
... // 在全局队列里存储将要响应的变化update函数 queue.push(watcher); 
 ... // 当async配置是false的时候,页面更新是同步的 
 if (!config.async) { 
  flushSchedulerQueue();  return } 
// 将页面更新函数放进异步API里执行,同步代码执行完开始执行更新页面函数
 nextTick(flushSchedulerQueue);}

深入解读VUE中的异步渲染的实现

5、nextTick函数的执行后,传入的flushSchedulerQueue函数又一次push进callbacks全局数组里,pending在初始情况下是false,这时候将触发timerFunc。

function nextTick (cb, ctx) { 
 var _resolve; callbacks.push(function () { 
  if (cb) { 
   try { 
   cb.call(ctx); 
  } 
catch (e) { 
   handleError(e, ctx, 'nextTick'); 
   } 
 } else if (_resolve) { 
  _resolve(ctx);  } }); 
 if (!pending) { 
 pending = true;  timerFunc(); } // $flow-disable-line 
 if (!cb && typeof Promise !== 'undefined') { 
 return new Promise(function (resolve) {   _resolve = resolve;  }) }}

6、timerFunc函数是由浏览器的Promise、MutationObserver、setImmediate、setTimeout这些异步API实现的,异步API的回调函数是flushCallbacks函数。

var timerFunc;// 这里Vue内部对于异步API的选用,
由Promise、MutationObserver、setImmediate、setTimeout里取一个//
 取用的规则是 Promise存在取由Promise,不存在取MutationObserver,
MutationObserver不存在setImmediate,// setImmediate不存在setTimeout。
if (typeof Promise !== 'undefined' && isNative(Promise)) {
 var p = Promise.resolve(); timerFunc = function () { 
  p.then(flushCallbacks);  
 if (isIOS) { 
   setTimeout(noop);  } }; 
 isUsingMicroTask = true;
} 
  else if (!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) ||                              
  // PhantomJS and iOS 7.x                                 
  MutationObserver.toString() === '[object MutationObserverConstructor]')) 
       { 
	    var counter = 1; 
		var observer = new MutationObserver(flushCallbacks);      
		var textNode = document.createTextNode(String(counter)); 
		observer.observe(textNode, {characterData: true}); 
		timerFunc = function () {  
		    counter = (counter + 1) % 2;  
			textNode.data = String(counter); 
		}; 
  isUsingMicroTask = true;
 } 
  else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { 
         timerFunc = function () {  
		              setImmediate(flushCallbacks); 
			         };
		
         } 
		 else { timerFunc = function () 
		    {  
			setTimeout(flushCallbacks, 0); 

};
}

7、flushCallbacks函数中将遍历执行nextTick里push的callback全局数组,全局callback数组中实际是第5步的push的flushSchedulerQueue的执行函数。

// 将nextTick里push进去的flushSchedulerQueue函数进行for循环依次调用
function flushCallbacks () { 
 pending = false; 
 var copies = callbacks.slice(0); 
 callbacks.length = 0; 
 for (var i = 0; i < copies.length; i++) {  copies[i](); }}

8、callback遍历执行的flushSchedulerQueue函数中,flushSchedulerQueue里先按照id进行了优先级排序,接下来将第4步中的存储watcher对象全局queue遍历执行,触发渲染函数watcher.run。

function flushSchedulerQueue () {
var watcher, id;// 安装id从小到大开始排序,
越小的越前触发的updatequeue.sort(function (a, b) { 
return a.id - b.id; });// queue是全局数组,它在queueWatcher函数里,
每次update触发的时候将当时的watcher,push进去 for (index = 0; index < queue.length; index++) { 
  ...  watcher.run(); // 渲染  ... }}

9、watcher.run的实现在构造函数Watcher原型链上,初始状态下active属性为true,直接执行Watcher原型链的set方法。

Watcher.prototype.run = function run () {
 if (this.active) {  var value = this.get();  ... }};

10、get函数中,将实例watcher对象push到全局数组中,开始调用实例的getter方法,执行完毕后,将watcher对象从全局数组弹出,并且清除已经渲染过的依赖实例。

Watcher.prototype.get = function get () { 
 pushTarget(this); 
 // 将实例push到全局数组targetStack 
 var vm = 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) { 
 var vm = this; 
 ... var prevVnode = vm._vnode;
 vm._vnode = vnode;
 if (!prevVnode) { 
  // initial render  
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */); 
 } else {  
  // updates  
  vm.$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宏任务。

对于微任务与宏任务的区别这里不深入,只要记得同步代码执行完毕之后,优先执行微任务,其次才会执行宏任务。

Vue能不能同步渲染?

1、 Vue.config.async = false

当然是可以的,在第四段源码里,我们能看到如下一段,当config里的async的值为为false的情况下,并没有将flushSchedulerQueue加到nextTick里,而是直接执行了flushSchedulerQueue,就相当于把本次data里的值变化时,页面做了同步渲染。

function queueWatcher (watcher) { 
 ... // 在全局队列里存储将要响应的变化update函数 queue.push(watcher); 
 ... // 当async配置是false的时候,页面更新是同步的 
 if (!config.async) { 
  flushSchedulerQueue(); 
  return } // 将页面更新函数放进异步API里执行,同步代码执行完开始执行更新页面函数 
 nextTick(flushSchedulerQueue);}

在我们的开发代码里,只需要加入下一句即可让你的页面渲染同步进行。

import Vue from 'Vue'Vue.config.async = false

2、this._watcher.sync = true

在Watch的update方法执行源码里,可以看到当this.sync为true时,这时候的渲染也是同步的。

Watcher.prototype.update = function update () { 
 if (this.lazy) { 
 this.dirty = true; 
} else if (this.sync) { 
 this.run(); 
} else {  queueWatcher(this); }};

在开发代码中,需要将本次watcher的sync属性修改为true,对于watcher的sync属性变化只需要在需要同步渲染的数据变化操作前执行this._watcher.sync=true,这时候则会同步执行页面渲染动作。

像下面的写法中,页面会渲染出val为1,而不会渲染出2,最终渲染的结果是3,但是官网未推荐该用法,请慎用。

new Vue({ 
 el: '#app',
 sync: true, 
template: '<div>{{val}}</div>', 
 data () {  return { val: 0 } }, 
 mounted () { 
  this._watcher.sync = true 
 this.val = 1  debugger  
 this._watcher.sync = false 
 this.val = 2  this.val = 3 }})

总结

本文中介绍了Vue中为什么采用异步渲染页面的原因,并且从源码的角度深入剖析了整个渲染前的操作链路,同时剖析出Vue中的异步方法nextTick的实现与原生的异步API直接的联系。最后也从源码角度下了解到,Vue并非不能同步渲染,当我们的页面中需要同步渲染时,做适当的配置即可满足。

References

[1] https://github.com/vuejs/vue

[2] https://cn.vuejs.org/

到此这篇关于深入解读VUE中的异步渲染的实现的文章就介绍到这了,更多相关深入解读VUE中的异步渲染内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
起点页面传值js,有空研究学习下
Jan 25 Javascript
extjs 的权限问题 要求控制的对象是 菜单,按钮,URL
Mar 09 Javascript
JQuery 选择和过滤方法代码总结
Nov 19 Javascript
jquery animate图片模向滑动示例代码
Jan 26 Javascript
JQuery删除DOM节点的方法
Jun 11 Javascript
利用Angularjs实现幻灯片效果
Sep 07 Javascript
Angular中使用$watch监听object属性值的变化(详解)
Apr 24 Javascript
JS匿名函数和匿名自执行函数概念与用法分析
Mar 16 Javascript
Vue中$refs的用法详解
Jun 24 Javascript
react+ant design实现Table的增、删、改的示例代码
Dec 27 Javascript
浅谈Javascript中的对象和继承
Apr 19 Javascript
react-intl实现React国际化多语言的方法
Sep 27 Javascript
微信小程序报错: thirdScriptError的错误问题
Jun 19 #Javascript
微信小程序收藏功能的实现代码
Jun 19 #Javascript
微信小程序实现搜索框功能及踩过的坑
Jun 19 #Javascript
微信小程序返回上一级页面的实现代码
Jun 19 #Javascript
小程序中的箭头函数的具体使用
Jun 19 #Javascript
在VUE style中使用data中的变量的方法
Jun 19 #Javascript
深入分析JavaScript 事件循环(Event Loop)
Jun 19 #Javascript
You might like
德生PL450的电路分析和低放电路的改进办法
2021/03/02 无线电
PHP5中MVC结构学习
2006/10/09 PHP
php代码收集表单内容并写入文件的代码
2012/01/29 PHP
PHP实现的汉字拼音转换和公历农历转换类及使用示例
2014/07/01 PHP
php结合web uploader插件实现分片上传文件
2016/05/10 PHP
php获取网站根目录物理路径的几种方法(推荐)
2017/03/04 PHP
prototype 1.5 &amp; scriptaculous 1.6.1 学习笔记
2006/09/07 Javascript
解决IE下select标签innerHTML插入option的BUG(兼容IE,FF,Opera,Chrome,Safari)
2010/05/13 Javascript
用JQuery实现表格隔行变色和突出显示当前行的代码
2012/02/10 Javascript
javascript读写XML实现广告轮换(兼容IE、FF)
2013/08/09 Javascript
Javascript中拼接大量字符串的方法
2015/02/05 Javascript
基于PHP和Mysql相结合使用jqGrid读取数据并显示
2015/12/02 Javascript
第一次接触神奇的Bootstrap网格系统
2016/07/27 Javascript
Angular2库初探
2017/03/01 Javascript
详解vue几种主动刷新的方法总结
2019/02/19 Javascript
Element实现表格分页数据选择+全选所有完善批量操作
2019/06/07 Javascript
echarts实现获取datazoom的起始值(包括x轴和y轴)
2020/07/20 Javascript
javascript实现倒计时关闭广告
2021/02/09 Javascript
[44:09]DOTA2上海特级锦标赛A组小组赛#1 EHOME VS MVP.Phx第二局
2016/02/25 DOTA
python实现系统状态监测和故障转移实例方法
2013/11/18 Python
python学习手册中的python多态示例代码
2014/01/21 Python
Python访问MongoDB,并且转换成Dataframe的方法
2018/10/15 Python
Python面向对象程序设计类的封装与继承用法示例
2019/04/12 Python
python简单实现矩阵的乘,加,转置和逆运算示例
2019/07/10 Python
python实现淘宝购物系统
2019/10/25 Python
python实现图像全景拼接
2020/03/27 Python
HTML5的postMessage的使用手册
2018/12/19 HTML / CSS
俄语地区最大的中国商品在线购物网站之一:Umka Mall
2019/11/03 全球购物
经典婚礼主持词
2014/03/13 职场文书
材料成型及控制工程专业求职信
2014/06/19 职场文书
卫生院艾滋病宣传活动小结
2014/07/09 职场文书
赞助商致辞
2015/07/30 职场文书
情感电台广播稿
2015/08/18 职场文书
先进基层党组织事迹材料2016
2016/02/29 职场文书
Python实现单例模式的5种方法
2021/06/15 Python
JS前端可视化canvas动画原理及其推导实现
2022/08/05 Javascript