nextTick 的原理

109 阅读2分钟

本质: 是创建一个异步队列,将它的回调函数推入异步队列中。
作用: nextTick等下次DOM渲染完成才去调用nextTick内的DOM操作代码。(通过异步队列的方式保证当前页面完成渲染之后再进行DOM操作。
核心原理: Vue不能在渲染DOM的同时去更新DOM,所以更新DOM必须放在异步队列,nextTick主要实现的功能就是把操作DOM的任务放入异步队列。

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
        })
    }
}

nextTick的第一个参数是更改DOM的操作,第二个参数是执行上下文,如果没有传入更改DOM的操作就直接返回一个Promise对象,这个Promise对象的resolve方法就是更改DOM的操作。如果有第一个参数就将这些DOM操作收集起来,统一交给flushCallbacks方法依次执行更改DOM的操作。

function flushCallbacks() {
    pending = false
    const copies = callbacks.slice(0) // 拷贝一份 callbacks
    callbacks.length = 0 // 清空 callbacks
    for (let i = 0; i < copies.length; i++) { // 遍历执行传入的回调
        copies[i]()
    }
}

但是这些更改DOM的操作需要异步,所以要将flushCallbacks方法推入异步队列。但是在不同的环境中推入异步队列的方式不同,如果支持Promise和MutationObserver就推入微任务中,如果支持setImmediatesetTimeout就推入宏任务中。

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) }
}