Vue2和Vue3的nextTick实现原理

 更新时间:2023年04月27日 10:20:59   作者:前端mian  
Vue 中的数据绑定和模板渲染都是异步的,那么如何在更新完成后执行回调函数呢?这就需要用到 Vue 的 nextTick 方法了,本文详细介绍了Vue2和Vue3的nextTick实现原理,感兴趣的同学可以参考一下

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

今天是 Wed Apr 26 2023 14:29:19 GMT+0800 (China Standard Time),我们来聊一下 Vue 的异步更新机制中的 nextTick。Vue 中的数据绑定和模板渲染都是异步的,那么如何在更新完成后执行回调函数呢?这就需要用到 Vue 的 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()
      })
    })
  }
}

总结

在 Vue3 中,nextTick 的实现原理基于 MutationObserver 和 Promise.resolve().then()。如果浏览器不支持原生 Promise,则使用 setTimeout 来模拟 Promise 的行为,并在回调函数执行前添加一个空的定时器来强制推迟执行。

结论

无论是在 Vue2 还是 Vue3 中,nextTick 都是用来处理 DOM 更新完毕后执行回调函数的方法。在 Vue2 中,nextTick 的实现基于浏览器的异步任务队列和微任务队列,而在 Vue3 中,为了解决浏览器对 Promise 的缺陷和问题,使用 MutationObserver 和 Promise.resolve().then() 来实现。同时,Vue3 中的 nextTick 方法也支持返回 Promise 对象,方便等待所有回调函数执行完成后再进行下一步操作。

需要注意的是,尽管 Vue3 中使用了 MutationObserver 和 Promise.resolve().then() 来解决 Promise 在浏览器中的缺陷,但在某些情况下(例如非激活标签页中),仍然可能会出现问题。因此,在实际使用中,还需要根据具体情况选择合适的方案。

总之,了解 nextTick 的实现原理可以帮助我们更好地理解 Vue 中的异步更新机制,从而更好地优化和调试应用程序。

以上就是Vue2和Vue3的nextTick实现原理的详细内容,更多关于Vue2和Vue3 nextTick实现的资料请关注脚本之家其它相关文章!

相关文章

  • Vue如何引入远程JS文件

    Vue如何引入远程JS文件

    本篇文章主要介绍了Vue引入远程JS文件,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-04-04
  • 谈谈我在vue-cli3中用预渲染遇到的坑

    谈谈我在vue-cli3中用预渲染遇到的坑

    这篇文章主要介绍了谈谈我在vue-cli3中用预渲染遇到的坑,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-04-04
  • Vue中禁止编辑的常见方法(以禁止编辑输入框为例)

    Vue中禁止编辑的常见方法(以禁止编辑输入框为例)

    在我们开发项目的时候,有时候我们不希望用户对我们的页面进行操作,尤其是输入框之类的,这篇文章主要给大家介绍了Vue中禁止编辑的常见方法,文中介绍的方法主要以禁止编辑输入框为例,需要的朋友可以参考下
    2024-02-02
  • Vue生命周期详解

    Vue生命周期详解

    这篇文章详细介绍了Vue的生命周期,文中通过代码示例介绍的非常详细。对大家的学习有一定的参考借鉴价值,需要的朋友可以参考下
    2023-04-04
  • 解决vue-cli项目sourcemap因为文件重名导致的文件定位映射错误问题

    解决vue-cli项目sourcemap因为文件重名导致的文件定位映射错误问题

    这篇文章主要介绍了解决vue-cli项目sourcemap因为文件重名导致的文件定位映射错误问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-06-06
  • ant design vue pro 支持多页签模式问题

    ant design vue pro 支持多页签模式问题

    这篇文章主要介绍了ant design vue pro 支持多页签模式问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-11-11
  • vue-resource拦截器设置头信息的实例

    vue-resource拦截器设置头信息的实例

    下面小编就为大家带来一篇vue-resource拦截器设置头信息的实例。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • 详解在vue-cli中引用jQuery、bootstrap以及使用sass、less编写css

    详解在vue-cli中引用jQuery、bootstrap以及使用sass、less编写css

    这篇文章主要介绍了详解在vue-cli中引用jQuery、bootstrap以及使用sass、less编写css,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-11-11
  • Vue dialog模态框的封装方法

    Vue dialog模态框的封装方法

    这篇文章主要为大家详细介绍了Vue dialog模态框的封装方法,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-07-07
  • vue中常用方法的用法汇总

    vue中常用方法的用法汇总

    Vue.js 是一个用于构建用户界面的渐进式框架,本文主要为大家整理了一些常用的 Vue 方法及其详细说明和代码示例,有需要的小伙伴可以参考一下
    2023-11-11

最新评论