Vue中$nextTick方法将回调延迟到下次DOM更新循环之后执行,也就是在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,能够获取更新后的DOM。简单来说就是当数据更新时,在DOM中渲染完成后,执行回调函数。

描述#

通过一个简单的例子来演示$nextTick方法的作用,首先需要知道Vue在更新DOM时是异步执行的,也就是说在更新数据时其不会阻塞代码的执行,直到执行栈中代码执行结束之后,才开始执行异步任务队列的代码,所以在数据更新时,组件不会立即渲染,此时在获取到DOM结构后取得的值依然是旧的值,而在$nextTick方法中设定的回调函数会在组件渲染完成之后执行,取得DOM结构后取得的值便是新的值。

Copy    Vue

异步机制#

官方文档中说明,Vue在更新DOM时是异步执行的,只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更,如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作是非常重要的。然后,在下一个的事件循环tick中,Vue刷新队列并执行实际工作。Vue在内部对异步队列尝试使用原生的Promise.then、MutationObserver和setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0)代替。Js是单线程的,其引入了同步阻塞与异步非阻塞的执行模式,在Js异步模式中维护了一个Event Loop,Event Loop是一个执行模型,在不同的地方有不同的实现,浏览器和NodeJS基于不同的技术实现了各自的Event Loop。浏览器的Event Loop是在HTML5的规范中明确定义,NodeJS的Event Loop是基于libuv实现的。在浏览器中的Event Loop由执行栈Execution Stack、后台线程Background Threads、宏队列Macrotask Queue、微队列Microtask Queue组成。

  • 执行栈就是在主线程执行同步任务的数据结构,函数调用形成了一个由若干帧组成的栈。
  • 后台线程就是浏览器实现对于setTimeout、setInterval、XMLHttpRequest等等的执行线程。
  • 宏队列,一些异步任务的回调会依次进入宏队列,等待后续被调用,包括setTimeout、setInterval、setImmediate(Node)、requestAnimationFrame、UI rendering、I/O等操作
  • 微队列,另一些异步任务的回调会依次进入微队列,等待后续调用,包括Promise、process.nextTick(Node)、Object.observe、MutationObserver等操作

当Js执行时,进行如下流程

  1. 首先将执行栈中代码同步执行,将这些代码中异步任务加入后台线程中
  2. 执行栈中的同步代码执行完毕后,执行栈清空,并开始扫描微队列
  3. 取出微队列队首任务,放入执行栈中执行,此时微队列是进行了出队操作
  4. 当执行栈执行完成后,继续出队微队列任务并执行,直到微队列任务全部执行完毕
  5. 最后一个微队列任务出队并进入执行栈后微队列中任务为空,当执行栈任务完成后,开始扫面微队列为空,继续扫描宏队列任务,宏队列出队,放入执行栈中执行,执行完毕后继续扫描微队列为空则扫描宏队列,出队执行
  6. 不断往复...

实例#

Copy// Step 1console.log(1);// Step 2setTimeout(() => {  console.log(2);  Promise.resolve().then(() => {    console.log(3);  });}, 0);// Step 3new Promise((resolve, reject) => {  console.log(4);  resolve();}).then(() => {  console.log(5);})// Step 4setTimeout(() => {  console.log(6);}, 0);// Step 5console.log(7);// Step N// ...// Result/*  1  4  7  5  2  3  6*/

Step 1#

Copy// 执行栈 console// 微队列 []// 宏队列 []console.log(1); // 1

Step 2#

Copy// 执行栈 setTimeout// 微队列 []// 宏队列 [setTimeout1]setTimeout(() => {  console.log(2);  Promise.resolve().then(() => {    console.log(3);  });}, 0);

Step 3#

Copy// 执行栈 Promise// 微队列 [then1]// 宏队列 [setTimeout1]new Promise((resolve, reject) => {  console.log(4); // 4 // Promise是个函数对象,此处是同步执行的 // 执行栈 Promise console  resolve();}).then(() => {  console.log(5);})

Step 4#

Copy// 执行栈 setTimeout// 微队列 [then1]// 宏队列 [setTimeout1 setTimeout2]setTimeout(() => {  console.log(6);}, 0);

Step 5#

Copy// 执行栈 console// 微队列 [then1]// 宏队列 [setTimeout1 setTimeout2]console.log(7); // 7

