Vue中之nextTick函数源码分析详解


Posted in Javascript onOctober 17, 2017

1. 什么是Vue.nextTick()?

官方文档解释如下:

在下次DOM更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。

2. 为什么要使用nextTick?

<!DOCTYPE html>
<html>
 <head>
 <title>演示Vue</title>
 <script src="https://tugenhua0707.github.io/vue/vue1/vue.js"></script>
 </head>
 <body>
 <div id="app">
  <template>
  <div ref="list">
   {{name}}
  </div>
  </template>
 </div>
 <script>
  new Vue({
  el: '#app',
  data: {
   name: 'aa'
  },
  mounted() {
   this.updateData();
  },
  methods: {
   updateData() {
   var self = this;
   this.name = 'bb';
   console.log(this.$el.textContent); // aa
   this.$nextTick(function(){
    console.log(self.$el.textContent); // bb
   });
   }
  }
  });
 </script>
 </body>
</html>

如上代码 在页面视图上显示bb,但是当我在控制台打印的时候,获取的文本内容还是 aa,但是使用 nextTick后,获取的文本内容就是最新的内容bb了,因此在这种情况下,我们可以使用nextTick函数了。

上面的代码为什么改变this.name = 'bb';后,再使用console.log(this.$el.textContent);打印的值还是aa呢?那是因为设置name的值后,DOM还没有更新到,所以获取值还是之前的值,但是我们放到nextTick函数里面的时候,代码会在DOM更新后执行,因此DOM更新后,再去获取元素的值就可以获取到最新值了。

理解DOM更新:在VUE中,当我们修改了data中的某一个值后,并不会立即反应到该el中,vue将对更改的数据放到watcher的一个异步队列中,只有在当前任务空闲时才会执行watcher队列任务,这就有一个延迟时间,因此放到 nextTick函数后就可以获取该el的最新值了。如果我们把上面的nextTick改成setTimeout也是可以的。

3. Vue源码详解之nextTick(源码在 vue/src/core/util/env.js)

在理解nextTick源码之前,我们先来理解下 html5中新增的 MutationObserver的API,它的作用是用来监听DOM变动的接口,它能监听一个dom对象发生的子节点删除,属性修改,文本内容修改等等。

nextTick源码如下:

