title: Vue源码解析之nextTick
date: 2020-11-09 19:30:36
tags:

  • vue
  • 源码
    categories: 学习
    copyright: false
    top_img: /img/bg2.png

目标是写给自己看的所以就写的稍微简单一点.

在vue中 , 有一种情况 :

  1. <template>
  2. <div>
  3. <div ref="name">{{ name }}</div>
  4. <button @click="updateName">updateName</button>
  5. </div>
  6. </template>
  7. <script>
  8. export default {
  9. data() {
  10. return {
  11. name: 'test1'
  12. }
  13. },
  14. methods: {
  15. updateName() {
  16. this.name = 'test2'
  17. console.log(this.$refs.name.textContent)//test1
  18. this.$nextTick(()=>{
  19. console.log(this.$refs.name.textContent)//test2
  20. })
  21. }
  22. }
  23. }
  24. </script>

如上代码 :

页面中显示了一个插值表达式{{name}},按钮绑定了一个updateName的函数,当点击按钮,会将name改为test2,这时候打印reftextContent,打印出的值为test1.

那我们想打印的肯定是vue响应式更新后的dom,理所当然的以为是test2 , 那这到底是为什么呢 ?

响应式

这里简单介绍下vue的响应式 :

当我们定义了一个组件时,就相应的创建了一个Watcher, 在组件里定义了一个data,这时候vue会对data里的数据进行拦截(通过Object.defineProperty),设置getset函数 ,

get函数:在读取的时候 , 创建一个dep对象,并且在dep对象下的sub对象上push watcher对象,产生依赖,同时在watcher添加dep的相关信息.

set函数:在设置的时候,改变值的同时执行notify,通知dep中所有的watcher执行update更新dom. 接下来虚拟dom这方面的知识.这里先不做拓展.只要知道,vue会将oldVNode和newVNode进行diff比对,算出哪里变了,然后就进行更新dom.

这里着重来看下watcherupdate方法.

  1. update () {
  2. /* istanbul ignore else */
  3. if (this.lazy) {
  4. this.dirty = true
  5. } else if (this.sync) {
  6. this.run()
  7. } else {
  8. queueWatcher(this)
  9. }
  10. }

很显然这里的queueWatcher就是核心函数,我们继续深入

  1. export function queueWatcher (watcher: Watcher) {
  2. const id = watcher.id
  3. if (has[id] == null) {
  4. has[id] = true
  5. if (!flushing) {
  6. queue.push(watcher)
  7. } else {
  8. // if already flushing, splice the watcher based on its id
  9. // if already past its id, it will be run next immediately.
  10. let i = queue.length - 1
  11. while (i > index && queue[i].id > watcher.id) {
  12. i--
  13. }
  14. queue.splice(i + 1, 0, watcher)
  15. }
  16. // queue the flush
  17. if (!waiting) {
  18. waiting = true
  19. if (process.env.NODE_ENV !== 'production' && !config.async) {
  20. flushSchedulerQueue()
  21. return
  22. }
  23. nextTick(flushSchedulerQueue)
  24. }
  25. }
  26. }

如上源码, 我们从第一句代码执行过来, 首先获取该 id = watcher.id; 然后判断该id是否存在 if (has[id] == null) {} , 如果已经存在则直接跳过,不存在则执行if语句内部代码, 并且标记哈希表has[id] = true;

用于下次检验。如果 flushing 为false的话, 则把该watcher对象push到队列中, 考虑到一些情况, 比如正在更新队列中的watcher时, 又有事件塞入进来怎么处理? 因此这边加了一个flushing来表示队列的更新状态。

