Vue中nextTick原理解析(附源码解读)

来点鸡汤

男人一事无成时的温柔是最廉价的segmentfault

前言

在理解nextTick以前,咱们先要了解js的运行机制浏览器

js运行机制

js执行是单线程的,基于事件循环,事件循环大体分为如下几个步骤:bash

(1)全部同步任务都在主线程上执行,造成一个执行栈(execution context stack)。异步

(2)主线程以外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件,能够看出,这个任务队列主要存放异步任务函数

(3)一旦"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。ui

(4)主线程不断重复上面的第三步。this

主线程的执行过程就是一个 tick,而全部的异步结果都是经过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,而且每一个 macro task 执行结束后,都要检查一遍micro task,并执行清理完成再进入下一轮事件循环。

传送门:segmentfault.com/a/119000001…spa

nextTick

它的源码很简单,
源码:src/core/util/next-tick.js线程

export let isUsingMicroTask = false
/* 存放异步执行的回调 */
const callbacks = []
/* 标记位,若是已经有timerFunc被推送到任务队列中去则不须要重复推送 */
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]()
  }
}

/* 一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timeFunc将被调用 */
let timerFunc
/* 兼容Promise的浏览器 */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
  }
  /* 执行微任务标志位 */
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  /* 不支持Promise则使用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)) {
  /* 不支持Promise和MutationObserver则使用setImmediate */
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  /* 不然都使用setTimeout */
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
/* 延迟一个任务使其异步执行,在下一个tick时执行,这个函数的做用是在task或者microtask中推入一个timerFunc,在当前调用栈执行完之后以此执行直到执行到timerFunc,目的是延迟到当前调用栈执行完之后执行 */
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)
    }
  })
  /* 已经有timerFunc被推送到任务队列中去则不须要重复推送 */
  if (!pending) {
    pending = true
    timerFunc()
  }
  /* nextTick没有传回调函数时,返回一个Promise,咱们能够nextTick().then(() => {}) */
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

复制代码

解析:能够看出,nextTick在内部对环境作了兼容,首先会检测是否支持微任务:Promise,支持的话则把咱们传入的回调押入队列中在下一个tick中等待执行(flushCallbacks依次执行回调),若不支持则使用MutationObserver,如以上二者都不支持则使用宏任务:setImmediate和setTimeout。
最后若是咱们没有给nextTick传入回调,则能够nextTick().then(() => {})跳到then逻辑中。指针

总结

结合上一节的 setter 分析,咱们了解到数据的变化到 DOM 的从新渲染是一个异步过程,发生在下一个 tick。这就是咱们平时在开发的过程当中,好比从服务端接口去获取数据的时候,数据作了修改,若是咱们的某些方法去依赖了数据修改后的 DOM 变化,咱们就必须在 nextTick 后执行。好比下面的伪代码:

getData(res).then(()=>{
  this.xxx = res.data
  this.$nextTick(() => {
    // 这里咱们能够获取变化后的 DOM
  })
})
复制代码
相关文章
相关标签/搜索