浅析NextTick源码(一)

270 阅读2分钟

在日常开发中,偶尔会有这样的一种场景,翻看代码逻辑以及书写顺序都没有什么问题(至少作为新手的我是这么认为的),然后随手在没有实现预期效果的代码前面加上await setTimeout(100ms)或者await NextTick问题就解决了。

上述场景大多数出现于监听某个dom是否渲染完成,那么为什么会出现这种情况呢?为什么加个定时器或者NextTick方法就解决了呢?接下来就一起揭开它们的神秘面纱吧~

mounted

相信大家对这个生命周期不是很陌生,当Vue实例被挂载完成后触发该钩子函数。在官方文档中对这个钩子函数的描述还附加了一条注意点。

nextTick又被点名了,那就继续往下看吧。

NextTick简介

Defer the callback to be executed after the next DOM update cycle. Use it immediately after you’ve changed some data to wait for the DOM update.

--将回调推迟到下一个 DOM 更新周期之后执行。在更改了一些数据以等待 DOM 更新后立即使用它。

源码

vue2.x

github.com/vuejs/vue/b…

/* @flow */

/* globals MutationObserver */

// 中文注释为本人个人见解,仅供参考,如有误导请见谅。英文注释为源码注释。

/**

 * Perform no operation.

 * Stubbing args to make Flow happy without leaving useless transpiled code

 * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).

 * export function noop (a?: any, b?: any, c?: any) {}

 */

import { noop } from 'shared/util'

import { handleError } from './error'

import { isIE, isIOS, isNative } from './env'



export let isUsingMicroTask = false



const callbacks = []

let pending = false



// 备份callbacks数组并依次执行每个cb

function flushCallbacks () {

  pending = false

  // slice return 一个新的数组

  // 为什么要新建一个备份数组?

  const copies = callbacks.slice(0)

  callbacks.length = 0

  // 循环遍历,按照 队列 数据结构 “先进先出” 的原则,逐一执行所有 callback 。

  for (let i = 0; i < copies.length; i++) {

    copies[i]()

  }

}



// 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



// 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是否为原生func(检测兼容性)

if (typeof Promise !== 'undefined' && isNative(Promise)) {

  // 和 new Promise()的区别??

  const p = Promise.resolve()

  timerFunc = () => {

    p.then(flushCallbacks)

    // 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.

    // 个人理解:部分场景Promise没有办法完整执行完毕,加一个空的定时器即可解决该问题

    if (isIOS) setTimeout(noop)

  }

  isUsingMicroTask = true

}

// 由于MutationObserver为微任务,且兼容性比Promise强,但是处理比较繁琐(猜的),所以它放在Promise的后面。

else if (!isIE && typeof MutationObserver !== 'undefined' && (

  isNative(MutationObserver) ||

  // PhantomJS and iOS 7.x

  MutationObserver.toString() === '[object MutationObserverConstructor]'

)) {

  // Use MutationObserver where native Promise is not available,

  // e.g. PhantomJS, iOS7, Android 4.4

  // (#6466 MutationObserver is unreliable in IE11)

  let counter = 1

  //MutationObserver - Web API 接口参考 | MDN

  //创建并返回一个新的 MutationObserver 它会在指定的DOM发生变化时被调用。

  const observer = new MutationObserver(flushCallbacks)

  const textNode = document.createTextNode(String(counter))

  observer.observe(textNode, {

    //https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserverInit

    //设为 true 以监视指定目标节点或子节点树中节点所包含的字符数据的变化。无默认值。

    characterData: true

  })

  timerFunc = () => {

    counter = (counter + 1) % 2

    textNode.data = String(counter)

  }

  isUsingMicroTask = true

}

//宏任务,兼容性极差,但是由于SetTimeOut不设置time也会有默认的4ms缺省值,故放在其前 

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)

  }

} else {

  // Fallback to setTimeout.

  timerFunc = () => {

    setTimeout(flushCallbacks, 0)

  }

}



export function nextTick (cb?: Function, ctx?: Object) {

  let _resolve

  callbacks.push(() => {

    if (cb) {

      try {

        cb.call(ctx)

      } catch (e) {

        handleError(e, ctx, 'nextTick')

      }

    } else if (_resolve) {

      _resolve(ctx)

    }

  })

  if (!pending) {

    pending = true

    timerFunc()

  }

  // $flow-disable-line

  if (!cb && typeof Promise !== 'undefined') {

    return new Promise(resolve => {

      _resolve = resolve

    })

  }

}

vue3.x

github.com/vuejs/vue-n…

export function nextTick(

  this: ComponentPublicInstance | void,

  fn?: () => void

): Promise<void> {

  const p = currentFlushPromise || resolvedPromise

  return fn ? p.then(this ? fn.bind(this) : fn) : p

}

Q&A

为什么要新建一个备份数组?

由于callbacks为全局变量,只要调用一次NextTick函数就会对callbacks进行push操作,当nextTick嵌套使用时,flushCallbacks通过拷贝并清空原数组的方式可以保证每次循环执行的都是当前nextTick的cb入参。

Const p = promise.resolve() 和 const p = new Promise(r=>r())的区别?

segmentfault.com/a/119000002…

为什么选择先微任务后宏任务的降级处理?

优先选择微任务原因是当使用nextTick时可以确保队列中的微任务能在一次事件循环前执行完毕,如果选择宏任务的话,则需要等待当前任务队列执行完毕后在下一次事件循环中才能执行nextTick中的callback。

参考文献

github.com/vuejs/vue/b…

Vue-nextTick源码解析

全面解析Vue.nextTick实现原理

developer.mozilla.org/zh-CN/docs/…

developer.mozilla.org/zh-CN/docs/…

developer.mozilla.org/zh-CN/docs/…

vuejs/vue-next

vue2和vue3的nextTick实现的不同方式

nextTick | Vue3