如果加入队列到更新状态时,又分为两种情况:

  1. 这个watcher还没有处理, 就找到这个watcher在队列中的位置, 并且把新的放在后面, 比如如下代码:
  1. if (!flushing) {
  2. queue.push(watcher)
  3. }
  1. 如果watcher已经更新过了, 就把这个watcher再放到当前执行的下一位, 当前的watcher处理完成后, 立即会处理这个最新的。如下代码:
  1. else {
  2. // if already flushing, splice the watcher based on its id
  3. // if already past its id, it will be run next immediately.
  4. let i = queue.length - 1
  5. while (i > index && queue[i].id > watcher.id) {
  6. i--
  7. }
  8. queue.splice(i + 1, 0, watcher)
  9. }
  1. waiting 为false, 等待下一个tick时, 会执行刷新队列。 如果不是正式环境的话, 会直接 调用该函数 flushSchedulerQueue; (源码在: vue/src/core/observer/scheduler.js) 中。否则的话, 把该函数放入 nextTick 函数延迟处理。
  1. if (!waiting) {
  2. waiting = true
  3. if (process.env.NODE_ENV !== 'production' && !config.async) {
  4. flushSchedulerQueue()
  5. return
  6. }
  7. nextTick(flushSchedulerQueue)
  8. }

这里我们看到vue进行响应式更新的时候也是将更新函数放到nextTick中去执行 .

关于nextTick的原理 , 和JS事件循环的宏任务/微任务相关.如果对这一块不熟悉的朋友,建议先去恶补相关知识.(看别人的文章就可以).

nextTick源码

  1. /* @flow */
  2. /* globals MutationObserver */
  3. import { noop } from 'shared/util'
  4. import { handleError } from './error'
  5. import { isIE, isIOS, isNative } from './env'
  6. export let isUsingMicroTask = false
  7. const callbacks = []
  8. let pending = false
  9. function flushCallbacks () {
  10. pending = false
  11. const copies = callbacks.slice(0)
  12. callbacks.length = 0
  13. for (let i = 0; i < copies.length; i++) {
  14. copies[i]()
  15. }
  16. }
  17. // Here we have async deferring wrappers using microtasks.
  18. // In 2.5 we used (macro) tasks (in combination with microtasks).
  19. // However, it has subtle problems when state is changed right before repaint
  20. // (e.g. #6813, out-in transitions).
  21. // Also, using (macro) tasks in event handler would cause some weird behaviors
  22. // that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
  23. // So we now use microtasks everywhere, again.
  24. // A major drawback of this tradeoff is that there are some scenarios
  25. // where microtasks have too high a priority and fire in between supposedly
  26. // sequential events (e.g. #4521, #6690, which have workarounds)
  27. // or even between bubbling of the same event (#6566).
  28. let timerFunc
  29. // The nextTick behavior leverages the microtask queue, which can be accessed
  30. // via either native Promise.then or MutationObserver.
  31. // MutationObserver has wider support, however it is seriously bugged in
  32. // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
  33. // completely stops working after triggering a few times... so, if native
  34. // Promise is available, we will use it:
  35. /* istanbul ignore next, $flow-disable-line */
  36. if (typeof Promise !== 'undefined' && isNative(Promise)) {
  37. const p = Promise.resolve()
  38. timerFunc = () => {
  39. p.then(flushCallbacks)
  40. // In problematic UIWebViews, Promise.then doesn't completely break, but
  41. // it can get stuck in a weird state where callbacks are pushed into the
  42. // microtask queue but the queue isn't being flushed, until the browser
  43. // needs to do some other work, e.g. handle a timer. Therefore we can
  44. // "force" the microtask queue to be flushed by adding an empty timer.
  45. if (isIOS) setTimeout(noop)
  46. }
  47. isUsingMicroTask = true
  48. } else if (!isIE && typeof MutationObserver !== 'undefined' && (
  49. isNative(MutationObserver) ||
  50. // PhantomJS and iOS 7.x
  51. MutationObserver.toString() === '[object MutationObserverConstructor]'
  52. )) {
  53. // Use MutationObserver where native Promise is not available,
  54. // e.g. PhantomJS, iOS7, Android 4.4
  55. // (#6466 MutationObserver is unreliable in IE11)
  56. let counter = 1
  57. const observer = new MutationObserver(flushCallbacks)
  58. const textNode = document.createTextNode(String(counter))
  59. observer.observe(textNode, {
  60. characterData: true
  61. })
  62. timerFunc = () => {
  63. counter = (counter + 1) % 2
  64. textNode.data = String(counter)
  65. }
  66. isUsingMicroTask = true
  67. } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  68. // Fallback to setImmediate.
  69. // Technically it leverages the (macro) task queue,
  70. // but it is still a better choice than setTimeout.
  71. timerFunc = () => {
  72. setImmediate(flushCallbacks)
  73. }
  74. } else {
  75. // Fallback to setTimeout.
  76. timerFunc = () => {
  77. setTimeout(flushCallbacks, 0)
  78. }
  79. }
  80. export function nextTick (cb?: Function, ctx?: Object) {
  81. let _resolve
  82. callbacks.push(() => {
  83. if (cb) {
  84. try {
  85. cb.call(ctx)
  86. } catch (e) {
  87. handleError(e, ctx, 'nextTick')
  88. }
  89. } else if (_resolve) {
  90. _resolve(ctx)
  91. }
  92. })
  93. if (!pending) {
  94. pending = true
  95. timerFunc()
  96. }
  97. // $flow-disable-line
  98. if (!cb && typeof Promise !== 'undefined') {
  99. return new Promise(resolve => {
  100. _resolve = resolve
  101. })
  102. }
  103. }

