Vue2和Vue3的nextTick怎么实现

寻技术 VUE 2023年07月11日 97

这篇“Vue2和Vue3的nextTick怎么实现”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Vue2和Vue3的nextTick怎么实现”文章吧。

    一次弄懂 Vue2 和 Vue3 的 nextTick 实现原理

    Vue2 中的 nextTick

    在 Vue2 中,nextTick 的实现基于浏览器的异步任务队列和微任务队列。

    异步任务队列

    在浏览器中,每个宏任务结束后会检查微任务队列,如果有任务则依次执行。当所有微任务执行完成后,才会执行下一个宏任务。因此可以通过将任务作为微任务添加到微任务队列中,来确保任务在所有宏任务执行完毕后立即执行。

    而使用 setTimeout 可以将任务添加到异步任务队列中,在下一轮事件循环中执行。

    在 Vue2 中,如果没有指定执行环境,则会优先使用 Promise.then / MutationObserver,否则使用 setTimeout。

    javascript
    // src/core/util/next-tick.js
    
    /* istanbul ignore next */
    const callbacks = []
    let pending = false
    
    function flushCallbacks() {
      pending = false
      const copies = callbacks.slice(0)
      callbacks.length = 0
      for (let i = 0; i < copies.length; i++) {
        copies[i]()
      }
    }
    
    let microTimerFunc
    let macroTimerFunc
    let useMacroTask = false
    
    if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
      // 使用 setImmediate
      macroTimerFunc = () => {
        setImmediate(flushCallbacks)
      }
    } else if (
      typeof MessageChannel !== 'undefined' &&
      (isNative(MessageChannel) ||
        // PhantomJS
        MessageChannel.toString() === '[object MessageChannelConstructor]')
    ) {
      const channel = new MessageChannel()
      const port = channel.port2
      channel.port1.onmessage = flushCallbacks
      macroTimerFunc = () => {
        port.postMessage(1)
      }
    } else {
      // 使用 setTimeout
      macroTimerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
    }
    
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
      // 使用 Promise.then
      const p = Promise.resolve()
      microTimerFunc = () => {
        p.then(flushCallbacks)
      }
    } else {
      // 使用 MutationObserver
      const observer = new MutationObserver(flushCallbacks)
      const textNode = document.createTextNode(String(1))
      observer.observe(textNode, {
        characterData: true
      })
      microTimerFunc = () => {
        textNode.data = String(1)
      }
    }
    
    export function nextTick(cb?: Function, ctx?: Object) {
      let _resolve
      callbacks.push(() => {
        if (cb) {
          try {
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        } else if (_resolve) {
          _resolve(ctx)
        }
      })
      if (!pending) {
        pending = true
        if (useMacroTask) {
          macroTimerFunc()
        } else {
          microTimerFunc()
        }
      }
      if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }

    宏任务和微任务

    在 Vue2 中,可以通过设置 useMacroTask 来使 nextTick 方法使用宏任务或者微任务。

    Vue2 中默认使用微任务,在没有原生 Promise 和 MutationObserver 的情况下,才会改用 setTimeout。

    javascript
    let microTimerFunc
    let macroTimerFunc
    let useMacroTask = false // 默认使用微任务
    
    if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
      // 使用 setImmediate
      macroTimerFunc = () => {
        setImmediate(flushCallbacks)
      }
    } else if (
      typeof MessageChannel !== 'undefined' &&
      (isNative(MessageChannel) ||
        // PhantomJS
        MessageChannel.toString() === '[object MessageChannelConstructor]')
    ) {
      const channel = new MessageChannel()
      const port = channel.port2
      channel.port1.onmessage = flushCallbacks
      macroTimerFunc = () => {
        port.postMessage(1)
      }
    } else {
      // 使用 setTimeout
      macroTimerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
    }
    
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
      // 使用 Promise.then
      const p = Promise.resolve()
      microTimerFunc = () => {
        p.then(flushCallbacks)
      }
    } else {
      // 使用 MutationObserver
      const observer = new MutationObserver(flushCallbacks)
      const textNode = document.createTextNode(String(1))
      observer.observe(textNode, {
        characterData: true
      })
      microTimerFunc = () => {
        textNode.data = String(1)
      }
    }
    
    export function nextTick(cb?: Function, ctx?: Object) {
      let _resolve
      callbacks.push(() => {
        if (cb) {
          try {
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        } else if (_resolve) {
          _resolve(ctx)
        }
      })
      if (!pending) {
        pending = true
        if (useMacroTask) {
          macroTimerFunc()
        } else {
          microTimerFunc()
        }
      }
      if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }

    总结

    在 Vue2 中,nextTick 的实现原理基于浏览器的异步任务队列和微任务队列。Vue2 默认使用微任务,在没有原生 Promise 和 MutationObserver 的情况下才会改用 setTimeout。

    Vue3 中的 nextTick

    在 Vue3 中,nextTick 的实现有了较大变化,主要是为了解决浏览器对 Promise 的缺陷和问题。

    Promise 在浏览器中的问题

    在浏览器中,Promise 有一个缺陷:如果 Promise 在当前事件循环中被解决,那么在 then 回调函数之前添加的任务将不能在同一个任务中执行。

    例如:

    javascript
    Promise.resolve().then(() => {
      console.log('Promise 1')
    }).then(() => {
      console.log('Promise 2')
    })
    
    console.log('Hello')

    输出结果为:

    Hello
    Promise 1
    Promise 2

    这是因为 Promise 虽然是微任务,但是需要等到当前宏任务结束才能执行。

    Vue3 中解决 Promise 缺陷的方法

    在 Vue3 中,通过使用 MutationObserver 和 Promise.resolve().then() 来解决 Promise 在浏览器中的缺陷。具体实现如下:

    javascript
    const queue: Array<Function> = []
    let has: { [key: number]: boolean } = {}
    let flushing = false
    let index = 0
    
    function resetSchedulerState() {
      queue.length = 0
      has = {}
      flushing = false
    }
    
    function flushSchedulerQueue() {
      flushing = true
      let job
      while ((job = queue.shift())) {
        if (!has[job.id]) {
          has[job.id] = true
          job()
        }
      }
      resetSchedulerState()
    }
    
    let macroTimerFunc
    let microTimerFunc
    
    if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
      macroTimerFunc = () => {
        setImmediate(flushSchedulerQueue)
      }
    } else {
      macroTimerFunc = () => {
        setTimeout(flushSchedulerQueue, 0)
      }
    }
    
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
      const p = Promise.resolve()
      microTimerFunc = () => {
        p.then(flushSchedulerQueue)
        if (isIOS) setTimeout(noop)
      }
    } else {
      microTimerFunc = macroTimerFunc
    }
    
    export function nextTick(fn?: Function): Promise<void> {
      const id = index++
      const job = fn.bind(null)
      queue.push(job)
    
      if (!flushing) {
        if (useMacroTask) {
          macroTimerFunc()
        } else {
          microTimerFunc()
        }
      }
    
      if (!fn && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          resolvedPromise.then(() => {
            if (has[id] || !queue.includes(job)) {
              return
            }
            queue.splice(queue.indexOf(job), 1)
            resolve()
          })
        })
      }
    }

    在 Vue3 中,nextTick 的实现原理基于MutationObserver 和 Promise.resolve().then(),通过 MutationObserver 监测 DOM 变化,在下一个微任务中执行回调函数。

    而如果当前浏览器不支持原生 Promise,则使用 setTimeout 来模拟 Promise 的行为,并在回调函数执行前添加一个空的定时器来强制推迟执行(解决 iOS 中 setTimeout 在非激活标签页中的问题)。

    如果需要等待所有回调函数执行完成,则可以通过返回一个 Promise 对象来实现。

    javascript
    export function nextTick(fn?: Function): Promise<void> {
      const id = index++
      const job = fn.bind(null)
      queue.push(job)
    
      if (!flushing) {
        if (useMacroTask) {
          macroTimerFunc()
        } else {
          microTimerFunc()
        }
      }
    
      if (!fn && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          resolvedPromise.then(() => {
            if (has[id] || !queue.includes(job)) {
              return
            }
            queue.splice(queue.indexOf(job), 1)
            resolve()
          })
        })
      }
    }
    关闭

    用微信“扫一扫”