Vue.nextTick如何高效实现DOM更新后的回调操作?

2026-03-31 16:421阅读0评论SEO资讯
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计2011个文字,预计阅读时间需要9分钟。

Vue.nextTick如何高效实现DOM更新后的回调操作?

目录

一、nextTick小测试

二、nextTick源码实现

1. 全局变量 2. flushCallbacks 3. nextTick的异步实现 4. nextTick方法实现

三、Vue组件的异步更新

1. queueWatcher做了什么? 2. flushSchedulerQueue做了什么?

目录
  • 一、nextTick小测试
  • 二、nextTick源码实现
    • 1. 全局变量
    • 2.flushCallbacks
    • 3.nextTick的异步实现
    • 4.nextTick方法实现
  • 三、Vue组件的异步更新
    • 1.queueWatcher做了什么?
    • 2.flushSchedulerQueue做了什么?
  • 四、回归题目本身

    用过Vue的朋友多多少少都知道$nextTick~ 在正式讲解nextTick之前,我想你应该清楚知道 Vue 在更新 DOM 时是异步执行的,因为接下来讲解过程会结合组件更新一起讲~ 事不宜迟,我们直进主题吧(本文以v2.6.14版本的Vue源码进行讲解)

    一、nextTick小测试

    你真的了解nextTick吗?来,直接上题~

    <template> <div id="app"> <p ref="name">{{ name }}</p> <button @click="handleClick">修改name</button> </div> </template> <script> export default { name: 'App', data () { return { name: '井柏然' } }, mounted() { console.log('mounted', this.$refs.name.innerText) }, methods: { handleClick () { this.$nextTick(() => console.log('nextTick1', this.$refs.name.innerText)) this.name = 'jngboran' console.log('sync log', this.$refs.name.innerText) this.$nextTick(() => console.log('nextTick2', this.$refs.name.innerText)) } } } </script>

    请问上述代码中,当点击按钮“修改name”时,'nextTick1''sync log''nextTick2'对应的this.$refs.name.innerText分别会输出什么?注意,这里打印的是DOM的innerText~(文章结尾处会贴出答案)

    如果此时的你有非常坚定的答案,那你可以不用继续往下看了~但如果你对自己的答案有所顾虑,那不如跟着我,接着往下看。相信你看完,不需要看到答案都能有个肯定的答案了~!

    二、nextTick源码实现

    源码位于core/util/next-tick中。可以将其分为4个部分来看,直接上代码

    1. 全局变量

    callbacks队列、pending状态

    const callbacks = [] // 存放cb的队列 let pending = false // 是否马上遍历队列,执行cb的标志

    2.flushCallbacks

    遍历callbacks执行每个cb

    function flushCallbacks () { pending = false // 注意这里,一旦执行,pending马上被重置为false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() // 执行每个cb } }

    3.nextTick的异步实现

    根据执行环境的支持程度采用不同的异步实现策略

    let timerFunc // nextTick异步实现fn if (typeof Promise !== 'undefined' && isNative(Promise)) { // Promise方案 const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) // 将flushCallbacks包装进Promise.then中 } isUsingMicroTask = true } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // MutationObserver方案 let counter = 1 const observer = new MutationObserver(flushCallbacks) // 将flushCallbacks作为观测变化的cb const textNode = document.createTextNode(String(counter)) // 创建文本节点 // 观测文本节点变化 observer.observe(textNode, { characterData: true }) // timerFunc改变文本节点的data,以触发观测的回调flushCallbacks timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // setImmediate方案 timerFunc = () => { setImmediate(flushCallbacks) } } else { // 最终降级方案setTimeout timerFunc = () => { setTimeout(flushCallbacks, 0) } }

    这里用个真实案例加深对MutationObserver的理解。毕竟比起其他三种异步方案,这个应该是大家最陌生的

    const observer = new MutationObserver(() => console.log('观测到文本节点变化')) const textNode = document.createTextNode(String(1)) observer.observe(textNode, { characterData: true }) console.log('script start') setTimeout(() => console.log('timeout1')) textNode.data = String(2) // 这里对文本节点进行值的修改 console.log('script end')

    知道对应的输出会是怎么样的吗?

    script startscript end会在第一轮宏任务中执行,这点没问题

    setTimeout会被放入下一轮宏任务执行

    MutationObserver是微任务,所以会在本轮宏任务后执行,所以先于setTimeout

    结果如下图:

    Vue.nextTick如何高效实现DOM更新后的回调操作?

    4.nextTick方法实现

    cbPromise方式

    export function nextTick (cb?: Function, ctx?: Object) { let _resolve // 往全局的callbacks队列中添加cb callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { // 这里是支持Promise的写法 _resolve(ctx) } }) if (!pending) { pending = true // 执行timerFunc,在下一个Tick中执行callbacks中的所有cb timerFunc() } // 对Promise的实现,这也是我们使用时可以写成nextTick.then的原因 if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }

    • 深入细节,理解pending有什么用,如何运作?
    • 案例1,同一轮Tick中执行2次$nextTicktimerFunc只会被执行一次

    this.$nextTick(() => console.log('nextTick1')) this.$nextTick(() => console.log('nextTick2'))

    • 用图看看更直观?

    三、Vue组件的异步更新

    这里如果有对Vue组件化、派发更新不是十分了解的朋友,可以先戳这里,看图解Vue响应式原理了解下Vue组件化和派发更新的相关内容再回来看噢~

    Vue的异步更新DOM其实也是使用nextTick来实现的,跟我们平时使用的$nextTick其实是同一个~

    这里我们回顾一下,当我们改变一个属性值的时候会发生什么?

    根据上图派发更新过程,我们从watcher.update开时讲起,以渲染Watcher为例,进入到queueWatcher

    1.queueWatcher做了什么?

    // 用来存放Wathcer的队列。注意,不要跟nextTick的callbacks搞混了,都是队列,但用处不同~ const queue: Array<Watcher> = [] function queueWatcher (watcher: Watcher) { const id = watcher.id // 拿到Wathcer的id,这个id每个watcher都有且全局唯一 if (has[id] == null) { // 避免添加重复wathcer,这也是异步渲染的优化做法 has[id] = true if (!flushing) { queue.push(watcher) } if (!waiting) { waiting = true // 这里把flushSchedulerQueue推进nextTick的callbacks队列中 nextTick(flushSchedulerQueue) } } }

    2.flushSchedulerQueue做了什么?

    function flushSchedulerQueue () { currentFlushTimestamp = getNow() flushing = true let watcher, id // 排序保证先父后子执行更新,保证userWatcher在渲染Watcher前 queue.sort((a, b) => a.id - b.id) // 遍历所有的需要派发更新的Watcher执行更新 for (index = 0; index < queue.length; index++) { watcher = queue[index] id = watcher.id has[id] = null // 真正执行派发更新,render -> update -> patch watcher.run() } }

    • 最后,一张图搞懂组件的异步更新过程

    四、回归题目本身

    相信经过上文对nextTick源码的剖析,我们已经揭开它神秘的面纱了。这时的你一定可以坚定地把答案说出来了~话不多说,我们一起核实下,看看是不是如你所想!

    如图所示,mounted时候的innerText是“井柏然”的中文

    接下来是点击按钮后,打印结果如图所示

    没错,输出结果如下(意不意外?惊不惊喜?)

    sync log 井柏然

    nextTick1 井柏然

    nextTick2 jngboran

    下面简单分析一下每个输出:

    this.$nextTick(() => console.log('nextTick1', this.$refs.name.innerText)) this.name = 'jngboran' console.log('sync log', this.$refs.name.innerText) this.$nextTick(() => console.log('nextTick2', this.$refs.name.innerText))

    sync log:这个同步打印没什么好说了,相信大部分童鞋的疑问点都不在这里。如果不清楚的童鞋可以先回顾一下EventLoop,这里不多赘述了~

    nextTick1:注意其虽然是放在$nextTick的回调中,在下一个tick执行,但是他的位置是在this.name = 'jngboran&#39;的前。也就是说,他的cb会比App组件的派发更新(flushSchedulerQueue)更先进入队列,当nextTick1打印时,App组件还未派发更新,所以拿到的还是旧的DOM值。

    nextTick2就不展开了,大家可以自行分析一下。相信大家对它应该是最肯定的,我们平时不就是这样拿到更新后的DOM吗?

    最后来一张图加深理解

    写在最后,nextTick其实在Vue中也算是比较核心的一个东西了。因为贯穿整个Vue应用的组件化、响应式的派发更新与其息息相关~深入理解nextTick的背后实现原理,不仅能让你在面试的时候一展风采,更能让你在日常开发工作中,少走弯路少踩坑!

    以上就是Vue.nextTick纯干货使用方法详解的详细内容,更多关于Vue.nextTick使用方法的资料请关注易盾网络其它相关文章!

    本文共计2011个文字,预计阅读时间需要9分钟。

    Vue.nextTick如何高效实现DOM更新后的回调操作?

    目录

    一、nextTick小测试

    二、nextTick源码实现

    1. 全局变量 2. flushCallbacks 3. nextTick的异步实现 4. nextTick方法实现

    三、Vue组件的异步更新

    1. queueWatcher做了什么? 2. flushSchedulerQueue做了什么?

    目录
    • 一、nextTick小测试
    • 二、nextTick源码实现
      • 1. 全局变量
      • 2.flushCallbacks
      • 3.nextTick的异步实现
      • 4.nextTick方法实现
    • 三、Vue组件的异步更新
      • 1.queueWatcher做了什么?
      • 2.flushSchedulerQueue做了什么?
    • 四、回归题目本身

      用过Vue的朋友多多少少都知道$nextTick~ 在正式讲解nextTick之前,我想你应该清楚知道 Vue 在更新 DOM 时是异步执行的,因为接下来讲解过程会结合组件更新一起讲~ 事不宜迟,我们直进主题吧(本文以v2.6.14版本的Vue源码进行讲解)

      一、nextTick小测试

      你真的了解nextTick吗?来,直接上题~

      <template> <div id="app"> <p ref="name">{{ name }}</p> <button @click="handleClick">修改name</button> </div> </template> <script> export default { name: 'App', data () { return { name: '井柏然' } }, mounted() { console.log('mounted', this.$refs.name.innerText) }, methods: { handleClick () { this.$nextTick(() => console.log('nextTick1', this.$refs.name.innerText)) this.name = 'jngboran' console.log('sync log', this.$refs.name.innerText) this.$nextTick(() => console.log('nextTick2', this.$refs.name.innerText)) } } } </script>

      请问上述代码中,当点击按钮“修改name”时,'nextTick1''sync log''nextTick2'对应的this.$refs.name.innerText分别会输出什么?注意,这里打印的是DOM的innerText~(文章结尾处会贴出答案)

      如果此时的你有非常坚定的答案,那你可以不用继续往下看了~但如果你对自己的答案有所顾虑,那不如跟着我,接着往下看。相信你看完,不需要看到答案都能有个肯定的答案了~!

      二、nextTick源码实现

      源码位于core/util/next-tick中。可以将其分为4个部分来看,直接上代码

      1. 全局变量

      callbacks队列、pending状态

      const callbacks = [] // 存放cb的队列 let pending = false // 是否马上遍历队列,执行cb的标志

      2.flushCallbacks

      遍历callbacks执行每个cb

      function flushCallbacks () { pending = false // 注意这里,一旦执行,pending马上被重置为false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() // 执行每个cb } }

      3.nextTick的异步实现

      根据执行环境的支持程度采用不同的异步实现策略

      let timerFunc // nextTick异步实现fn if (typeof Promise !== 'undefined' && isNative(Promise)) { // Promise方案 const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) // 将flushCallbacks包装进Promise.then中 } isUsingMicroTask = true } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // MutationObserver方案 let counter = 1 const observer = new MutationObserver(flushCallbacks) // 将flushCallbacks作为观测变化的cb const textNode = document.createTextNode(String(counter)) // 创建文本节点 // 观测文本节点变化 observer.observe(textNode, { characterData: true }) // timerFunc改变文本节点的data,以触发观测的回调flushCallbacks timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // setImmediate方案 timerFunc = () => { setImmediate(flushCallbacks) } } else { // 最终降级方案setTimeout timerFunc = () => { setTimeout(flushCallbacks, 0) } }

      这里用个真实案例加深对MutationObserver的理解。毕竟比起其他三种异步方案,这个应该是大家最陌生的

      const observer = new MutationObserver(() => console.log('观测到文本节点变化')) const textNode = document.createTextNode(String(1)) observer.observe(textNode, { characterData: true }) console.log('script start') setTimeout(() => console.log('timeout1')) textNode.data = String(2) // 这里对文本节点进行值的修改 console.log('script end')

      知道对应的输出会是怎么样的吗?

      script startscript end会在第一轮宏任务中执行,这点没问题

      setTimeout会被放入下一轮宏任务执行

      MutationObserver是微任务,所以会在本轮宏任务后执行,所以先于setTimeout

      结果如下图:

      Vue.nextTick如何高效实现DOM更新后的回调操作?

      4.nextTick方法实现

      cbPromise方式

      export function nextTick (cb?: Function, ctx?: Object) { let _resolve // 往全局的callbacks队列中添加cb callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { // 这里是支持Promise的写法 _resolve(ctx) } }) if (!pending) { pending = true // 执行timerFunc,在下一个Tick中执行callbacks中的所有cb timerFunc() } // 对Promise的实现,这也是我们使用时可以写成nextTick.then的原因 if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }

      • 深入细节,理解pending有什么用,如何运作?
      • 案例1,同一轮Tick中执行2次$nextTicktimerFunc只会被执行一次

      this.$nextTick(() => console.log('nextTick1')) this.$nextTick(() => console.log('nextTick2'))

      • 用图看看更直观?

      三、Vue组件的异步更新

      这里如果有对Vue组件化、派发更新不是十分了解的朋友,可以先戳这里,看图解Vue响应式原理了解下Vue组件化和派发更新的相关内容再回来看噢~

      Vue的异步更新DOM其实也是使用nextTick来实现的,跟我们平时使用的$nextTick其实是同一个~

      这里我们回顾一下,当我们改变一个属性值的时候会发生什么?

      根据上图派发更新过程,我们从watcher.update开时讲起,以渲染Watcher为例,进入到queueWatcher

      1.queueWatcher做了什么?

      // 用来存放Wathcer的队列。注意,不要跟nextTick的callbacks搞混了,都是队列,但用处不同~ const queue: Array<Watcher> = [] function queueWatcher (watcher: Watcher) { const id = watcher.id // 拿到Wathcer的id,这个id每个watcher都有且全局唯一 if (has[id] == null) { // 避免添加重复wathcer,这也是异步渲染的优化做法 has[id] = true if (!flushing) { queue.push(watcher) } if (!waiting) { waiting = true // 这里把flushSchedulerQueue推进nextTick的callbacks队列中 nextTick(flushSchedulerQueue) } } }

      2.flushSchedulerQueue做了什么?

      function flushSchedulerQueue () { currentFlushTimestamp = getNow() flushing = true let watcher, id // 排序保证先父后子执行更新,保证userWatcher在渲染Watcher前 queue.sort((a, b) => a.id - b.id) // 遍历所有的需要派发更新的Watcher执行更新 for (index = 0; index < queue.length; index++) { watcher = queue[index] id = watcher.id has[id] = null // 真正执行派发更新,render -> update -> patch watcher.run() } }

      • 最后,一张图搞懂组件的异步更新过程

      四、回归题目本身

      相信经过上文对nextTick源码的剖析,我们已经揭开它神秘的面纱了。这时的你一定可以坚定地把答案说出来了~话不多说,我们一起核实下,看看是不是如你所想!

      如图所示,mounted时候的innerText是“井柏然”的中文

      接下来是点击按钮后,打印结果如图所示

      没错,输出结果如下(意不意外?惊不惊喜?)

      sync log 井柏然

      nextTick1 井柏然

      nextTick2 jngboran

      下面简单分析一下每个输出:

      this.$nextTick(() => console.log('nextTick1', this.$refs.name.innerText)) this.name = 'jngboran' console.log('sync log', this.$refs.name.innerText) this.$nextTick(() => console.log('nextTick2', this.$refs.name.innerText))

      sync log:这个同步打印没什么好说了,相信大部分童鞋的疑问点都不在这里。如果不清楚的童鞋可以先回顾一下EventLoop,这里不多赘述了~

      nextTick1:注意其虽然是放在$nextTick的回调中,在下一个tick执行,但是他的位置是在this.name = 'jngboran&#39;的前。也就是说,他的cb会比App组件的派发更新(flushSchedulerQueue)更先进入队列,当nextTick1打印时,App组件还未派发更新,所以拿到的还是旧的DOM值。

      nextTick2就不展开了,大家可以自行分析一下。相信大家对它应该是最肯定的,我们平时不就是这样拿到更新后的DOM吗?

      最后来一张图加深理解

      写在最后,nextTick其实在Vue中也算是比较核心的一个东西了。因为贯穿整个Vue应用的组件化、响应式的派发更新与其息息相关~深入理解nextTick的背后实现原理,不仅能让你在面试的时候一展风采,更能让你在日常开发工作中,少走弯路少踩坑!

      以上就是Vue.nextTick纯干货使用方法详解的详细内容,更多关于Vue.nextTick使用方法的资料请关注易盾网络其它相关文章!