深入解读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 相关文章推荐
form表单中去掉默认的enter键提交并绑定js方法实现代码
Apr 01 Javascript
浅析js中2个等号与3个等号的区别
Aug 06 Javascript
jQuery获取选中内容及设置元素属性的方法
Jul 09 Javascript
JavaScript弹出新窗口后向父窗口输出内容的方法
Apr 06 Javascript
jQuery学习笔记——jqGrid的使用记录(实现分页、搜索功能)
Nov 09 Javascript
微信JS-SDK自定义分享功能实例详解【分享给朋友/分享到朋友圈】
Nov 25 Javascript
js编写简单的聊天室功能
Aug 17 Javascript
jQuery条件分页 代替离线查询(附代码)
Aug 17 jQuery
微信小程序五子棋游戏的悔棋实现方法【附demo源码下载】
Feb 20 Javascript
JS实现滚动条触底加载更多
Sep 19 Javascript
JavaScript中变量提升机制示例详解
Dec 27 Javascript
vue实现简单加法计算器
Oct 22 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
php仿ZOL分页类代码
2008/10/02 PHP
PHP设计模式 注册表模式
2012/02/05 PHP
关于mysql字符集设置了character_set_client=binary 在gbk情况下会出现表描述是乱码的情况
2013/01/06 PHP
php定时计划任务与fsockopen持续进程实例
2014/05/23 PHP
PHP实现连接设备、通讯和发送命令的方法
2015/10/13 PHP
PHP判断json格式是否正确的实现代码
2017/09/20 PHP
php实现的中秋博饼游戏之绘制骰子图案功能示例
2017/11/06 PHP
jBox 2.3基于jquery的最新多功能对话框插件 常见使用问题解答
2011/11/10 Javascript
jQuery之日期选择器的深入解析
2013/06/19 Javascript
jquery选择符快速提取web表单数据示例
2014/03/27 Javascript
JS打开新窗口防止被浏览器阻止的方法
2015/01/03 Javascript
js时间戳转为日期格式的方法
2015/12/28 Javascript
关于JavaScript 原型链的一点个人理解
2016/07/31 Javascript
为Jquery EasyUI 组件加上清除功能的方法(详解)
2017/04/13 jQuery
ES6解构赋值的功能与用途实例分析
2017/10/31 Javascript
解决Linux无法正常安装与卸载Node.js的方法
2018/01/19 Javascript
ES6的解构赋值实例详解
2019/05/06 Javascript
详解Node.js异步处理的各种写法
2019/06/09 Javascript
vue的keep-alive用法技巧
2019/08/15 Javascript
探究Python中isalnum()方法的使用
2015/05/18 Python
python中如何使用正则表达式的集合字符示例
2017/10/09 Python
Python实现感知机(PLA)算法
2017/12/20 Python
python实现textrank关键词提取
2018/06/22 Python
Pandas读写CSV文件的方法示例
2019/03/27 Python
从列表或字典创建Pandas的DataFrame对象的方法
2019/07/06 Python
Django中使用CORS实现跨域请求过程解析
2019/08/05 Python
pytorch查看通道数 维数 尺寸大小方式
2020/05/26 Python
Python面向对象实现方法总结
2020/08/12 Python
详解Css3新特性应用之过渡与动画
2017/01/10 HTML / CSS
简易离婚协议书范本2014
2014/10/15 职场文书
客户答谢会致辞
2015/01/20 职场文书
农业项目投资意向书
2015/05/09 职场文书
Java使用jmeter进行压力测试
2021/07/09 Java/Android
MySQL修炼之联结与集合浅析
2021/10/05 MySQL
JavaScript函数柯里化
2021/11/07 Javascript
用PYTHON去计算88键钢琴的琴键频率和音高
2022/04/10 Python