Vue中的nextTick涉及到Vue中DOM的异步更新,感觉很有意思,特意了解了一下。其中关于nextTick的源码涉及到不少知识,很多不太理解,暂且根据自己的一些感悟介绍下nextTick

一、示例

  先来一个示例了解下关于Vue中的DOM更新以及nextTick的作用。

  1. //模板
  2. <div class="app">
  3. <div ref="msgDiv">{{msg}}</div>
  4. <div v-if="msg1">Message got outside $nextTick: {{msg1}}</div>
  5. <div v-if="msg2">Message got inside $nextTick: {{msg2}}</div>
  6. <div v-if="msg3">Message got outside $nextTick: {{msg3}}</div>
  7. <button @click="changeMsg">
  8. Change the Message
  9. </button>
  10. </div>
  11.  
  12. //Vue实例
  13. new Vue({
  14. el: '.app',
  15. data: {
  16. msg: 'Hello Vue.',
  17. msg1: '',
  18. msg2: '',
  19. msg3: ''
  20. },
  21. methods: {
  22. changeMsg() {
  23. this.msg = "Hello world."
  24. this.msg1 = this.$refs.msgDiv.innerHTML
  25. this.$nextTick(() => {
  26. this.msg2 = this.$refs.msgDiv.innerHTML
  27. })
  28. this.msg3 = this.$refs.msgDiv.innerHTML
  29. }
  30. }
  31. })

  点击后:

  从图中可以得知:msg1和msg3显示的内容还是变换之前的,而msg2显示的内容是变换之后的。其根本原因是因为Vue中DOM更新是异步的(详细解释在后面)。

二、应用场景

  下面了解下nextTick的主要应用的场景及原因:

  1、在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中。

  因为在created()钩子函数执行的时候 DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted()钩子函数,因为该钩子函数执行时所有的DOM挂载和渲染都已完成,此时在该钩子函数中进行任何DOM操作都不会有问题 。

  2、在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作都应该放进Vue.nextTick()的回调函数中

  具体原因在Vue的官方文档中详细解释:

  Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的 Promise.thenMessageChannel,如果执行环境不支持,会采用 setTimeout(fn, 0)代替。

  例如,当你设置vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个“tick”更新。多数情况我们不需要关心这个过程,但是如果你想在 DOM 状态更新后做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员沿着“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们确实要这么做。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。

