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

1,707 阅读6分钟

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

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(图例中第二个步骤,即'下次更新循环')

  • 同步任务执行完毕,开始执行异步 watcher 队列的任务,更新 DOM 。Vue 在内部尝试对异步队列使用原生的 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 的包装,后面会讲到),就只有几行代码。大致步骤如下: image.png

通过数组 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 版本之前包装都是一样的。优雅降级方案为:

image.png

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

issues:link

2.5 版本

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

image.png

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

issues:link

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

2.6 版本

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

image.png

总结

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

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

参考