如上代码,我们从上往下看,首先定义变量 callbacks = []; 该变量的作用是: 用来存储所有需要执行的回调函数。let pending = false; 该变量的作用是表示状态,判断是否有正在执行的回调函数。
也可以理解为,如果代码中 timerFunc 函数被推送到任务队列中去则不需要重复推送。

flushCallbacks() 函数,该函数的作用是用来执行callbacks里面存储的所有回调函数。如下代码:

timerFunc: 保存需要被执行的函数。

继续看接下来的代码,我们上面讲解过,在Vue中使用了几种情况来延迟调用该函数。

1. promise.then 延迟调用, 基本代码如下:

  1. if (typeof Promise !== 'undefined' && isNative(Promise)) {
  2. const p = Promise.resolve()
  3. timerFunc = () => {
  4. p.then(flushCallbacks)
  5. if (isIOS) setTimeout(noop)
  6. }
  7. isUsingMicroTask = true
  8. }

如上代码的含义是: 如果我们的设备(或叫浏览器)支持Promise, 那么我们就使用 Promise.then的方式来延迟函数的调用。Promise.then会将函数延迟到调用栈的最末端,从而会做到延迟。

2. MutationObserver 监听, 基本代码如下:

  1. else if (!isIE && typeof MutationObserver !== 'undefined' && (
  2. isNative(MutationObserver) ||
  3. // PhantomJS and iOS 7.x
  4. MutationObserver.toString() === '[object MutationObserverConstructor]'
  5. )) {
  6. // Use MutationObserver where native Promise is not available,
  7. // e.g. PhantomJS, iOS7, Android 4.4
  8. // (#6466 MutationObserver is unreliable in IE11)
  9. let counter = 1
  10. const observer = new MutationObserver(flushCallbacks)
  11. const textNode = document.createTextNode(String(counter))
  12. observer.observe(textNode, {
  13. characterData: true
  14. })
  15. timerFunc = () => {
  16. counter = (counter + 1) % 2
  17. textNode.data = String(counter)
  18. }
  19. isUsingMicroTask = true
  20. }

如上代码,首先也是判断我们的设备是否支持 MutationObserver 对象, 如果支持的话,我们就会创建一个MutationObserver构造函数, 并且把flushCallbacks函数当做callback的回调, 然后我们会创建一个文本节点, 之后会使用MutationObserver对象的observe来监听该文本节点, 如果文本节点的内容有任何变动的话,它就会触发 flushCallbacks 回调函数。那么要怎么样触发呢? 在该代码内有一个 timerFunc 函数, 如果我们触发该函数, 会导致文本节点的数据发生改变,进而触发MutationObserver构造函数。

3. setImmediate 监听, 基本代码如下:

  1. else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  2. // Fallback to setImmediate.
  3. // Techinically it leverages the (macro) task queue,
  4. // but it is still a better choice than setTimeout.
  5. timerFunc = () => {
  6. setImmediate(flushCallbacks)
  7. }
  8. }

如果上面的 Promise 和 MutationObserver 都不支持的话, 我们继续会判断设备是否支持 setImmediate, 我们上面分析过, 他属于 macrotasks(宏任务)的。该任务会在一个宏任务里执行回调队列。

4. 使用setTimeout 做降级处理

如果我们上面三种情况, 设备都不支持的话, 我们会使用 setTimeout 来做降级处理, 实现延迟效果。如下基本代码:

  1. else {
  2. // Fallback to setTimeout.
  3. timerFunc = () => {
  4. setTimeout(flushCallbacks, 0)
  5. }
  6. }

现在我们的源码继续往下看, 会看到我们的nextTick函数被export了,如下基本代码:

  1. export function nextTick (cb?: Function, ctx?: Object) {
  2. let _resolve
  3. callbacks.push(() => {
  4. if (cb) {
  5. try {
  6. cb.call(ctx)
  7. } catch (e) {
  8. handleError(e, ctx, 'nextTick')
  9. }
  10. } else if (_resolve) {
  11. _resolve(ctx)
  12. }
  13. })
  14. if (!pending) {
  15. pending = true
  16. timerFunc()
  17. }
  18. // $flow-disable-line
  19. if (!cb && typeof Promise !== 'undefined') {
  20. return new Promise(resolve => {
  21. _resolve = resolve
  22. })
  23. }
  24. }

如上代码, nextTick 函数接收2个参数,cb 是一个回调函数, ctx 是一个上下文。 首先会把它存入callbacks函数数组里面去, 在函数内部会判断cb是否是一个函数,如果是一个函数,就调用执行该函数,当然它会在callbacks函数数组遍历的时候才会被执行。其次 如果cb不是一个函数的话, 那么会判断是否有_resolve值, 有该值就使用Promise.then() 这样的方式来调用。比如: this.$nextTick().then(cb) 这样的使用方式。因此在下面的if语句内会判断赋值给_resolve:

  1. if (!cb && typeof Promise !== 'undefined') {
  2. return new Promise(resolve => {
  3. _resolve = resolve
  4. })
  5. }

使用Promise返回了一个 fulfilled 的Promise。赋值给 _resolve; 然后在callbacks.push 中会执行如下:

  1. _resolve(ctx);

以上就可以解释为什么

  1. this.name = 'test2'
  2. console.log(this.$refs.name.textContent)//test1
  3. this.$nextTick(()=>{
  4. console.log(this.$refs.name.textContent)//test2
  5. })
  1. 当我们执行script,主体代码的时候(主体代码属于宏任务),执行了this.name = 'test2'

  2. 这时候触发了更新,update将watcher的callback1推到了nextTick中,也就是微任务队列中

  3. 接下来执行console.log(this.$refs.name.textContent)这时候并没有执行到微任务,所以dom对象并没有改变,所以拿到的textNode还是test1.

  4. 执行this.$nextTick(),将callback2,继续推入微任务队列。

  5. 执行微任务队列。callback1 改变dom对象(页面渲染要在一次事件循环结束后,也就是微任务队列执行完毕后UI渲染刷新),callback2拿到修改后的dom对象,理所当然textContenttest2.(此时页面没有重新渲染刷新,分清时间点)

  6. 微任务队列执行完毕一次事件循环结束,页面重新渲染刷新.

宏任务: 主体代码

微任务: callback1 => callback2