三、nextTick源码浅析

  作用:Vue.nextTick用于延迟执行一段代码,它接受2个参数(回调函数和执行回调函数的上下文环境),如果没有提供回调函数,那么将返回promise对象。

  源码

  1. export const nextTick = (function () {
  2. const callbacks = []
  3. let pending = false
  4. let timerFunc
  5.  
  6. function nextTickHandler () {
  7. pending = false;
  8. /*
  9. 之所以要slice复制一份出来是因为有的cb执行过程中又会往callbacks中加入内容,比如$nextTick的回调函数里又有$nextTick,
  10. 那么这些应该放入到下一个轮次的nextTick去执行,所以拷贝一份,遍历完成即可,防止一直循环下去。
  11. */
  12. const copies = callbacks.slice()
  13. callbacks.length =
  14. for (let i = ; i < copies.length; i++) {
  15. copies[i]()
  16. }
  17. }
  18.  
  19. // the nextTick behavior leverages the microtask queue, which can be accessed
  20. // via either native Promise.then or MutationObserver.
  21. // MutationObserver has wider support, however it is seriously bugged in
  22. // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
  23. // completely stops working after triggering a few times... so, if native
  24. // Promise is available, we will use it:
  25. /* istanbul ignore if */
  26. /*
  27. nextTick行为利用了microtask队列, 先使用 Promise.resolve().then(nextTickHandler)来将异步回调
  28. 放入到microtask中,Promise 和 MutationObserver都可以使用,但是 MutationObserver 在IOS9.3以上的
  29. WebView中有bug,因此如果满足第一项的话就可以执行,如果没有原生Promise就用 MutationObserver。
  30. */
  31. if (typeof Promise !== 'undefined' && isNative(Promise)) {
  32. var p = Promise.resolve()
  33. var logError = err => { console.error(err) }
  34. timerFunc = () => {
  35. p.then(nextTickHandler).catch(logError)
  36. // in problematic UIWebViews, Promise.then doesn't completely break, but
  37. // it can get stuck in a weird state where callbacks are pushed into the
  38. // microtask queue but the queue isn't being flushed, until the browser
  39. // needs to do some other work, e.g. handle a timer. Therefore we can
  40. // "force" the microtask queue to be flushed by adding an empty timer.
  41. if (isIOS) setTimeout(noop)
  42. }
  43. } else if (typeof MutationObserver !== 'undefined' && (
  44. isNative(MutationObserver) ||
  45. // PhantomJS and iOS 7.x
  46. MutationObserver.toString() === '[object MutationObserverConstructor]'
  47. )) {
  48. // use MutationObserver where native Promise is not available,
  49. // e.g. PhantomJS IE11, iOS7, Android 4.4
  50. /*
  51. 创建一个MutationObserver,observe监听到DOM改动之后执行的回调 nextTickHandler
  52. */
  53. var counter =
  54. var observer = new MutationObserver(nextTickHandler)
  55. var textNode = document.createTextNode(String(counter));
  56. // 使用MutationObserver的接口,监听文本节点的字符内容
  57. observer.observe(textNode, {
  58. characterData: true
  59. });
  60. /*
  61. 每次执行timerFunc函数都会让文本节点的内容在0/1之间切换,切换之后将新赋值到那个我们MutationObserver监听的文本节点上去。
  62. */
  63. timerFunc = () => {
  64. counter = (counter + ) %
  65. textNode.data = String(counter)
  66. }
  67. } else {
  68. // fallback to setTimeout
  69. /*
  70. 如果上面的两种都不支持的话,我们就使用setTimeout来执行
  71. */
  72. timerFunc = () => {
  73. setTimeout(nextTickHandler, )
  74. }
  75. }
  76.  
  77. return function queueNextTick (cb?: Function, ctx?: Object) {
  78. let _resolve
  79. callbacks.push(() => {
  80. if (cb) {
  81. try {
  82. cb.call(ctx)
  83. } catch (e) {
  84. handleError(e, ctx, 'nextTick')
  85. }
  86. } else if (_resolve) {
  87. _resolve(ctx)
  88. }
  89. });
  90. /* 如果pending为true,表明本轮事件循环中已经执行过 timerFunc(nextTickHandler, 0) */
  91. if (!pending) {
  92. pending = true
  93. timerFunc()
  94. }
  95. if (!cb && typeof Promise !== 'undefined') {
  96. return new Promise((resolve, reject) => {
  97. _resolve = resolve
  98. })
  99. }
  100. }
  101. })()

  首先,先了解nextTick中定义的三个重要变量。

  • callbacks:用来存储所有需要执行的回调函数
  • pending:用来标志是否正在执行回调函数
  • timerFunc:用来触发执行回调函数
  1. var callbacks = []; // 缓存函数的数组
  2. var pending = false; // 是否正在执行
  3. var timerFunc; // 保存着要执行的函数

  接下来,了解nextTickHandler()函数。

  1. function nextTickHandler () {
  2. pending = false;
  3. // 拷贝出函数数组副本
  4. var copies = callbacks.slice();
  5. // 把函数数组清空
  6. callbacks.length = ;
  7. // 依次执行函数
  8. for (var i = ; i < copies.length; i++) {
  9. copies[i]();
  10. }
  11. }

  这个函数就是$nextTick内实际调用的函数。

  接下来,是vue分了三种情况来延迟调用以上这个函数,因为$nextTick目的就是把传进来的函数延迟到dom更新后再使用,所以这里依次优雅降序的使用js的方法来做到这一点。

  1、先判断是否原生支持promise,如果支持,则利用promise来触发执行回调函数;

  1. if (typeof Promise !== 'undefined' && isNative(Promise)) {
  2. var p = Promise.resolve();
  3. var logError = function (err) { console.error(err); };
  4. timerFunc = function () {
  5. p.then(nextTickHandler).catch(logError);
  6. if (isIOS) { setTimeout(noop); }
  7. };
  8. }

  如果浏览器支持Promise,那么就用Promise.then的方式来延迟函数调用,Promise.then方法可以将函数延迟到当前函数调用栈最末端,也就是函数调用栈最后调用该函数。从而做到延迟。

  2、否则,如果支持MutationObserver,则实例化一个观察者对象,观察文本节点发生变化时,触发执行所有回调函数。

  1. else if (typeof MutationObserver !== 'undefined' && (
  2. isNative(MutationObserver) ||
  3. MutationObserver.toString() === '[object MutationObserverConstructor]'
  4. )) {
  5.  
  6. var counter = ;
  7. var observer = new MutationObserver(nextTickHandler);
  8. var textNode = document.createTextNode(String(counter));
  9. observer.observe(textNode, {
  10. characterData: true
  11. });
  12. timerFunc = function () {
  13. counter = (counter + ) % ;
  14. textNode.data = String(counter);
  15. };
  16. }

  MutationObserver是h5新加的一个功能,其功能是监听dom节点的变动,在所有dom变动完成后,执行回调函数。

  具体有以下几点变动的监听

  • childList:子元素的变动
  • attributes:属性的变动
  • characterData:节点内容或节点文本的变动
  • subtree:所有下属节点(包括子节点和子节点的子节点)的变动

  可以看出,以上代码是创建了一个文本节点,来改变文本节点的内容来触发的变动,因为我们在数据模型更新后,将会引起dom节点重新渲染,所以,我们加了这样一个变动监听,用一个文本节点的变动触发监听,等所有dom渲染完后,执行函数,达到我们延迟的效果。

  3、如果都不支持,则利用setTimeout设置延时为0

  1. else {
  2. timerFunc = function () {
  3. setTimeout(nextTickHandler, );
  4. };
  5. }

  利用setTimeout的延迟原理,setTimeout(func, 0)会将func函数延迟到下一次函数调用栈的开始,也就是当前函数执行完毕后再执行该函数,因此完成了延迟功能。

  最后是queueNextTick函数。因为nextTick是一个即时函数,所以queueNextTick函数是返回的函数,接受用户传入的参数,用来往callbacks里存入回调函数。

  1. return function queueNextTick (cb, ctx) {
  2. var _resolve;
  3. callbacks.push(function () {
  4. if (cb) { cb.call(ctx); }
  5. if (_resolve) { _resolve(ctx); }
  6. });
  7. // 如果没有函数队列在执行才执行
  8. if (!pending) {
  9. pending = true;
  10. timerFunc();
  11. }
  12. // promise化
  13. if (!cb && typeof Promise !== 'undefined') {
  14. console.log('进来了')
  15. return new Promise(function (resolve) {
  16. _resolve = resolve;
  17. })
  18. }
  19. }

  这个return的函数就是我们实际使用的闭包函数,每一次添加函数,都会想callbacks这个函数数组入栈。然后监听当前是否正在执行,如果没有,执行函数。这个很好理解。下面一个if是promise化。

  1. this.$nextTick(function () {
  2.  
  3. })
  4. // promise化
  5. this.$nextTick().then(function () {
  6.  
  7. }.bind(this))

  以上代码中第二种写法我们不常见把,直接调用$nextTick函数然后用promise格式去书写代码,不过这个then里面需要手动绑定this,vue内部没有给做处理。

  这就是一个this.$nextTick的实现,其中利用了优雅降序的巧妙手法,使代码尽可能优化。而且还提供了promise的写法,虽然我们不经常用,但是有总比没有好。

