温故而知新,浅析 Vue nextTick 原理 |8月更文挑战

简介: nextTick 本质就是执行延迟回调的钩子,接受一个回调函数作为参数,在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。Vue 2.1.0 开始,如果没有提供回调函数,且在支持 Promise 的环境中,则返回一个 Promise 。注意 Vue 本身是不自带 polyfill 的,如果环境不支持 Promise ,则需要自己提供 polyfill。

网络异常,图片无法展示
|


网络异常,图片无法展示
|


nextTick 是什么?


nextTick 本质就是执行延迟回调的钩子,接受一个回调函数作为参数,在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。Vue 2.1.0 开始,如果没有提供回调函数,且在支持 Promise 的环境中,则返回一个 Promise 。注意 Vue 本身是不自带 polyfill 的,如果环境不支持 Promise ,则需要自己提供 polyfill。


nextTick 的作用


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


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

nextTick 除了让我们可以在 DOM 更新之后执行延迟回调,还有一个作用就是 Vue 内部 使用nextTick,把渲染 Dom 操作这个操作 放入到 callbacks 中。


nextTick 为什么是 next tick?


从字面意思理解,next 下一个,tick 滴答(钟表)来源于定时器的周期性中断(输出脉冲),一次中断表示一个 tick,也被称做一个“时钟滴答”,nextTick 顾名思义就是下一个时钟滴答,下一个任务。下一个任务,在 Event Loop 中在熟悉不过了通过一个例子简单回忆一下 Event Loop。


console.log('同步代码1');
setTimeout(() => {
    console.log('setTimeout')
}, 0)
new Promise((resolve) => {
  console.log('同步代码2')
  resolve()
}).then(() => {
    console.log('promise.then')
})
console.log('同步代码3');
// 最终输出"同步代码1"、"同步代码2"、"同步代码3"、"promise.then"、"setTimeout"

网络异常,图片无法展示
|

网络异常,图片无法展示
|


了解了浏览器的事件循环机制之后,我们回头来看 Vue nextTick。 nextTick 是下次DOM更新循环结束后执行延迟回调。


网络异常,图片无法展示
|


第一个 tick(图例中第一个步骤,即'本次更新循环')


  • 首先修改数据,这是同步任务。同一事件循环的所有的同步任务都在主线程上执行,形成一个执行栈,此时还未涉及 DOM 。
  • Vue 开启一个异步队列,并缓冲在此事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。
  • 同步任务在主程执行,这是第一个task。


第二个 tick(图例中第二个步骤,即'下次更新循环')


  • ue 在内部尝试对异步队列使用原生的 Promise.then 和 MO,如果执行环境不支持,会采用setImmediate或者是setTimeout(fn, 0) 代替(不同Vue 版本 API 不一样)。
  • DOM 更新是第二个 task 。


第三个 tick(图例中第三个步骤)


  • 当DOM 更新循环结束之后,此时调用下一个task执行,也就是nextTick中注册的延迟回调 。$nextTick 其实和第二个 task 是一样的操作,但是属于不同的 task。
  • 也就是第三个task。


nextTick 原理


nextTick 的原理,用一句话总结就是『利用 Event loop 事件线程去异步操作』。本质上就是注册异步任务来对任务进行处理。不同的是,在Vue 的不同版本对这个异步任务的优雅降级不太一样。


一个例子

<div id="example">
   <span>{{test}}</span>
   <button @click="handleClick">change</button>
</div>
var vm = new Vue({
  el: '#example',
  data: { 
    test: 'begin',
  },
  methods: {
    handleClick: function() {
      this.test = 1;
      console.log('script')
      this.$nextTick(function () { 
        console.log('nextTick')
      });
      Promise.resolve().then(function () {
        console.log('promise')
      })
    }
  }
});


Vue 2.4 输出 script、nextTick、promise。nextTick 执行顺序的。测试源码:链接

Vue 2.5+ 中,这段代码的输出顺序是 script、promise、nextTick。测试源码:链接

Vue 2.6+ 中,输出 script、nextTick、promise。虽然这里和 Vue2.4 输出一致,但是内

部实现不太一样,后面会讲到。


注意:这里输出的顺序并不是唯一的,还和 API 兼容性有关系。


看看源码,原来很简单

网络异常,图片无法展示
|


nextTick 的源码很简单(示例截图:2.6.11,2.x 版本这里差别不大,差别在于 timerFunc 的包装,后面会讲到),就只有几行代码。大致步骤如下:

网络异常,图片无法展示
|


