【Vue】【全局API - 通用】nextTick

117 阅读6分钟

前言

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 使用场景

  • 生命周期钩子函数:在大部分的钩子函数中都可以使用。需要注意的是,在beforeCreatecreated中获取的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 内部实现

通过下面的源码,我们可以获得以下几个方面的信息:

  1. timerFunc函数定义,这里是根据当前环境支持什么方法则确定调用哪个?分别有:Promise.thenMutationObserversetImmediatesetTimeout,并且它们的优先级是这样的:Promise---> MutationObserver---> setImmediate---> setTimeout

    • setTimeout 可能产生一个 4ms 的延迟;
    • setImmediate 会在主线程执行完后立即执行(在IE10和node中支持)

    流程如下图所示:

    image.png

  2. nextTick的执行顺序流程如下:

    image.png

  3. 源码

    版本: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 应该放在下一轮执行

资料