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


相关文章
|
5月前
|
JavaScript 前端开发 Serverless
Vue.js的介绍、原理、用法、经典案例代码以及注意事项
Vue.js的介绍、原理、用法、经典案例代码以及注意事项
161 2
|
3月前
|
JavaScript 算法 编译器
vue3 原理 实现方案
【8月更文挑战第15天】vue3 原理 实现方案
45 1
|
20天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
33 9
|
2月前
|
缓存 JavaScript 前端开发
「offer来了」从基础到进阶原理,从vue2到vue3,48个知识点保姆级带你巩固vuejs知识体系
该文章全面覆盖了Vue.js从基础知识到进阶原理的48个核心知识点,包括Vue CLI项目结构、组件生命周期、响应式原理、Composition API的使用等内容,并针对Vue 2与Vue 3的不同特性进行了详细对比与讲解。
「offer来了」从基础到进阶原理,从vue2到vue3,48个知识点保姆级带你巩固vuejs知识体系
|
1月前
|
JavaScript UED
Vue双向数据绑定的原理
【10月更文挑战第7天】
|
22天前
|
JavaScript 前端开发 API
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
24 0
|
2月前
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy
该文章对比了Vue2与Vue3在响应式原理上的不同,重点介绍了Vue3如何利用Proxy替代Object.defineProperty来实现更高效的数据响应机制,并探讨了这种方式带来的优势与挑战。
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy
|
2月前
|
开发框架 JavaScript 前端开发
手把手教你剖析vue响应式原理,监听数据不再迷茫
该文章深入剖析了Vue.js的响应式原理,特别是如何利用`Object.defineProperty()`来实现数据变化的监听,并探讨了其在异步接口数据处理中的应用。
|
2月前
|
缓存 JavaScript 容器
vue动态组件化原理
【9月更文挑战第2天】vue动态组件化原理
45 2
|
3月前
|
缓存 JavaScript 前端开发
[译] Vue.js 内部原理浅析
[译] Vue.js 内部原理浅析