Vue响应式源码篇(三)nextTick和事件循环

169 阅读3分钟

nextTick 的实现中,首先申明了 timerFunc 变量,然后会去检测环境来决定最终函数的实现方式。优先实现为promise的版本。

  • 为什么优先实现为微任务版本?

    根据 HTML Standard,在每个 task 运行完以后,UI 都会重渲染,那么在 microtask 中就完成数据更新,当前 task 结束就可以得到最新的 UI 了。如果新建一个 task 来做数据更新,那么渲染就会进行两次。

继续看 nextTick(),它的逻辑也很简单,把传入的回调函数 cb 压入 callbacks 数组,然后执行 timerFunc 时执行 flushCallbacksflushCallbacks 的逻辑非常简单,对 callbacks 遍历,然后执行相应的回调函数。

  • 为什么使用 callbacks 而不是直接在 nextTick 中执行回调函数?

    原因是保证在同一个 tick 内多次执行 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。

export let isUsingMicroTask = falseconst callbacks = []
let pending = falsefunction flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
​
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)
  }
}
​
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
    })
  }
}

事件循环

浏览器渲染进程的主线程非常繁忙,要做的事情很多,比如解析执行js代码,渲染UI,用户交互事件(如鼠标点击)等。

为了有条不紊的处理这些任务于是引入了事件循环系统和消息队列

事件循环系统就是渲染进程的主线程上形成一个执行栈,用来执行所有同步任务,相当于while(true)循环,然后不断取出消息队列中的任务来进行执行。

消息队列的任务有哪些呢?js脚本、定时器、渲染事件(解析html,css等)、用户交互事件等。

ef9c43f306a16e9395ad892ce659a13.png

这基本上就是事件循环了,然后为什么又引入微任务,把任务区分为宏任务、微任务呢?

原因是为了高优先级的任务先处理。比如 DOM 节点的修改,如果直接塞到消息队列的尾部,那么实时性很差,因为要等前面的任务执行完毕;而如果做成同步任务,那么当前任务的执行时间会被拉长,从而导致执行效率下降。

针对这种情况,微任务就应运而生了。把消息队列中的任务称为宏任务,并且每个宏任务中包含了一个微任务队列。等宏任务执行完毕后,检查并执行微任务队列的微任务。

微任务系统本质上是 V8 引擎创建全局执行上下文的同时,也会在其内部创建一个微任务队列。产生微任务有两种方式:

一种是使用 MutationObserver 监控某个 DOM 节点,当 DOM 节点变化时就会产生 DOM 变化记录的微任务。

第二种是使用 Promise,当调用 resolve() 或者 reject() 的时候会产生微任务。

\