前端边角料 | 聊聊 Vue 的 $nextTick 原理__Vue.js
发布于 3 年前 作者 banyungong 1198 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

关键词:Vue $nextTick 异步更新队列 微任务 宏任务 EventLoop

目录

  • 什么是 $nextTick
  • $nextTick 的用途
  • 源码解析
  • 杂谈
  • 参考资料

什么是 $nextTick

官方文档说明:https://cn.vuejs.org/v2/guide/reactivity.html#%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0%E9%98%9F%E5%88%97

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

$nextTick 的用途

  • 需要在视图更新之后,基于新的视图进行操作
  • 在 created 周期中使用,因为在 created() 钩子函数执行的时候 DOM 其实并未进行任何渲染,其实相当于 mounted() 钩子函数的应用
  • 用法一:
<div id="example">{{message}}</div>

var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})
  • 在组件内使用 vm.$nextTick() 实例方法特别方便,因为它不需要全局 Vue,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上:
Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: '未更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '已更新'
      console.log(this.$el.textContent) // => '未更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '已更新'
      })
    }
  }
})
  • 用法二,因为 $nextTick() 返回一个 Promise 对象,所以你可以使用新的 ES2017 async/await 语法完成相同的事情:
methods: {
  updateMessage: async function () {
    this.message = '已更新'
    console.log(this.$el.textContent) // => '未更新'
    await this.$nextTick()
    console.log(this.$el.textContent) // => '已更新'
  }
}

源码解析

  • 理解 flushCallbacks
// 同步执行回调函数队列
function flushCallbacks () {
  // 设置当前执行状态为非等待状态,即执行回调队列中
  pending = false
  // 浅拷贝回调队列数组
  const copies = callbacks.slice(0)
  // 重置回调队列,等价于 callbacks = []
  callbacks.length = 0
  // 循环执行回调函数
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
  • 理解 timerFunc
// 根据不同的平台(浏览器、APP的 WebView),
// 不同的版本(IOS版本,浏览器版本),
// 分别设置优先级由 Promise -> MutationObserver -> setImmediate -> setTimeout 的方法赋值给 timerFunc
// 目的是在执行 timerFunc 时将其设置为 微任务 -> 宏任务,应用于浏览器的 EventLoop 之中
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
   
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
 
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
 
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
  • 理解 nextTick
// 导出供 Vue 内部,及 vm / Vue 外部调用的 nextTick 方法
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 将传入的回调函数放入回调队列中
  callbacks.push(() => {
    if (cb) {
    // 防止由某一个回调函数报错导致整个 JS 线程挂掉的情况,使用了 try ... catch
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 如果当前的 nextTick 并没有闲置,则设置其为非闲置,并开始执行回调函数队列
  if (!pending) {
    pending = true
    timerFunc()
  }
  // 如果没有传入回调函数,则将 nextTick 当做一个 Promise 返回
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

杂谈

以下均为个人观点

  • Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用
  • nextTick 的核心利用了如 Promise 、 MutationObserver 、 setImmediate 、 setTimeout 的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列
  • nextTick 不仅是 Vue 内部的异步队列的调用方法,同时也允许开发者在实际项目中使用这个方法来满足实际应用中对 Dom 更新数据时机的后续逻辑处理
  • nextTick 是典型的将底层 JavaScript 执行原理应用到具体案例中的示例
  • 引入异步更新队列机制的原因:
    • 如果是同步更新,则多次对一个或多个属性赋值,会频繁触发 UI/Dom 的渲染,可以减少一些无用渲染
    • 同时由于 VirtualDOM 的引入,每一次状态发生变化后,状态变化的信号会发送给组件,组件内部使用 VirtualDOM 进行计算得出需要更新的具体的 DOM 节点,然后对 DOM 进行更新操作,每次更新状态后的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所以异步渲染变得更加至关重要

参考资料

Vue官网-异步更新队列

Vue-next-tick.js

本文使用 mdnice 排版

浏览知识共享许可协议

知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: hauk0101 原文链接:https://juejin.im/post/6856288219584331790

回到顶部