通过数组 callbacks 来存储用户注册的回调。声明了变量 pending 来标记是否正在执行任务。这里使用一个异步锁,等待任务队列执行完毕之后,在执行下一个任务。当前任务队列正常进行时,将 pending 设置为 true,每当任务被执行完成时将 pending 设置为 false,这样就可以通过 pending 的值来判断当前的任务队列是否在执行,新来的任务是否需要放到下一次的任务队列中。在当前的队列中,执行函数 flushCallbacks。当这个函数被触发时,会将 callbacks 中的所有函数依次执行,然后清空 callbacks,并将 pending 设置为 false。即一轮事件循环中,flushCallbacks 只会执行一次。这里需要注意,执行 flushCallbacks 函数时备份回调函数队列。因为,会出现这么一种情况


nextTick 的回调函数中还使用 nextTick。如果 flushCallbacks 不做特殊处理,直接循环执行回调函数,会导致里面nextTick 中的回调函数会直接进入回调队列。

网络异常,图片无法展示
|


nextTick timerFunc 进化史


网络异常,图片无法展示
|


2.4 版本之前


从 Vue 的 git 上拉取了2.0版本之后关于 timerFunc 的包装,发现在 2.4 版本之前包装都是一样的。优雅降级方案为:

网络异常,图片无法展示
|

但是这样的方案,在后续的版本中已经表明是有一定的问题,问题在于由于 microTask 的执行优先级非常高,在某些场景之下它甚至要比事件冒泡还要快,就会导致一些诡异的问题。 例如:

网络异常,图片无法展示
|

issues:link


2.5 版本


针对 2.5 版本之前的问题,进行了一个 timerFunc 的重新包装:

网络异常,图片无法展示
|

在 2.5 版本中,将 microTask 混合 macroTask 进行优雅降级,但是这个方案也存在一些问题:

网络异常,图片无法展示
|


issues:link

由于截图可能问题不太明显,codepen.io/ericcirone/…,大家有兴趣可以看源测试代码,问题点在于当页面在1000px(大于1000,小于1000)来回切换时,列表显示影藏存在 1s 的闪烁。本质上原因是优先使用macroTask,对一些有重绘和动画的场景会有性能的影响,造成了闪烁。


2.6 版本


为了解决之前版本的一些历史问题,最终 nextTick 采取的策略是默认走 microTask ,对于一些 DOM 的交互事件,如 v-on 绑定的事件回调处理函数的处理,会强制走 macroTask。在源码层面上也存在一个优雅的降级,如下:

网络异常,图片无法展示
|


总结


nextTick 在面试中会经常出现,面试官一般通过 nextTick 考验候选人的 Event Loop,或者通过 Event Loop 衍生 nextTick。文章从几个方面浅析了 Vue 的 nextTick 的原理,也从Vue 升级版本来窥视了 nextTick 的前世今生,希望对正在阅读的你有所帮助。

以上就是本文的全部内容。谢谢观看,如果你还觉得不错,帮忙点个赞,谢谢。


参考

目录
相关文章
|
28天前
|
JavaScript API 开发者
Vue是如何进行组件化的
Vue是如何进行组件化的
|
3天前
|
JavaScript 关系型数据库 MySQL
基于VUE的校园二手交易平台系统设计与实现毕业设计论文模板
基于Vue的校园二手交易平台是一款专为校园用户设计的在线交易系统,提供简洁高效、安全可靠的二手商品买卖环境。平台利用Vue框架的响应式数据绑定和组件化特性,实现用户友好的界面,方便商品浏览、发布与管理。该系统采用Node.js、MySQL及B/S架构,确保稳定性和多功能模块设计,涵盖管理员和用户功能模块,促进物品循环使用,降低开销,提升环保意识,助力绿色校园文化建设。
|
28天前
|
JavaScript 前端开发 开发者
Vue是如何劫持响应式对象的
Vue是如何劫持响应式对象的
25 1
|
28天前
|
JavaScript 前端开发 开发者
Vue是如何进行组件化的
Vue是如何进行组件化的
|
28天前
|
存储 JavaScript 前端开发
介绍一下Vue的核心功能
介绍一下Vue的核心功能
|
28天前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
54 0
|
JavaScript 前端开发 缓存
|
1月前
|
JavaScript 前端开发 开发者
vue 数据驱动视图
总之,Vue 数据驱动视图是一种先进的理念和技术,它为前端开发带来了巨大的便利和优势。通过理解和应用这一特性,开发者能够构建出更加动态、高效、用户体验良好的前端应用。在不断发展的前端领域中,数据驱动视图将继续发挥重要作用,推动着应用界面的不断创新和进化。
|
1月前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱前端的大一学生,专注于JavaScript与Vue,正向全栈进发。博客分享Vue学习心得、命令式与声明式编程对比、列表展示及计数器案例等。关注我,持续更新中!🎉🎉🎉
36 1
vue学习第一章
|
1月前
|
JavaScript 前端开发 索引
vue学习第三章
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中的v-bind指令,包括基本使用、动态绑定class及style等,希望能为你的前端学习之路提供帮助。持续关注,更多精彩内容即将呈现!🎉🎉🎉
27 1
vue学习第三章