export const nextTick = (function () {
 const callbacks = []
 let pending = false
 let timerFunc

 function nextTickHandler () {
 pending = false;
 /*
  之所以要slice复制一份出来是因为有的cb执行过程中又会往callbacks中加入内容,比如$nextTick的回调函数里又有$nextTick,
  那么这些应该放入到下一个轮次的nextTick去执行,所以拷贝一份,遍历完成即可,防止一直循环下去。
  */
 const copies = callbacks.slice(0)
 callbacks.length = 0
 for (let i = 0; i < copies.length; i++) {
  copies[i]()
 }
 }

 // the nextTick behavior leverages the microtask queue, which can be accessed
 // via either native Promise.then or MutationObserver.
 // MutationObserver has wider support, however it is seriously bugged in
 // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
 // completely stops working after triggering a few times... so, if native
 // Promise is available, we will use it:
 /* istanbul ignore if */
 /*
 nextTick行为利用了microtask队列, 先使用 Promise.resolve().then(nextTickHandler)来将异步回调
 放入到microtask中,Promise 和 MutationObserver都可以使用,但是 MutationObserver 在IOS9.3以上的
 WebView中有bug,因此如果满足第一项的话就可以执行,如果没有原生Promise就用 MutationObserver。
 */
 if (typeof Promise !== 'undefined' && isNative(Promise)) {
 var p = Promise.resolve()
 var logError = err => { console.error(err) }
 timerFunc = () => {
  p.then(nextTickHandler).catch(logError)
  // in problematic UIWebViews, Promise.then doesn't completely break, but
  // it can get stuck in a weird state where callbacks are pushed into the
  // microtask queue but the queue isn't being flushed, until the browser
  // needs to do some other work, e.g. handle a timer. Therefore we can
  // "force" the microtask queue to be flushed by adding an empty timer.
  if (isIOS) setTimeout(noop)
 }
 } else if (typeof MutationObserver !== 'undefined' && (
 isNative(MutationObserver) ||
 // PhantomJS and iOS 7.x
 MutationObserver.toString() === '[object MutationObserverConstructor]'
 )) {
 // use MutationObserver where native Promise is not available,
 // e.g. PhantomJS IE11, iOS7, Android 4.4
 /*
  创建一个MutationObserver,observe监听到DOM改动之后执行的回调 nextTickHandler 
  */
 var counter = 1
 var observer = new MutationObserver(nextTickHandler)
 var textNode = document.createTextNode(String(counter));
 // 使用MutationObserver的接口,监听文本节点的字符内容
 observer.observe(textNode, {
  characterData: true
 });
 /*
  每次执行timerFunc函数都会让文本节点的内容在0/1之间切换,切换之后将新赋值到那个我们MutationObserver监听的文本节点上去。
  */
 timerFunc = () => {
  counter = (counter + 1) % 2
  textNode.data = String(counter)
 }
 } else {
 // fallback to setTimeout
 /*
  如果上面的两种都不支持的话,我们就使用setTimeout来执行
  */
 timerFunc = () => {
  setTimeout(nextTickHandler, 0)
 }
 }

 return function queueNextTick (cb?: Function, ctx?: Object) {
 let _resolve
 callbacks.push(() => {
  if (cb) {
  try {
   cb.call(ctx)
  } catch (e) {
   handleError(e, ctx, 'nextTick')
  }
  } else if (_resolve) {
  _resolve(ctx)
  }
 });
 /* 如果pending为true,表明本轮事件循环中已经执行过 timerFunc(nextTickHandler, 0) */
 if (!pending) {
  pending = true
  timerFunc()
 }
 if (!cb && typeof Promise !== 'undefined') {
  return new Promise((resolve, reject) => {
  _resolve = resolve
  })
 }
 }
})()

整体思路理解:首先 nextTick 是一个闭包函数,代码立即执行,在理解整体代码之前,我们先来看个类似的demo,如下代码:

<!DOCTYPE html>
<html>
 <head>
 <title>演示Vue</title>
 </head>
 <body>
 <div id="app">
  
 </div>
 <script>
  var nextTick = (function(){
  return function queueNextTick(cb, ctx) {
   if (cb) {
   try {
    cb.call(ctx)
   } catch (e) {
    console.log('出错了');
   }
   }
  }
  })();

  // 方法调用
  nextTick(function(){
  console.log(2); // 打印2
  })
 </script>
 </body>
</html>

demo代码和上面的代码很类似。

我们也可以再来抽离使用nextTick做demo代码如下:

var nextTick2 = (function(){
 const callbacks = [];
 let pending = false;
 let timerFunc;

 function nextTickHandler () {
 pending = false
 const copies = callbacks.slice(0)
 callbacks.length = 0
 for (let i = 0; i < copies.length; i++) {
  copies[i]()
 }
 }
 if (typeof Promise !== 'undefined') {
 var p = Promise.resolve()
 var logError = err => { console.error(err) }
 timerFunc = () => {
  p.then(nextTickHandler).catch(logError)
 }
 } else if (typeof MutationObserver !== 'undefined' ||
 // PhantomJS and iOS 7.x
 MutationObserver.toString() === '[object MutationObserverConstructor]'
 ) {
 // use MutationObserver where native Promise is not available,
 // e.g. PhantomJS IE11, iOS7, Android 4.4
 var counter = 1
 var observer = new MutationObserver(nextTickHandler)
 var textNode = document.createTextNode(String(counter))
 observer.observe(textNode, {
  characterData: true
 })
 timerFunc = () => {
  counter = (counter + 1) % 2
  textNode.data = String(counter)
 }
 } else {
 // fallback to setTimeout
 /* istanbul ignore next */
 timerFunc = () => {
  setTimeout(nextTickHandler, 0)
 }
 }
 return function queueNextTick (cb, ctx) {
 let _resolve
 callbacks.push(() => {
  if (cb) {
  try {
   cb.call(ctx)
  } catch (e) {
   handleError(e, ctx, 'nextTick')
  }
  } else if (_resolve) {
  _resolve(ctx)
  }
 })
 if (!pending) {
  pending = true
  timerFunc()
 }
 if (!cb && typeof Promise !== 'undefined') {
  return new Promise((resolve, reject) => {
  _resolve = resolve
  })
 }
 }
})();
nextTick2(function(){
 console.log(2222);
});

