深入解读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 showModalDialog 内跳转页面的问题
Nov 25 Javascript
iframe 上下滚动条如何默认在下方实现原理
Dec 10 Javascript
简单实用的反馈表单无刷新提交带验证
Nov 15 Javascript
一个不错的字符串转码解码函数(自写)
Jul 31 Javascript
javascript无刷新评论实现方法
May 13 Javascript
jquery表单验证插件formValidator使用方法
Apr 01 Javascript
基于jquery插件编写countdown计时器
Jun 12 Javascript
jQuery实现带遮罩层效果的blockUI弹出层示例【附demo源码下载】
Sep 14 Javascript
Vue-Cli中自定义过滤器的实现代码
Aug 12 Javascript
详解基于mpvue的小程序markdown适配解决方案
May 08 Javascript
vue实现的组件兄弟间通信功能示例
Dec 04 Javascript
微信小程序自定义导航教程(兼容各种手机)
Dec 12 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 搜索框提示(自动完成)实例代码
2012/02/05 PHP
php中unserialize返回false的解决方法
2014/09/22 PHP
PHP共享内存用法实例分析
2016/02/12 PHP
yii 框架实现按天,月,年,自定义时间段统计数据的方法分析
2020/04/04 PHP
PHP Web表单生成器案例分析
2020/06/02 PHP
让ie运行js时提示允许阻止内容运行的解决方法
2010/10/24 Javascript
不同Jquery版本引发的问题解决
2013/10/14 Javascript
利用JS来控制键盘的上下左右键(示例代码)
2013/12/14 Javascript
jQuery基础知识小结
2014/12/22 Javascript
基于jQuery实现数字滚动效果
2017/01/16 Javascript
JavaScript基于Dom操作实现查找、修改HTML元素的内容及属性的方法
2017/01/20 Javascript
jquery在vue脚手架中的使用方式示例
2017/08/29 jQuery
基于VuePress 轻量级静态网站生成器的实现方法
2018/04/17 Javascript
基于ionic实现下拉刷新功能
2018/05/10 Javascript
webpack4 SplitChunks实现代码分隔详解
2019/05/23 Javascript
JavaScript中的this/call/apply/bind的使用及区别
2020/03/06 Javascript
Vue实现点击导航栏当前标签后变色功能
2020/08/19 Javascript
vue中是怎样监听数组变化的
2020/10/24 Javascript
[01:05]DOTA2完美大师赛趣味视频之选手教你打职业
2017/11/23 DOTA
Python中的作用域规则详解
2015/01/30 Python
常用python编程模板汇总
2016/02/12 Python
Python MySQL数据库连接池组件pymysqlpool详解
2017/07/07 Python
python gdal安装与简单使用
2019/08/01 Python
python为什么会环境变量设置不成功
2020/06/23 Python
Python用requests库爬取返回为空的解决办法
2021/02/21 Python
Mybag美国/加拿大:英国奢华包包和名牌手袋网站
2020/02/16 全球购物
请用用Java代码写一个堆栈
2012/01/26 面试题
幼儿园教育教学反思
2014/01/31 职场文书
银行类自荐信
2014/02/04 职场文书
文秘应聘自荐书范文
2014/02/18 职场文书
正风肃纪剖析材料范文
2014/10/10 职场文书
教师个人教学总结
2015/02/11 职场文书
传单、海报早OUT了,另类传单营销方案送给你!
2019/07/15 职场文书
详解Python中的进程和线程
2021/06/23 Python
vue项目多环境配置(.env)的实现
2021/07/21 Vue.js
Python+SeaTable实现计算两个日期间的工作日天数
2022/07/07 Python