在吗?花5分钟来学习下Vue的$nextTick的原理呗

3,249 阅读1分钟

在深入$nextTick原理之前,我们需要先来简单了解下浏览器的事件循环机制。

浏览器事件循环机制

我们都知道,JavaScript是单线程的,也就是说,JavaScript在执行代码的时候,只有一个主线程来处理所有的任务。 那么要怎么处理异步代码(如I/O操作)呢?那就是我们即将要说的事件循环机制啦。

执行栈和任务队列

JavaScript在执行代码的时候,是从上往下按顺序执行代码的。

当遇到同步任务的时候,会把同步任务加入执行栈中执行;

遇到异步任务的时候,不会立刻执行,而是加入到任务队列中,等待栈中同步任务都执行完之后,再执行任务队列中的异步任务。

宏任务(Macro Task)和微任务(Micro Task)

异步任务又分为宏任务微任务两种。

异步宏任务包括:setTimeout, setInterval, setImmediate, I/O, UI Rendering

异步微任务包括:process.nextTick, Promise, MutationObserver, Object.observe

事件循环过程

  1. 处理同步任务,依次把同步任务加入到执行栈中执行
  2. 等待执行栈中的同步任务都处理完成,处理任务队列
  3. 按顺序执行所有微任务
  4. 进行必要的UI渲染
  5. 执行宏任务中的异步代码
  6. 开始下一轮事件循环

$nextTick

Vue的$nextTick也是利用事件循环机制来实现在数据更新和视图渲染完成之后执行异步任务的。

我们先来看下$nextTick的主要函数是怎么样的吧:

...
function flushCallbacks () {
  // 执行所有回调函数
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
...
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()
  }
  
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

timerFunc则会根据浏览器的支持情况,来选择合适的异步任务执行方式。

我们从事件循环的过程中可以知道,执行完微任务之后会有必要的UI渲染,然后再执行异步的宏任务。

也就是说,如果我们想要在视图渲染完成后尽快执行回调函数,我们最好选择微任务来处理异步任务,如果浏览器不支持的话我们再选择宏任务的方式来执行。

然后我们来看下timerFunc是如何定义的:

let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // 如果支持Promise,则使用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]'
)) {
  // 使用MutationObserver
  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)) {
  // 使用setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 使用setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

总结

$nextTick使用事件循环的原理来实现在视图渲染完成后执行所有回调函数的功能。

而且,为在视图渲染完成后尽快执行回调,$nextTick根据浏览器的兼容性来渲染执行异步任务的方法,先检查是否能使用微任务(Promise, MutationObserver),不行的话则使用宏任务(setImmediate, setTimeout)。

😉如果文章对你有所帮助的话,请帮忙点亮旁边的那颗大拇指呗,有问题的欢迎在评论区讨论。