如上代码是nextTick源码的抽离,为了更好的理解nextTick,做了如上的demo。

我们再来理解一下整体的代码的含义;

先定义数组 callbacks = [];来存放所有需要执行的回调函数,定义let pending = false;判断本轮事件是否执行过 timerFunc(nextTickHandler, 0)这个函数,为true说明执行过 timeFunc函数,接着定义nextTickHandler函数,该函数的作用是依次遍历数组callbacks保存的函数,依次执行;

请看源代码如下:

function nextTickHandler () {
 pending = false
 const copies = callbacks.slice(0)
 callbacks.length = 0
 for (let i = 0; i < copies.length; i++) {
 copies[i]()
 }
}

然后就是三个判断了,代码如下:

if (typeof Promise !== 'undefined' && isNative(Promise)) {
 var p = Promise.resolve();
 var logError = err => { console.error(err) }
 timerFunc = () => {
 p.then(nextTickHandler).catch(logError);
} else if (typeof MutationObserver !== 'undefined' && (
 isNative(MutationObserver) ||
 // PhantomJS and iOS 7.x
 MutationObserver.toString() === '[object MutationObserverConstructor]'
)){
 var counter = 1
 var observer = new MutationObserver(nextTickHandler)
 var textNode = document.createTextNode(String(counter))
 observer.observe(textNode, {
 characterData: true
 })
 timerFunc = () => {
 counter = (counter + 1) % 2
 textNode.data = String(counter)
 }
} else {
 timerFunc = () => {
 setTimeout(nextTickHandler, 0)
 }
}

首先判断是否支持Promise对象,如果支持的话,定义了timeFunc()函数,为了下一步调用做准备,然后继续判断是否支持该对象 MutationObserver,如果支持的话,创建一个文本节点,监听该节点数据是否发生改变,如果发生改变的话,调用timerFunc函数,counter值会在0/1切换,如果值改变了的话,把该数据值赋值到data属性上面去,那么data属性发生改变了,就会重新渲染页面(因为vue是通过Object.defineProperty来监听属性值是否发生改变),如果上面两种情况都不满足的话,那么直接使用setTimeout来执行nextTickHandler函数了;

最后nextTick代码返回一个函数,代码如下:

return function queueNextTick (cb?: Function, ctx?: Object) {
 let _resolve
 callbacks.push(() => {
 if (cb) {
  try {
  cb.call(ctx)
  } catch (e) {
  handleError(e, ctx, 'nextTick')
  }
 } else if (_resolve) {
  _resolve(ctx)
 }
 })
 if (!pending) {
 pending = true
 timerFunc()
 }
 if (!cb && typeof Promise !== 'undefined') {
 return new Promise((resolve, reject) => {
  _resolve = resolve
 })
 }
}

代码的含义是:传入的cb是否是函数,ctx参数是否是一个对象,如果cb是一个函数的话,使用cb.call(ctx), 如果timerFunc没有执行过的话,那么pending为false,因此执行 timerFunc()函数。基本的思路就是这样的。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JS打开新窗口的2种方式
Apr 18 Javascript
A标签触发onclick事件而不跳转的多种解决方法
Jun 27 Javascript
使用JavaScript获取地址栏参数的方法
Dec 19 Javascript
深入理解JavaScript系列(29):设计模式之装饰者模式详解
Mar 03 Javascript
jQuery实现自定义checkbox和radio样式
Jul 13 Javascript
jQueryUI Datepicker组件设置日期高亮
Oct 13 Javascript
浅析js的模块化编写 require.js
Dec 07 Javascript
微信小程序之picker日期和时间选择器
Feb 09 Javascript
gulp加批处理(.bat)实现ng多应用一键自动化构建
Feb 16 Javascript
详解js创建对象的几种方法及继承
Apr 12 Javascript
原生js实现贪吃蛇游戏
Oct 26 Javascript
记录一次websocket封装的过程
Nov 23 Javascript
vue mint-ui 实现省市区街道4级联动示例(仿淘宝京东收货地址4级联动)
Oct 16 #Javascript
jquery一键控制checkbox全选、反选或全不选
Oct 16 #jQuery
Vue2.0父子组件传递函数的教程详解
Oct 16 #Javascript
使用clipboard.js实现复制功能的示例代码
Oct 16 #Javascript
requireJS模块化实现返回顶部功能的方法详解
Oct 16 #Javascript
JavaScript简单实现合并两个Json对象的方法示例
Oct 16 #Javascript
seajs实现强制刷新本地缓存的方法分析
Oct 16 #Javascript
You might like
二次元帅气男生排行榜,只想悄悄收藏系列
2020/03/04 日漫
分享8个最佳的代码片段在线测试网站
2013/06/29 PHP
PHP判断指定时间段的2个方法
2014/03/14 PHP
反射调用private方法实践(php、java)
2015/12/21 PHP
php格式化json函数示例代码
2016/05/12 PHP
PHP载入图像imagecreatefrom_gif_jpeg_png系列函数用法分析
2016/11/14 PHP
PHP加MySQL消息队列深入理解
2021/02/27 PHP
javascript编程起步(第四课)
2007/01/10 Javascript
JS小功能(setInterval实现图片效果显示时间)实例代码
2013/11/28 Javascript
详解JavaScript对Date对象的操作问题(生成一个倒数7天的数组)
2015/10/01 Javascript
Bootstrap table分页问题汇总
2016/05/30 Javascript
js原生Ajax的封装和原理详解
2017/03/11 Javascript
react router 4.0以上的路由应用详解
2017/09/21 Javascript
jquery中有哪些api jQuery主要API
2017/11/20 jQuery
Angular实现的简单查询天气预报功能示例
2017/12/27 Javascript
使用Vue开发动态刷新Echarts组件的教程详解
2018/03/22 Javascript
JS逻辑运算符短路操作实例分析
2018/07/09 Javascript
vue 解决循环引用组件报错的问题
2018/09/06 Javascript
小程序开发踩坑:页面窗口定位(相对于浏览器定位)(推荐)
2019/04/25 Javascript
[01:46]DOTA2上海特锦赛小组赛英文解说KotlGuy采访
2016/02/27 DOTA
python实现简单的socket server实例
2015/04/29 Python
讲解Python中if语句的嵌套用法
2015/05/14 Python
Python实现的将文件每一列写入列表功能示例【测试可用】
2018/03/19 Python
Python 读取某个目录下所有的文件实例
2018/06/23 Python
widows下安装pycurl并利用pycurl请求https地址的方法
2018/10/15 Python
浅谈Python爬虫基本套路
2019/03/25 Python
浅谈JupyterNotebook导出pdf解决中文的问题
2020/04/22 Python
用 python 进行微信好友信息分析
2020/11/28 Python
canvas绘制圆角头像的实现方法
2019/01/17 HTML / CSS
英国领先的高街书籍专家:Waterstones
2018/02/01 全球购物
学习十八大报告感言
2014/02/28 职场文书
扩大国家免疫规划实施方案
2014/03/21 职场文书
党员自我评议个人对照检查材料
2014/09/16 职场文书
夫妻房产协议书的格式
2014/10/11 职场文书
承诺保证书格式
2015/02/28 职场文书
Pytorch反向传播中的细节-计算梯度时的默认累加操作
2021/06/05 Python