Step 6#

Copy// 执行栈 then1// 微队列 []// 宏队列 [setTimeout1 setTimeout2]console.log(5); // 5

Step 7#

Copy// 执行栈 setTimeout1// 微队列 [then2]// 宏队列 [setTimeout2]console.log(2); // 2Promise.resolve().then(() => {    console.log(3);});

Step 8#

Copy// 执行栈 then2// 微队列 []// 宏队列 [setTimeout2]console.log(3); // 3

Step 9#

Copy// 执行栈 setTimeout2// 微队列 []// 宏队列 []console.log(6); // 6

分析#

在了解异步任务的执行队列后,回到中$nextTick方法,当用户数据更新时,Vue将会维护一个缓冲队列,对于所有的更新数据将要进行的组件渲染与DOM操作进行一定的策略处理后加入缓冲队列,然后便会在$nextTick方法的执行队列中加入一个flushSchedulerQueue方法(这个方法将会触发在缓冲队列的所有回调的执行),然后将$nextTick方法的回调加入$nextTick方法中维护的执行队列,在异步挂载的执行队列触发时就会首先会首先执行flushSchedulerQueue方法来处理DOM渲染的任务,然后再去执行$nextTick方法构建的任务,这样就可以实现在$nextTick方法中取得已渲染完成的DOM结构。在测试的过程中发现了一个很有意思的现象,在上述例子中的加入两个按钮,在点击updateMsg按钮的结果是3 2 1,点击updateMsgTest按钮的运行结果是2 3 1。

Copy    Vue

这里假设运行环境中Promise对象是完全支持的,那么使用setTimeout是宏队列在最后执行这个是没有异议的,但是使用$nextTick方法以及自行定义的Promise实例是有执行顺序的问题的,虽然都是微队列任务,但是在Vue中具体实现的原因导致了执行顺序可能会有所不同,首先直接看一下$nextTick方法的源码,关键地方添加了注释,请注意这是Vue2.4.2版本的源码,在后期$nextTick方法可能有所变更。

