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 相关文章推荐
javascript下string.format函数补充
Aug 24 Javascript
改进UCHOME的记录发布,增强可访问性用户体验
Jan 17 Javascript
js confirm()方法的使用方法实例
Jul 13 Javascript
js格式化金额可选是否带千分位以及保留精度
Jan 28 Javascript
jquery实现在页面加载完毕后获取图片高度或宽度
Jun 16 Javascript
JQuery表格拖动调整列宽效果(自己动手写的)
Sep 01 Javascript
Jquery基础教程之DOM操作
Aug 19 Javascript
jQuery实现监控页面所有ajax请求的方法
Dec 10 Javascript
jQuery+ajax读取并解析XML文件的方法
Sep 09 Javascript
JavaScript日期对象(Date)基本用法示例
Jan 18 Javascript
Vue学习之组件用法实例详解
Jan 06 Javascript
使用JavaScript获取扫码枪扫描得到的条形码的思路代码详解
Jun 10 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
PHP4实际应用经验篇(2)
2006/10/09 PHP
PHP基于数组实现的分页函数实例
2014/08/20 PHP
php魔法函数与魔法常量使用介绍
2017/07/23 PHP
PHP正则匹配到2个字符串之间的内容方法
2018/12/24 PHP
Jquery 动态添加按钮实现代码
2010/05/06 Javascript
jquery ajax 局部无刷新更新数据的实现案例
2014/02/08 Javascript
js中unicode转码方法详解
2015/10/09 Javascript
使用RequireJS库加载JavaScript模块的实例教程
2016/06/06 Javascript
xtemplate node.js 的使用方法实例解析
2016/08/22 Javascript
关于vue-resource报错450的解决方案
2017/07/24 Javascript
微信小程序自定义多选事件的实现代码
2018/05/17 Javascript
解决webpack+Vue引入iView找不到字体文件的问题
2018/09/28 Javascript
JS闭包经典实例详解
2018/12/20 Javascript
vue router总结 $router和$route及router与 router与route区别
2019/07/05 Javascript
swiper Scrollbar滚动条组件详解
2019/09/08 Javascript
JavaScript实现沿五角星形线摆动的小圆实例详解
2020/07/28 Javascript
Python 功能和特点(新手必学)
2015/12/30 Python
Python基础语法(Python基础知识点)
2016/02/28 Python
浅谈Python2获取中文文件名的编码问题
2018/01/09 Python
利用pyinstaller打包exe文件的基本教程
2019/05/02 Python
PyQt5重写QComboBox的鼠标点击事件方法
2019/06/25 Python
python读取Kafka实例
2019/12/23 Python
Python3-异步进程回调函数(callback())介绍
2020/05/02 Python
CSS3打造磨砂玻璃背景效果
2016/09/28 HTML / CSS
ProBikeKit英国:在线公路自行车之家
2017/02/10 全球购物
简述synchronized和java.util.concurrent.locks.Lock的异同
2014/12/08 面试题
后进生转化工作制度
2014/01/17 职场文书
十月份红领巾广播稿
2014/01/22 职场文书
离职证明标准格式
2014/09/15 职场文书
承德避暑山庄导游词
2015/02/03 职场文书
保送生自荐信范文
2015/03/26 职场文书
大学生活委员竞选稿
2015/11/21 职场文书
Win11安装受阻怎么办? Windows11安装问题与解决方案汇总
2021/11/21 数码科技
vue 把二维或多维数组转一维数组
2022/04/24 Vue.js
GPU服务器的多用户配置方法
2022/07/07 Servers
MySQL中dd::columns表结构转table过程及应用详解
2022/09/23 MySQL