vue2源码系列-nextTick实现原理

简介: nextTick实现nextTick 作为 vue 的全局 api 之一,想必大家都非常熟悉。我们在上篇文章 深入Watcher 分析异步 watcher 的时候也是利用了 nextTick 来实现异步执行。今天我们就来分析分析 nextTick 的实现原理。

nextTick实现


nextTick 作为 vue 的全局 api 之一,想必大家都非常熟悉。我们在上篇文章 深入Watcher 分析异步 watcher 的时候也是利用了 nextTick 来实现异步执行。今天我们就来分析分析 nextTick 的实现原理。


任务队列


如果想要更好的理解 nextTick,需要补充下任务队列的知识储备,感兴趣的可以看看之前的文章 JS事件循环(Event Loop),这边就不过多分析了。


nextTick源码


nextTick 的实现源码在 src/core/tuil/next-tick.js 中,我们撸起袖子加油干,直接对其进行分析


// 辅助函数
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
// 是否使用微任务
export let isUsingMicroTask = false
// 回调函数队列
const callbacks = []
// 回调函数队列的执行状态
let pending = false
// 触发队列执行
function flushCallbacks () {
  // 改为执行状态
  pending = false
  // 这边就是对回调队列进行遍历执行
  // 注意这边会对回调函数进行浅复制
  // 因为在回调函数也会往callbacks中添加新值
  // 但是我们将它们归到下一个异步中执行
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
// 这边的源码注释比较多不舍得删了
// 英文较差(如我)的可以借助翻译工具看看
// 大致就是解释了为什么nextTick异步实现选择微任务来实现以及改版历史
// 最先的时候是是否微任务来实现的但是后面发现了out-in transitions出现的bug
// 后面改为宏任务(MessageChannel )与微任务结合的方式但是出现了更多的小问题
// 比较经典的就是我们知道有些事件需要用户手动点击触发 比如播放video
// 但是将这个播放事件作为宏任务放到下个异步中可能就不再执行了
// 所以后面又改为全部优先使用微任务实现的方式
// 当然微任务由于触发优先级很高也会导致某些问题 比较典型的#6566 就是由于微任务的优先级比事件冒泡还高导致的
// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc
// 下面就是异步函数timerFunc的实现
// 注释也比较清楚了分析了使用不同实现的优劣
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
// 优先使用微任务promise.then
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    // 在then回调中调用flushCallbacks 实现了异步调用flushCallbacks 下面的其它调用也是如此
    p.then(flushCallbacks)
    // 修复IOS的兼容性BUG
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  // 表明使用微任务
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // 使用MutationObserver来实现异步微任务
  // MutationObserver的使用这边就不分析了
  // 大家可以看看MDN教程 https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver/MutationObserver
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  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
// 降级使用宏任务setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
// 最后没办法降级使用宏任务setTimeout
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
// 暴露的nextTick实现
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 其实调用nextTick就是往回调队列中添加回调cb
  // 在flushCallbacks中进行遍历执行
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    // 这边有个条件分支
    // 如果没有cb参数则执行_resolve 而_resolve实际是Promise实例的resolve函数
    // 所以我们可以在代码中调用nextTick().then()这样的写法
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 如果不是正在执行态则立即执行timerFunc
  // 调用timerFunc实现 timerFunc(微任务/宏任务) -> flushCallbacks -> cb.call(ctx) 的完整流程
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
复制代码


梳理


经过上面的源码分析,想必大家对 nextTick 的实现都理解的差不多了,下面我们将梳理下学习到的知识点


nextTick的实现原理


nextTick 的实现实际也是个 订阅发布模式 的实现。我们通过调用 nextTick 往任务队列 callBacks 添加回调函数,实现订阅。


同时调用 timerFunc 来开启一个异步任务。


当异步任务结束时调用函数 flushCallbacks,在 flushCallbacks 中再遍历调用任务队列,实现通知。


异步执行的实现


nextTick 的异步任务根据不同的浏览器支持情况,采用一个降级的方式 Promise.resolive().then -> MutationObserver -> setImmediate -> setTimeout,优先使用微任务 Promise.resolive().thenMutationObserver


nextTick方法的返回值


通过源码的阅读我们可以发现,nextTick(cb).then(() => {}) 这样的调用方式是需要 cb 未定义才能这样调的。


// true
nextTick().then(() => {})
// error
nextTick(() => {}).then(() => {})
复制代码


为什么需要使用nextTick


现在我们再来聊一聊为什么需要使用 nextTick


平常我们调用 nextTick 比较多的场景是当某个数据修改之后需要实时获取 DOM,举个例子

this.showXXEl = true;
this.$refs.XX; // undefined
this.nextTick(()  => {
  this.$refs.XX; // DOMElement
})
复制代码

为什么会出现这样的问题呢?我们继续梳理下


  1. 我们在渲染组件的时候会创建 渲染watcher,其订阅了某些数据属性的 dep
  2. 手动修改数据 this.showXXEl = true 触发 dep 通知

  3. watcher 接收到通知,通过 queueWatcher 进行更新函数排队
  4. queueWatcher 中调用 nextTick 进行异步等待

  5. 开发者函数中的同步代码此时继续执行 this.$refs.XX 所有抛出 undefined
  6. 开发者手动调用 nextTick(() => { this.$refs.XX }) 进行异步等待

  7. 同步代码执行结束后,异步任务执行

  8. 开始执行 nextTick 中的回调队列,按照先进先出的队列顺序,

  9. 执行 watcher 的更新函数 updateComponent 完成渲染

  10. 执行开发者定义的异步回调 () => { this.$refs.XX },在上一步已经渲染的情况下,这边就能拿到 this.$refs.XXDOM

小结


究其原因就是 watcher 的通知使用了异步队列 queueWatcher,所以只有在当前 nextTick 异步等待之后才会去执行 watcher 的回调进行渲染之类的操作。而我们手动调用 nextTick 则可将自定义回调排在 watcher 回调之后,也就可以获取到最新页面及数据了


总结

nextTick 的实现并不复杂,相对于前面的文章来说应该也是比较简单的一篇,所以在这对其分析的也比较透彻一些。至此关于响应式原理的部分就结束了,后面将分析 Vue 组件化的实现。

good good staduy day day up


相关文章
|
2天前
|
JavaScript
Vue3中props的原理与使用
Vue3中props的原理与使用
10 0
|
2天前
|
JavaScript 前端开发 开发者
响应式原理:Vue 如何跟踪数据变化
【4月更文挑战第22天】Vue 的响应式系统是其核心,通过数据双向绑定实现视图与数据同步。依赖收集和观测数据使Vue能跟踪变化,变化通知组件更新视图。高效的更新策略如批量更新和虚拟DOM提升性能。组件化和可组合性支持有效通信和代码复用,强调数据驱动开发。开发者应合理组织数据、谨慎处理变更并充分利用组件化优势,以提高效率和用户体验。
|
2天前
|
JavaScript API
Vue3的响应式原理
Vue 3 中的响应式原理是通过使用 ES6 的 `Proxy 对象`来实现的**。在 Vue 3 中,每个组件都有一个响应式代理对象,当组件中的数据发生变化时,代理对象会立即响应并更新视图。
|
2天前
|
JavaScript 前端开发
vue中nextTick使用以及原理
vue中nextTick使用以及原理
7 0
|
2天前
|
JavaScript 前端开发
深入了解前端框架Vue.js的响应式原理
本文将深入探讨Vue.js前端框架的核心特性之一——响应式原理。通过分析Vue.js中的数据绑定、依赖追踪和虚拟DOM等机制,读者将对Vue.js的响应式系统有更深入的理解,从而能够更好地利用Vue.js构建灵活、高效的前端应用。
|
2天前
|
开发框架 JavaScript 算法
了解vue3的基本特性和底层原理
Vue3的底层原理涵盖了响应式系统的Proxy-based实现、组件的模板编译与渲染更新机制、组合式API带来的逻辑组织变革,以及其他关键特性的具体实现。这些原理共同构成了Vue3强大、高效、灵活的现代前端开发框架基础。
28 2
|
2天前
|
JavaScript
Vue3中props的原理与使用
Vue3中props的原理与使用
|
2天前
|
JavaScript 前端开发 开发者
Vue的响应式原理:深入探索Vue的响应式系统与依赖追踪
【4月更文挑战第24天】Vue的响应式原理通过JavaScript getter/setter实现,当数据变化时自动更新视图。它创建Watcher对象收集依赖,并通过依赖追踪机制精确通知更新。当属性改变,setter触发更新相关Watcher,重新执行操作以反映数据最新状态。Vue的响应式系统结合依赖追踪,有效提高性能,简化复杂应用的开发,但对某些复杂数据结构需额外处理。
|
2天前
|
JavaScript 前端开发 API
Vue中v-model的原理
Vue中v-model的原理
|
2天前
|
JavaScript 前端开发 API
vue中的ref/reactive区别及原理
vue中的ref/reactive区别及原理
19 0