nextTick是什么

文章类型:Vue

发布者:hp

发布时间:2023-02-27

一:定义

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM

简单来说,就是因为Vue更新是异步的,数据发生变化的时候,Vue有一个异步执行队列,视图更新需要等队列所有数据变化完成后,一起更新,本质是一种优化策略


二:原因

获取dom的值是旧值

<template>
  <div>
    <button @click="add()">添加</button>
    <ul>
      <li v-for="(i, index) in list" ref="l" key="index">{{ i }}</li>
    </ul>
  </div>
</template>

<script setup>
import { nextTick, ref } from "vue";
const l = ref(null)
const list = ref([1, 2, 3])
console.log(l.value);
</script>

三:原理

(一)方式:

主要使用了宏任务和微任务,利用 Event loop 事件线程去异步操作,本质上就是注册了异步任务对任务进行处理

根据执行环境尝试Promise、MutationObserver‘’setImmediate如果以上都不行则采用 setTimeout

数据变更后,Vue 会先执行同步的数据变更操作,然后将需要更新的 DOM 操作推入微任务队列或宏任务队列,等待当前执行栈清空后执行。

在任务队列执行完毕后,Vue 会触发 nextTick 的回调函数,以便我们可以在 DOM 更新完成后执行相应的操作。它的底层原理是通过利用 JavaScript 的任务队列来实现的

(二)简化代码:


// 用于存储待执行的回调函数数组
const callbacks = [];

// 标记任务队列是否正在执行中
let pending = false;

// 定义执行任务队列的函数
function flushCallbacks() {
  pending = false;
  const copies = callbacks.slice(); // 复制一份待执行的回调函数数组
  callbacks.length = 0; // 清空回调函数数组
  for (let i = 0; i < copies.length; i++) {
    copies[i](); // 依次执行回调函数
  }
}

// 定义 nextTick 方法
function nextTick(callback) {
  callbacks.push(callback);

  if (!pending) {
    pending = true;
    // 在任务队列中添加一个微任务(Promise 微任务或 MutationObserver 微任务)
    // 可以确保回调函数在 DOM 更新循环结束之后执行
    // 这里简化为使用 Promise 微任务
    Promise.resolve().then(flushCallbacks);
  }
}

(三)执行流程

1:通过数组 callbacks 来存储用户注册待执行的回调函数

2:变量 pending 来标记是否正在执行任务(异步锁,正常执行true,执行完成则为false)

3:新来的任务是否需要放到下一次的任务队列中

4:执行函数 flushCallbacks。当这个函数被触发时,会将 callbacks 中的所有函数依次执行,然后清空 callbacks,并将 pending 设置为 false


四:作用

修改数据后立刻得到更新后的DOM结构

五:区别

Vue.nextTick()和$nextTick

1:nextTick(callback):当数据发生变化,更新后执行回调

2:$nextTick(callback):当dom发生变化,更新后执行的回调

3:两者区别在于nextTick是全局方法,而$nextTick自动绑定在this实例上的

六:源码






export let isUsingMicroTask = false     // 标记 nextTick 最终是否以微任务执行

const callbacks = []     // 存放调用 nextTick 时传入的回调函数
let pending = false     // 标记是否已经向任务队列中添加了一个任务,如果已经添加了就不能再添加了
    // 当向任务队列中添加了任务时,将 pending 置为 true,当任务被执行时将 pending 置为 false
    // 


// 声明 nextTick 函数,接收一个回调函数和一个执行上下文作为参数
// 回调的 this 自动绑定到调用它的实例上
export function nextTick(cb?: Function, ctx?: Object) {
    let _resolve
    // 将传入的回调函数存放到数组中,后面会遍历执行其中的回调
    callbacks.push(() => {
        if (cb) {   // 对传入的回调进行 try catch 错误捕获
            try {
                cb.call(ctx)
            } catch (e) {    // 进行统一的错误处理
                handleError(e, ctx, 'nextTick')
            }
        } else if (_resolve) {
            _resolve(ctx)
        }
    })
    
    // 如果当前没有在 pending 的回调,
    // 就执行 timeFunc 函数选择当前环境优先支持的异步方法
    if (!pending) {
        pending = true
        timerFunc()
    }
    
    // 如果没有传入回调,并且当前环境支持 promise,就返回一个 promise
    // 在返回的这个 promise.then 中 DOM 已经更新好了,
    if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
            _resolve = resolve
        })
    }
}


// 判断当前环境优先支持的异步方法,优先选择微任务
// 优先级:Promise---> MutationObserver---> setImmediate---> setTimeout
// setTimeout 可能产生一个 4ms 的延迟,而 setImmediate 会在主线程执行完后立刻执行
// setImmediate 在 IE10 和 node 中支持

// 当在同一轮事件循环中多次调用 nextTick 时 ,timerFunc 只会执行一次

let timerFunc   
// 判断当前环境是否原生支持 promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {  // 支持 promise
    const p = Promise.resolve()
    timerFunc = () => {
       // 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
        p.then(flushCallbacks)
        if (isIOS) setTimeout(noop)
        // 这里的 setTimeout 是用来强制刷新微任务队列的
        // 因为在 ios 下 promise.then 后面没有宏任务的话,微任务队列不会刷新
    }
    // 标记当前 nextTick 使用的微任务
    isUsingMicroTask = true
    
    
    // 如果不支持 promise,就判断是否支持 MutationObserver
    // 不是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    // 标记当前 nextTick 使用的微任务
    
    
    // 判断当前环境是否原生支持 setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    timerFunc = () => { setImmediate(flushCallbacks)  }
} else {

    // 以上三种都不支持就选择 setTimeout
    timerFunc = () => { setTimeout(flushCallbacks, 0) }
}


// 如果多次调用 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]()
    }
}

// 为什么要拷贝一份 callbacks

// 用 callbacks.slice(0) 将 callbacks 拷贝出来一份,
// 是因为考虑到在 nextTick 回调中可能还会调用 nextTick 的情况,
// 如果在 nextTick 回调中又调用了一次 nextTick,则又会向 callbacks 中添加回调,
// 而 nextTick 回调中的 nextTick 应该放在下一轮执行,
// 否则就可能出现一直循环的情况,
// 所以需要将 callbacks 复制一份出来然后清空,再遍历备份列表执行回调