4

这个 api 的源码很早就看过,只不过一直没有总结,因此这边总结一下。

这个 api 是怎么使用的

nextTick 在 Vue 中有两种用法,一种是作为全局方法 Vue.nextTick 使用,还有一种是挂载在组件实例上,通过 vm.$nextTick 的方式使用。作为实例方法调用的时候,回调的 this 自动绑定到调用它的实例上。

参数:

// {Function} [callback]
// {Object} [context]
Vue.nextTick([callback, context])

用法:

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
  // DOM 更新了
})

2.1.0 起新增:如果没有提供回调且在支持 Promise 的环境中,则返回一个 Promise 。

// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
  .then(function () {
    // DOM 更新了
  })

源码解析

Vue.nextTick 的源码在这个目录下:

node_modules/vue/src/core/util/next-tick.js

这边的代码推荐从第 33 行开始看:

let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  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
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

这部分的逻辑非常简单,就是嗅探环境,依次去检测 Promise -> MutationObserver -> setImmediate -> setTimeout 是否存在,找到存在的就使用它,以此来确定回调函数队列是以哪个 api 来异步执行。

然后再看第 87 行 nextTick 方法定义:

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
    })
  }
}

在上面的代码中,因为要实现新增的返回 Promise 的逻辑,所以略显复杂。可以直接看下面简化的逻辑:

export function nextTick (cb?: Function, ctx?: Object) {
  callbacks.push(() => {
    try {
      cb.call(ctx)
    } catch (e) {
      handleError(e, ctx, 'nextTick')
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
}

nextTick 函数接收到一个回调函数的时候,先不去调用它,而是把它 push 到一个全局的 callbacks 数组中。在 nextTick 方法最后,调用了 timerFunc 方法,这个方法其实就是刚才嗅探环境的时候选择的 api ,以 Promise 为例,timerFunc 方法应该是长这样:

const p = Promise.resolve()
let timerFunc = () => {
  p.then(flushCallbacks)
  if (isIOS) setTimeout(noop)
}

当调用了 timerFunc 之后,flushCallbacks 方法会在下一轮事件循环被执行。我们再来看 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]()
  }
}

flushCallbacks 做的事情很简单,就是把 callbacks 数组里面的方法全部执行一遍,然后清空 callbacks

到这里细心的同学可能会问,nextTick 里面的 pending 是做什么用了,一开始为 false ,然后调用 nextTick 的时候改为 true ,最后 flushCallbacks 执行的时候再改为 false ,咋一看好像没啥用。其实不然,我们知道,nextTick 方法是可以同步方法中多次调用的:

export default {
  methods: {
    handleClick() {
      this.$nextTick(() => {
        console.log(233);
      })
      this.$nextTick(() => {
        console.log(666);
      })
    }
  }
}

上面的代码中,第一个 nextTick 调用,回调函数被 push 进 callbacks 数组,然后调用 timerFunc 加入异步队列。当第二个 nextTick 调用的时候,回调函数被 push 进 callbacks 数组,因为这个时候 pending 已经是 true 了,所以 timerFunc 不会重复执行了,也就是不会重复添加异步队列。只有在下一轮事件循环中,异步队列中的函数都被执行之后,pending 变为 false ,这时候才能再次添加异步队列。

那么总结一下,nextTick 函数接收到一个回调函数的时候,先不去调用它,而是放到一个全局 queue 队列中,等待下一轮事件循环的时候把这个 queue 的函数依次执行。

这个队列可能是 microTask 队列,也可能是 macroTask 队列。PromiseMutationObserver 属于微任务队列,setImmediatesetTimeout 属于宏任务队列。

参考

Vue.nextTick - Vue 官方文档


一杯绿茶
199 声望17 粉丝

人在一起就是过节,心在一起就是团圆