深入解读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 相关文章推荐
JavaScript CSS 修改学习第四章 透明度设置
Feb 19 Javascript
ajax的hide隐藏问题解决方法
Dec 11 Javascript
禁止IE用右键的JS代码
Dec 30 Javascript
JavaScript实现生成GUID(全局统一标识符)
Sep 05 Javascript
JS+CSS实现的拖动分页效果实例
May 11 Javascript
AngularJS使用ng-options指令实现下拉框
Aug 23 Javascript
JS实现京东首页之页面顶部、Logo和搜索框功能
Jan 12 Javascript
详解vue 中使用 AJAX获取数据的方法
Jan 18 Javascript
webpack4 处理SCSS的方法示例
Sep 03 Javascript
Vue在页面数据渲染完成之后的调用方法
Sep 11 Javascript
jquery使用echarts实现有向图可视化功能示例
Nov 25 jQuery
vue 实现弹窗关闭后刷新效果
Apr 08 Vue.js
微信小程序报错: 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
一个取得文件扩展名的函数
2006/10/09 PHP
php获取汉字首字母的函数
2013/11/07 PHP
sae使用smarty模板的方法
2013/12/17 PHP
php获取发送给用户的header信息的方法
2015/03/16 PHP
关于PHP中协程和阻塞的一些理解与思考
2017/08/11 PHP
jquery.cvtooltip.js 基于jquery的气泡提示插件
2010/11/19 Javascript
jquery常用特效方法使用示例
2014/04/25 Javascript
JQuery以JSON方式提交数据到服务端示例代码
2014/05/05 Javascript
javascript基于HTML5 canvas制作画箭头组件
2014/06/25 Javascript
JavaScript实现找出数组中最长的连续数字序列
2014/09/03 Javascript
JavaScript将XML转成JSON的方法
2015/03/12 Javascript
jQuery实现鼠标划过添加和删除class的方法
2015/06/26 Javascript
理解javascript定时器中的setTimeout与setInterval
2016/02/23 Javascript
Angular2 (RC4) 路由与导航详解
2016/09/21 Javascript
Angularjs实现搜索关键字高亮显示效果
2017/01/17 Javascript
js绑定事件和解绑事件
2017/04/27 Javascript
利用vue + element实现表格分页和前端搜索的方法
2017/12/25 Javascript
vue开发中遇到的问题总结
2020/04/07 Javascript
Python中每次处理一个字符的5种方法
2015/05/21 Python
初步剖析C语言编程中的结构体
2016/01/16 Python
python输出100以内的质数与合数实例代码
2018/07/08 Python
python实现动态创建类的方法分析
2019/06/25 Python
Python List列表对象内置方法实例详解
2019/10/22 Python
pygame库实现移动底座弹球小游戏
2020/04/14 Python
用python制作个音乐下载器
2021/01/30 Python
python matplotlib工具栏源码探析三之添加、删除自定义工具项的案例详解
2021/02/25 Python
印度领先的眼镜电子商务网站:Lenskart
2019/12/16 全球购物
历史学专业毕业生求职信
2013/09/27 职场文书
四大名著读书笔记
2015/06/25 职场文书
2015年庆祝国庆节66周年演讲稿
2015/07/30 职场文书
2016年元旦致辞
2015/08/01 职场文书
财务年终工作总结大全
2019/06/20 职场文书
Vue3 Composition API的使用简介
2021/03/29 Vue.js
PyTorch的Debug指南
2021/05/07 Python
redis三种高可用方式部署的实现
2021/05/11 Redis
Pytorch中TensorBoard及torchsummary的使用详解
2021/05/12 Python