详解Vue中的nextTick的更多相关文章

  1. 详解Vue中的computed和watch

    作者:小土豆 博客园:https://www.cnblogs.com/HouJiao/ 掘金:https://juejin.cn/user/2436173500265335 1. 前言 作为一名Vue ...

  2. 详解Vue中watch的高级用法

    我们通过实例代码给大家分享了Vue中watch的高级用法,对此知识点有需要的朋友可以跟着学习下. 假设有如下代码: <div> <p>FullName: {{fullName} ...

  3. 详解Vue中的虚拟DOM

    摘要: 什么是虚拟DOM? 作者:浪里行舟 Fundebug经授权转载,版权归原作者所有. 前言 Vue.js 2.0引入Virtual DOM,比Vue.js 1.0的初始渲染速度提升了2-4倍,并 ...

  4. 详解Vue中的插槽

    作者: 小土豆 博客园:https://www.cnblogs.com/HouJiao/ 掘金:https://juejin.im/user/2436173500265335 什么是插槽 在日常的项目 ...

  5. 详解JavaScript中的原型

    前言 原型.原型链应该是被大多数前端er说烂的词,但是应该还有很多人不能完整的解释这两个内容,当然也包括我自己. 最早一篇原型链文章写于2019年07月,那个时候也是费了老大劲才理解到了七八成,到现在 ...

  6. Vue中之nextTick函数源码分析

    Vue中之nextTick函数源码分析 1. 什么是Vue.nextTick()?官方文档解释如下:在下次DOM更新循环结束之后执行的延迟回调.在修改数据之后立即使用这个方法,获取更新后的DOM. 2 ...

  7. 详解Vue.js 技术

    本文主要从8个章节详解vue技术揭秘,小编觉得挺有用的,分享给大家. 为了把 Vue.js 的源码讲明白,课程设计成由浅入深,分为核心.编译.扩展.生态四个方面去讲,并拆成了八个章节,如下: 准备工作 ...

  8. 详解vue的数据binding原理

    自从angular火了以后,各种mv*框架喷涌而出,angular虽然比较火,但是他的坑还是蛮多的,还有许多性能问题被人们吐槽.比如坑爹的脏检查机制,数据binding是受人喜爱的,脏检查就有点…性能 ...

  9. 详解Vue 方法与事件处理器

      本篇文章主要介绍了详解Vue 方法与事件处理器 ,小编觉得挺不错的,现在分享给大家,也给大家做个参考.一起跟随小编过来看看吧 方法与事件处理器 方法处理器 可以用 v-on 指令监听 DOM 事件 ...

随机推荐

  1. BZOJ2655 calc

    拉格朗日插值+dp 直接dp是n立方的,我们考虑优化. dp式子为f[i][j]=f[i-1][j-1]*j*i+f[i-1][j]表示i个元素选j个的答案 然后发现最高次就是2j次,所以我们预处理出 ...

  2. mount: unknown filesystem type 'vboxsf' centos ubuntu 处理方案

    Vagrant was unable to mount VirtualBox shared folders. This is usually because the filesystem " ...

  3. VMware vsphere Hypervisor、VMware vsphere和VMware Workstation小记

    VMware Workstation软件需要依赖于宿主操作系统之上. VMware vSphere是VMware公司推出一套服务器虚拟化解决方案,它是可以直接独立安装和运行在祼机上的系统. VMwar ...

  4. 【BZOJ-4016】最短路径树问题 Dijkstra + 点分治

    4016: [FJOI2014]最短路径树问题 Time Limit: 5 Sec  Memory Limit: 512 MBSubmit: 1092  Solved: 383[Submit][Sta ...

  5. linux 内核开发环境搭建

    http://blog.csdn.net/jemmy858585/article/details/46710299

  6. 推荐一个文献翻译软件--Deja Vu X

    首先我的这篇博客推荐的软件并非你觉得翻译精确度有多高的软件,假设是这种话就不用往下看了,免得浪费时间,仅仅是一个对于翻译文献非常方便的工具,方面在哪请看下文. 我是不会告诉你凡事用过这个软件的人都说好 ...

  7. 淘宝应对"双11"的技术架构分析

    原文地址:http://kb.cnblogs.com/page/193670/ 双“11”最热门的话题是TB ,最近正好和阿里的一个朋友聊淘宝的技术架构,发现很多有意思的地方,分享一下他们的解析资料: ...

  8. file is universal (4 slices) but does not contain a(n) armv7s slice

    关于ld: file is universal (2 slices) but does not contain a(n) armv7s slice 升级了xcode之后,支持iOS6和iPhone5, ...

  9. C#编程(三十八)----------运算符

    原文链接: http://blog.csdn.net/shanyongxu/article/details/46877353 运算符 类别 运算符 算术运算符 + - * / 逻辑运算符 &  ...

  10. Web安全测试漏洞场景

    HTTP.sys 远程代码执行   测试类型: 基础结构测试   威胁分类: 操作系统命令   原因: 未安装第三方产品的最新补丁或最新修订程序   安全性风险: 可能会在 Web 服务器上运行远程命 ...