Copy/** * Defer a task to execute it asynchronously. */var nextTick = (function () {  // 闭包 内部变量  var callbacks = []; // 执行队列  var pending = false; // 标识,用以判断在某个事件循环中是否为第一次加入,第一次加入的时候才触发异步执行的队列挂载  var timerFunc; // 以何种方法执行挂载异步执行队列,这里假设Promise是完全支持的  function nextTickHandler () { // 异步挂载的执行任务,触发时就已经正式准备开始执行异步任务了    pending = false; // 标识置false    var copies = callbacks.slice(0); // 创建副本    callbacks.length = 0; // 执行队列置空    for (var 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 */  if (typeof Promise !== 'undefined' && isNative(Promise)) {    var p = Promise.resolve();    var logError = function (err) { console.error(err); };    timerFunc = function () {      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    var counter = 1;    var observer = new MutationObserver(nextTickHandler);    var textNode = document.createTextNode(String(counter));    observer.observe(textNode, {      characterData: true    });    timerFunc = function () {      counter = (counter + 1) % 2;      textNode.data = String(counter);    };  } else {    // fallback to setTimeout    /* istanbul ignore next */    timerFunc = function () {      setTimeout(nextTickHandler, 0);    };  }  return function queueNextTick (cb, ctx) { // nextTick方法真正导出的方法    var _resolve;    callbacks.push(function () { // 添加到执行队列中 并加入异常处理      if (cb) {        try {          cb.call(ctx);        } catch (e) {          handleError(e, ctx, 'nextTick');        }      } else if (_resolve) {        _resolve(ctx);      }    });    //判断在当前事件循环中是否为第一次加入,若是第一次加入则置标识为true并执行timerFunc函数用以挂载执行队列到Promise    // 这个标识在执行队列中的任务将要执行时便置为false并创建执行队列的副本去运行执行队列中的任务,参见nextTickHandler函数的实现    // 在当前事件循环中置标识true并挂载,然后再次调用nextTick方法时只是将任务加入到执行队列中,直到挂载的异步任务触发,便置标识为false然后执行任务,再次调用nextTick方法时就是同样的执行方式然后不断如此往复    if (!pending) {       pending = true;      timerFunc();    }    if (!cb && typeof Promise !== 'undefined') {      return new Promise(function (resolve, reject) {        _resolve = resolve;      })    }  }})();

回到刚才提出的问题上,在更新DOM操作时会先触发$nextTick方法的回调,解决这个问题的关键在于谁先将异步任务挂载到Promise对象上。首先对有数据更新的updateMsg按钮触发的方法进行debug,断点设置在Vue.js的715行,版本为2.4.2,在查看调用栈以及传入的参数时可以观察到第一次执行$nextTick方法的其实是由于数据更新而调用的nextTick(flushSchedulerQueue);语句,也就是说在执行this.msg = "Update";的时候就已经触发了第一次的$nextTick方法,此时在$nextTick方法中的任务队列会首先将flushSchedulerQueue方法加入队列并挂载$nextTick方法的执行队列到Promise对象上,然后才是自行自定义的Promise.resolve().then(() => console.log(2))语句的挂载,当执行微任务队列中的任务时,首先会执行第一个挂载到Promise的任务,此时这个任务是运行执行队列,这个队列中有两个方法,首先会运行flushSchedulerQueue方法去触发组件的DOM渲染操作,然后再执行console.log(3),然后执行第二个微队列的任务也就是() => console.log(2),此时微任务队列清空,然后再去宏任务队列执行console.log(1)。接下来对于没有数据更新的updateMsgTest按钮触发的方法进行debug,断点设置在同样的位置,此时没有数据更新,那么第一次触发$nextTick方法的是自行定义的回调函数,那么此时$nextTick方法的执行队列才会被挂载到Promise对象上,很显然在此之前自行定义的输出2的Promise回调已经被挂载,那么对于这个按钮绑定的方法的执行流程便是首先执行console.log(2),然后执行$nextTick方法闭包的执行队列,此时执行队列中只有一个回调函数console.log(3),此时微任务队列清空,然后再去宏任务队列执行console.log(1)。简单来说就是谁先挂载Promise对象的问题,在调用$nextTick方法时就会将其闭包内部维护的执行队列挂载到Promise对象,在数据更新时Vue内部首先就会执行$nextTick方法,之后便将执行队列挂载到了Promise对象上,其实在明白Js的Event Loop模型后,将数据更新也看做一个$nextTick方法的调用,并且明白$nextTick方法会一次性执行所有推入的回调,就可以明白其执行顺序的问题了,下面是一个关于$nextTick方法的最小化的DEMO。

Copyvar nextTick = (function(){    var pending = false;    const callback = [];    var p = Promise.resolve();    var handler = function(){        pending = true;        callback.forEach(fn => fn());    }    var timerFunc = function(){        p.then(handler);    }    return function queueNextTick(fn){        callback.push(() => fn());        if(!pending){            pending = true;            timerFunc();        }    }})();(function(){    nextTick(() => console.log("触发DOM渲染队列的方法")); // 注释 / 取消注释 来查看效果    setTimeout(() => console.log(1))    Promise.resolve().then(() => console.log(2))    nextTick(() => {        console.log(3)    })})();
Copyvar nextTick = (function(){    var pending = false;    const callback = [];    var p = Promise.resolve();    var handler = function(){        pending = true;        callback.forEach(fn => fn());    }    var timerFunc = function(){        p.then(handler);    }    return function queueNextTick(fn){        callback.push(() => fn());        if(!pending){            pending = true;            timerFunc();        }    }})();(function(){    nextTick(() => console.log("触发DOM渲染队列的方法")); // 注释 / 取消注释 来查看效果    setTimeout(() => console.log(1))    Promise.resolve().then(() => console.log(2))    nextTick(() => {        console.log(3)    })})();

vue openlayer单击地图事件循环多次执行_Vue中$nextTick的理解相关推荐

  1. vue openlayer单击地图事件循环多次执行_VUE生命周期函数面试题

    什么是 vue 生命周期 vue生命周期是指vue实例对象从创建之初到销毁的过程,vue所有功能的实现都是围绕其生命周期进行的,在生命周期的不同阶段调用对应的钩子函数实现组件数据管理和DOM渲染两大重 ...

  2. vue openlayer单击地图事件循环多次执行_12道vue高频原理面试题,你能答出几道?

    前言 本文分享 12 道 vue 高频原理面试题,覆盖了 vue 核心实现原理,其实一个框架的实现原理一篇文章是不可能说完的,希望通过这 12 道问题,让读者对自己的 Vue 掌握程度有一定的认识(B ...

  3. vue+openlayer实现地图聚合效果和撒点效果

    前言: openlayer是目前我们gis常用的一款开源的,并且反馈都特别好的软件了,像之前的ol3,  风靡一时,地图实现也很简单,很实用,目前vue中使用地图也是非常多的,那么如果在vue中引入o ...

  4. js中的事件循环和宏任务和微任务的理解

    参考许多大神的文章,写下自己的理解 事件循环: 说到事件循环就得谈到js中同步和异步的执行顺序 1.javascript将任务分为同步任务和异步任务,同步任务放到主线中,异步函数首先进行回调函数注册. ...

  5. vue ref 绑定的事件需要移除吗_Vue易遗忘的基础复习(二)

    数据请求 Vue-resource请求 在Vue2.0之后已经被舍弃 2. fetch请求 因为传统 Ajax (指 XMLHttpRequest)存在一些令人头疼的问题:配置和调用方式非常混乱,而且 ...

  6. vue function (i)第一次点击不执行_vue下$nextTick及原理浅析

    vue->$nextTick 引用官方的话:为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) ex1 <div& ...

  7. 浏览器与node事件循环

    我们都知道在浏览器中由于dom操作,js是单线程的. 为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得 ...

  8. 如何跟面试官解释事件循环

    关于事件循环的问题面试官都尤其的偏爱,所以说准备面试如果不搞懂事件循环是非常危险的. 当面试官问你了解浏览器事件循环吗?这只是一个开始,接下来: 为什么js在浏览器中有事件循环机制 事件循环有哪些任务 ...

  9. 浏览器事件循环与node事件循环

    前言 最近看到一些关于 事件队列,浏览器执行机制的文章推荐,联想到很早以前遇到的一些面试题,才惊觉自己对这块一直都不怎么了解,借助这个机会好好记录一番.顺便感叹一句,阮一峰大神的 blog真是应有尽有 ...

最新文章

  1. 计算机word应用模块三,计算机应用基模块三.ppt
  2. c语言简单的模拟坐标,C语言模拟实现简单扫雷游戏
  3. centos下mysql多实例安装3306、3307实例(2014-10-15)
  4. 作者:丁伟(1972-),男,博士,中国联合网络通信有限公司网络技术研究院高级工程师。...
  5. 【2016年第5期】研究(国家自然科学基金项目成果集萃)导读
  6. 华为服务器型号2285,华为服务器2285磁盘阵列设置
  7. 被黑客NeoN“附体” QQ为推产品谎报漏洞
  8. 利用 VScode 对比两个文件差异
  9. C# UrlEncoding
  10. loadrunner脚本中关于httpCode401特殊情况
  11. python--循环绘制ERA5风场的空间分布图
  12. 硬件十万个为什么——运放篇(三)如何估算多级放大器的频宽
  13. 用户画像 | 标签数据存储之MySQL真实应用
  14. java一元多项式减法运算_一元多项式的表示及加减乘除运算
  15. php单例是什么意思,什么是PHP单例模式?
  16. 1800勘误表_专业的ASP.NET 2.0勘误表
  17. 数字图像隐藏图像的两种算法及实现代码
  18. 《鱿鱼游戏》全球大火,奈飞却难借IP变现成为“大赢家”
  19. API比较PostMessageA和SendMessageA游戏屏蔽
  20. grep多关键词查询

热门文章

  1. 马云获福布斯终身成就奖;华为推出首款 4G 芯片 Balong 711;PyPy 7.2 发布 | 极客头条...
  2. IBM 推出全球首台计算性能最强悍的 53 位量子计算机
  3. 谁说 C++ 的强制类型转换很难懂?
  4. 马化腾评 Facebook 加密货币;苹果或将 15% 产能移出中国;Python 新版发布 | 极客头条...
  5. 滴滴顺风车回归倒计时!
  6. 00 后的 AI 开发者进阶之道:从入门到鏖战 MIT 编程大赛 | 人物志
  7. 阿里云的工程师要被祭天了?
  8. 拆除指令怎么设置_张店区设置不合理隔离桩 将陆续拆除
  9. gan怎么输入一维数据_GAN评价指标最全汇总
  10. 干货来袭!java核心技术卷一pdf