前言
nextTick 是Vue中常使用到的一个全局API。
本文主要是介绍这个API是什么?为什么需要他?它的作用是什么?使用场景有哪些?
一、nextTick
1.1 什么是nextTick?
-
官方定义:等待下一次DOM更新刷新的工具方法
-
通俗理解:在下次DOM 更新循环结束之后延迟回调(在修改数据之后立即使用这个方法,获取更新后的DOM)
-
参数:
{Function}[callback]
1.2 为什么需要nextTick
因为当响应式数据更改后,我们立即去获取该数据对应的DOM时,会发现该DOM不存在或者不是最新状态,但是我们需要的是获取最新状态的DOM,再进行下步操作,这个时候 nextTick 就可以闪亮登场了。
那么更新响应式数据后,为什么DOM不是最新状态?理由如下:
Vue 在更新 DOM 时是异步的,当数据发生变化后,Vue并不会立刻去更新DOM,而是开启一个异步更新队列。视图需要等队列中所有数据变化完成之后,再统一进行更新。
异步更新队列机制的优点在于:
- 性能优化
- 减少重复更新
- 防止过度渲染
- 提升用户体验
1.3 nextTick的作用
nextTick 方法是Vue提供的异步操作API,它用于在下次DOM更新循环之后执行特定的回调函数。这意味着,当您调用 nextTick 时,Vue会等待当前数据更新完成,然后执行您传递的回调函数,确保您在最新的DOM状态下操作元素。
1.4 基本使用
1.4.1 使用场景
- 生命周期钩子函数:在大部分的钩子函数中都可以使用。需要注意的是,在
beforeCreate和created中获取的DOM可能不是最新DOM - 路由组件守卫
beforeRouteEnter:此时组件实例this尚未创建,需要通过next访问组件实例beforeRouteEnter (to, from, next) { console.log(this); // undefined next(vm => { // 通过 `vm` 访问组件实例 console.log(vm); // 组件实例 vm.$nextTick(() => { XXX }) }) }
1.4.2 使用方法
-
使用方式1 - 传递回调函数:
- 传参:
- 第一个参数:回调函数(可以获取最近的DOM结构)
- 第二个参数:执行函数上下文
// 修改数据 vm.message = '修改后的值' // DOM 还没有更新 console.log(vm.$el.textContent) // 原始的值 Vue.nextTick(function () { // DOM 更新了 console.log(vm.$el.textContent) // 修改后的值 })组件内使用
vm.$nextTick()实例方法只需要通过this.$nextTick(),并且回调函数中的this将自动绑定到当前的Vue实例上this.message = '修改后的值' console.log(this.$el.textContent) // => '原始的值' this.$nextTick(function () { console.log(this.$el.textContent) // => '修改后的值' }) - 传参:
-
使用方法二 - 使用
async/await:$nextTick()会返回一个Promise对象,可以使用async/await完成相同作用的事情this.message = '修改后的值' console.log(this.$el.textContent) // => '原始的值' await this.$nextTick() console.log(this.$el.textContent) // => '修改后的值'
二、实现原理
2.1 基本原理
将传入的回调函数包装成异步任务
2.2 内部实现
通过下面的源码,我们可以获得以下几个方面的信息:
-
timerFunc函数定义,这里是根据当前环境支持什么方法则确定调用哪个?分别有:Promise.then、MutationObserver、setImmediate、setTimeout,并且它们的优先级是这样的:Promise---> MutationObserver---> setImmediate---> setTimeout- setTimeout 可能产生一个 4ms 的延迟;
- setImmediate 会在主线程执行完后立即执行(在IE10和node中支持)
流程如下图所示:
-
nextTick的执行顺序流程如下:
-
源码
版本:2.7.14
源码位置:src\core\util\next-tick.ts
/* globals MutationObserver */ import { noop } from 'shared/util' import { handleError } from './error' import { isIE, isIOS, isNative } from './env' // 上面三行与核心代码关系不大,了解即可 // noop: 一个空操作函数,用于占位。用作函数默认值,防止 undefined 导致报错。 // handleError: 错误处理函数,用于捕获异常,并在控制台打印异常信息。 // isIE: 判断当前浏览器是否为 IE 浏览器。 // isIOS: 判断当前浏览器是否为 iOS 设备。 // isNative: 判断某个属性或方法是否原生支持,如果不支持或通过第三方实现支持都会返回 false。 // 标记 nextTick 最终是否以微任务执行。false 表示宏任务,true 表示微任务。 export let isUsingMicroTask = false // 存放调用 nextTick 时传入的回调函数 const callbacks: Array<Function> = [] /** * 标记是否已经向任务队列中添加一个任务,如果已经添加了就不能再添加了 * 当向任务队列中添加了任务时,将 pending 设置为 true * 当任务队列中的任务执行完毕后,将 pending 设置为 false */ let pending = false // flushCallbacks 函数用于执行回调队列中的所有回调函数 // 如果多次调用 nextTick,会一次执行上面的方法,将 nextTick 的回调放在 callbacks 中 // 最后通过 flushCallbacks 函数遍历 callbacks 数组的靠背并执行其中的回调 function flushCallbacks() { pending = false const copies = callbacks.slice(0) // 拷贝一份 callbacks callbacks.length = 0 // 清空 callbacks for (let i = 0; i < copies.length; i++) { // 遍历执行传入的回调 copies[i]() } } // 当在同一轮事件循环中,多次调用 nextTick 时,只会执行一次 timerFunc let timerFunc // 判断当前环境优先支持的异步方法,优先选择微任务 if (typeof Promise !== 'undefined' && isNative(Promise)) { // 支持原生 Promise const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) // 用 Promise.then 把 flushCallbacks包裹成一个异步微任务 /** * 在 ios 设备上,Promise.then 后面没有宏任务胡话,微任务队列不对刷新 * 这里的 setTimeout 是用来强制刷新微任务队列的 */ if (isIOS) setTimeout(noop) } isUsingMicroTask = true /** * 如果不支持 Promise,就判断是否支持 MutationObserver * MutationObserver: 是一个 Web API,它能够监听 DOM 的变化。属于微任务 * 不是IE环境,并且原生支持 MutationObserver */ } else if ( !isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]') ) { let counter = 1 // new 一个 MutationObserver 类 const observer = new MutationObserver(flushCallbacks) // 创建一个文本节点 const textNode = document.createTextNode(String(counter)) // 监听这个文本节点,当数据发生变化就执行 flushCallbacks observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true // 判断当前环境是否原生支持 setImmediate // setImmediate:该方法用来把一些需要长时间运行的操作放在一个回调函数里,在浏览器完成后面的其他语句后,就立刻执行这个回调函数。 // 宏任务 } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(flushCallbacks) } } else { // 以上三种都不支持就使用 setTimeout timerFunc = () => { setTimeout(flushCallbacks, 0) } } export function nextTick(): Promise<void> export function nextTick<T>(this: T, cb: (this: T, ...args: any[]) => any): void export function nextTick<T>(cb: (this: T, ...args: any[]) => any, ctx: T): void /** * @internal */ export function nextTick(cb?: (...args: any[]) => any, ctx?: object) { let _resolve // 将 nextTick 的回调函数放入到异步操作队列 callbacks 中 callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e: any) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) // 如果任务队列中的任务执行完毕后,执行异步延迟函数 timerFunc if (!pending) { pending = true timerFunc() } // 当 nextTick 没有传如回调函数,并且Promise存在时,返回一个 Promise 化的调用 if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }为什么要拷贝一份 callbacks?
用 callbacks.slice(0) 将 callbacks 拷贝出来一份,然后清空 callbacks 数组 => 是因为考虑到在 nextTick 的回调中可能还会有 nextTick 的情况, 如果在 nextTick 回调中又调用了一次 nextTick,则又会向 callbacks 中添加回调,这样就会导致无限循环,所以需要拷贝一份 callbacks 出来执行。 所以,nextTick 回调中的 nextTick 应该放在下一轮执行
资料: