Vue.nextTick 如果和 Promise 比谁先执行?

1,665 阅读1分钟
 mounted () {
    this.count++
    Promise.resolve().then(() => {
      console.log('promise')
    })
    this.$nextTick(() => {
      console.log('nextTick')
    })
  },

输出顺序是啥?看上去是两个微任务嘛,promise nextTick?不对!

Vue.nextTick 源码回顾

源码路径 src/core/util/next-tick.js,以下版本为 2.6.11

判断顺序:1、Promise 2、MutationObserver 3、setImmediate 4、setTimeout 优先尝试 microtask 再 macrotask

let timerFunc
// 1、Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
  // 2、MutationObserver
} else if (
  !isIE &&
  typeof MutationObserver !== 'undefined' &&
  (isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    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
  // 3、setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
  // 4、setTimeout
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

可以看到 vue 的 nextTick 也正是利用 eventloop 的 microtask 的 Promise 和 MutationObserver,如果不支持再降级为 setImmediate、setTimeout

但是为什么一定要是 nextTick 之后才能拿到 dom 修改呢?我直接Promise.resolve().then可不可以呢?我们知道 vue 的响应机制是 get 时做依赖收集,在 set 时通知更新,通过源码我们发现 vue 为了优化会合并更新,最后也是调用的 nextTick

// 1、src/core/observer/watcher.js
  run () {
    this.cb.call(this.vm, value, oldValue) // 真正的dom更新回调方法
  }
  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

// 2、src/core/observer/scheduler.js
const queue: Array<Watcher> = []
function flushSchedulerQueue () {
   for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    watcher.run()
   }
}
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      // 也是调用的 nextTick
      nextTick(flushSchedulerQueue)
    }
  }
}
// 3、nextTick(flushSchedulerQueue)
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 先往callbacks塞一个匿名回调
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    // 触发异步promise或者降级方案,异步回调就是flushCallbacks清空callbacks
    timerFunc() 
  }
}

// 4、timerFunc =》flushCallbacks
const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]() // 就是遍历执行
  }
}

所以其实是所有的数据的 set 都会通知 watcher.update,到了 update 的时候,watcher 却不着急,watcher 先排起队来,然后在 nextTick 时 flushSchedulerQueue 批量清空 watcher 执行 watcher.run.cb 才会有真实的 dom 更新

原来 vue 数据更新是异步的,也是调用了 nextTick,而 nextTick,执行时它也不着急,先把回调收集到 callbacks 中,然后启用一个 mcirotask 或者 macrotask 来统一 flushCallbacks,大概这么个包含关系

mcirotask[
  flushCallbacks(
    [
      flushSchedulerQueue([watcher,watcher,watcher]) // 数据修改触发的watcher
      user_nextTick_cb, // 用户主动调用$nextTick的回调
      user_nextTick_cb,// 用户主动调用$nextTick的回调
    ]
  )
]

由此可见,修改数据后拿不到真实 dom 的原因,其实是更新 dom 的回调压根还没执行,在 watcher 队列里等着呢,等事件循环清理 mcirotask 时才会真的执行,而用户主动写的 user_nextTick_cb 刚好排在后面才能拿到修改的真实 dom

那么如果用户 Promise.resolve().then(()=>{}) 在 then 回调里去拿 dom 呢,队列大概会这样

mcirotask[
  flushCallbacks(
    [
      flushSchedulerQueue([watcher,watcher,watcher]) // 数据修改触发的watcher
      user_nextTick_cb, // 用户主动调用$nextTick的回调
    ]
  ),
  user_promise_then_cb // 在这里,它是一个新的microtask
]

所以也能拿到真实 dom,根据上面的理解,可以得到一个有意思的面试题

<template>
  <div id="app">
    {{count}}
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  mounted () {
    this.count++
    Promise.resolve().then(() => {
      console.log('promise')
    })
    this.$nextTick(() => {
      console.log('nextTick')
    })
  },
}
</script>

输出顺序?

nextTick
promise

如果把 this.count++ 注释掉,或者模板未使用这个 count 绑定,顺序就是

promise
nextTick

看看是否理解?

如果模板里有 count 绑定,执行 render 函数时就会有 watcher 收集,修改 count 时就会通知这个 watcher.update 就会调用一次 nextTick,以上代码的队列大概会是这样

mcirotask[
  flushCallbacks(
    [
      flushSchedulerQueue([watcher]) // 1. 修改count的更新watcher
      user_nextTick_cb, // 3. 用户主动调用$nextTick的回调,它是被直接push到callbacks里的
    ]
  ),
  user_promise_then_cb // 2. 它是一个新的microtask
]

所以是先执行 flushCallbacks 输出 nextTick,再是 promise

如果注释掉 this.count++,那么顺序就是 promise 再 nextTick

mcirotask[
  user_promise_then_cb // 1. 它是一个新的microtask,
  flushCallbacks(
    [
      user_nextTick_cb, // 2. 用户主动调用$nextTick的回调
    ]
  ),
]