nextTick 的实现中,首先申明了 timerFunc 变量,然后会去检测环境来决定最终函数的实现方式。优先实现为promise的版本。
-
为什么优先实现为微任务版本?
根据 HTML Standard,在每个 task 运行完以后,UI 都会重渲染,那么在 microtask 中就完成数据更新,当前 task 结束就可以得到最新的 UI 了。如果新建一个 task 来做数据更新,那么渲染就会进行两次。
继续看 nextTick(),它的逻辑也很简单,把传入的回调函数 cb 压入 callbacks 数组,然后执行 timerFunc 时执行 flushCallbacks。flushCallbacks 的逻辑非常简单,对 callbacks 遍历,然后执行相应的回调函数。
-
为什么使用
callbacks而不是直接在nextTick中执行回调函数?原因是保证在同一个 tick 内多次执行
nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。
export let isUsingMicroTask = false
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]()
}
}
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等)、用户交互事件等。
这基本上就是事件循环了,然后为什么又引入微任务,把任务区分为宏任务、微任务呢?
原因是为了高优先级的任务先处理。比如 DOM 节点的修改,如果直接塞到消息队列的尾部,那么实时性很差,因为要等前面的任务执行完毕;而如果做成同步任务,那么当前任务的执行时间会被拉长,从而导致执行效率下降。
针对这种情况,微任务就应运而生了。把消息队列中的任务称为宏任务,并且每个宏任务中包含了一个微任务队列。等宏任务执行完毕后,检查并执行微任务队列的微任务。
微任务系统本质上是 V8 引擎创建全局执行上下文的同时,也会在其内部创建一个微任务队列。产生微任务有两种方式:
一种是使用 MutationObserver 监控某个 DOM 节点,当 DOM 节点变化时就会产生 DOM 变化记录的微任务。
第二种是使用 Promise,当调用 resolve() 或者 reject() 的时候会产生微任务。
\