microtask 这一名词是 JS 中比较新的概念,几乎全部人都是在学习 ES6 的 Promise 时才接触这一新概念,我也不例外。当我刚开始学习 Promise 的时候,对其中回调函数的执行方式特别着迷,因而乎便看到了 microtask 这一个单词,可是困难的是国内不多有关于这方面的文章,有一小部分人探讨过不过对其中的原理和机制的讲解也是十分晦涩难懂。直到我看到了 Jake Archibald 的文章,我才对 microtask 有了一个完整的认识,因此我便想把这篇文章翻译过来,供你们学习和参考。html
本篇文章绝大部分翻译自 Jake Archibald 的文章 Tasks, microtasks, queues and schedules。有英文功底的同窗建议阅读原著,毕竟人家比我写的好...git
适合人群:有必定的 JavaScript 开发基础,对 JavaScript Event Loop 有基本的认识,掌握 ES6 Promise 。github
让咱们先来看一段代码,猜猜它将会以何种顺序输出:web
1 console.log('script start'); 2 3 setTimeout(function() { 4 console.log('setTimeout'); 5 }, 0); 6 7 Promise.resolve().then(function() { 8 console.log('promise1'); 9 }).then(function() { 10 console.log('promise2'); 11 }); 12 13 console.log('script end');
你能够在这里查看输出结果:编程
正确的答案是:'script start'、'script end'、'promise1'、'promise2'、'setTimeout'。可是不一样的浏览器可能会出现不一样的输出顺序。api
Microsoft Edge, FireFox 40, iOS Safari 以及 Safari 8.0.8 将会在 'promise1' 和 'promise2' 以前输出 'setTimeout'。可是奇怪的是,FireFox 39 和 Safari 8.0.7 却又是按照正确的顺序输出。promise
要理解上面代码的输出原理,你就须要了解 JavaScript 的 event loop 是如何处理 tasks 以及 microtasks,当你第一次看到这一堆概念的时候,相信你也是和我同样的一头雾水,别急,让咱们先深呼吸一下,而后开始咱们的 microtask 之旅。浏览器
每个“线程”都有一个独立的 event loop,每个 web worker 也有一个独立的 event loop,因此它能够独立的运行。若是不是这样的话,那么全部的窗口都将共享一个 event loop,即便它们能够同步的通讯。event loop 将会持续不断的,有序的执行队列中的任务(tasks)。每个 event loop 都有着众多不一样的任务来源(task source),这些 task source 可以保证其中的 task 可以有序的执行(参见标准 Indexed Database API 2.0)。不过,在每一轮事件循环结束以后,浏览器能够自行选择将哪个 source 当中的 task 加入到执行队列当中。这样也就使得了浏览器能够优先选择那些敏感性的任务,例如用户的的输入。(看完这段话,估计大部分人都晕了,别急... be patient)app
Task 是严格按照时间顺序压栈和执行的,因此浏览器可以使得 JavaScript 内部任务与 DOM 任务可以有序的执行。当一个 task 执行结束后,在下一个 task 执行开始前,浏览器能够对页面进行从新渲染。每个 task 都是须要分配的,例如从用户的点击操做到一个点击事件,渲染HTML文档,同时还有上面例子中的 setTimeout。dom
setTimeout 的工做原理相信你们应该都知道,其中的延迟并非彻底精确的,这是由于 setTimeout 它会在延迟时间结束后分配一个新的 task 至 event loop 中,而不是当即执行,因此 setTimeout 的回调函数会等待前面的 task 都执行结束后再运行。这就是为何 'setTimeout' 会输出在 'script end' 以后,由于 'script end' 是第一个 task 的其中一部分,而 'setTimeout' 则是一个新的 task。这里咱们先解释了 event loop 的基本原理,接下来咱们会经过这个来说解 microtask 的工做原理。
Microtask 一般来讲就是须要在当前 task 执行结束后当即执行的任务,例如须要对一系列的任务作出回应,或者是须要异步的执行任务而又不须要分配一个新的 task,这样即可以减少一点性能的开销。microtask 任务队列是一个与 task 任务队列相互独立的队列,microtask 任务将会在每个 task 任务执行结束以后执行。每个 task 中产生的 microtask 都将会添加到 microtask 队列中,microtask 中产生的 microtask 将会添加至当前队列的尾部,而且 microtask 会按序的处理完队列中的全部任务。microtask 类型的任务目前包括了 MutationObserver 以及 Promise 的回调函数。
每当一个 Promise 被决议(或是被拒绝),便会将其回调函数添加至 microtask 任务队列中做为一个新的 microtask 。这也保证了 Promise 能够异步的执行。因此当咱们调用 .then(resolve, reject) 的时候,会当即生成一个新的 microtask 添加至队列中,这就是为何上面的 'promise1' 和 'promise2' 会输出在 'script end' 以后,由于 microtask 任务队列中的任务必须等待当前 task 执行结束后再执行,而 'promise1' 和 'promise2' 输出在 'setTimeout' 以前,这是由于 'setTimeout' 是一个新的 task,而 microtask 执行在当前 task 结束以后,下一个 task 开始以前。
下面这个 demo 将会逐步的分析 event loop 的运做方式:
经过以上的 demo 相信你们对 microtask 的运做方式有了了解了吧,不得不说我十分佩服 Jake Archibald ,人家本身一个字一个字的码了一个事件轮循器出来。做为一位膜拜者,我也一个字一个字的码了一个出来!...详情可参见引言中贴出的文章。
有一些浏览器会输出:'script start'、'script end'、'setTimeout'、'promise1'、'promise2'。这些浏览器将会在 'setTimeout' 以后输出 Promise 的回调函数,这看起来像是这类浏览器不支持 microtask 而将 Promise 的回调函数做为一个新的 task 来执行。
不过这一点也是能够理解的,由于 Promise 是来自于 ECMAScript 而不是 HTML。ES 当中有一个 “jobs” 的概念,它和 microtask 很类似,不过他们之间的关系目前尚未一个明确的定义。不过,广泛的共识都认为,Promise 的回调函数是应该做为一个 microtask 来运行的。
若是说把 Promise 当作一个新的 task 来执行的话,这将会形成一些性能上的问题,由于 Promise 的回调函数可能会被延迟执行,由于在每个 task 执行结束后浏览器可能会进行一些渲染工做。因为做为一个 task 将会和其余任务来源(task source)相互影响,这也会形成一些不肯定性,同时这也将打破一些与其余 API 的交互,这样一来便会形成一系列的问题。
Edge 浏览器目前已经修复了这个问题(an Edge ticket),WebKit 彷佛始终是标准的,Safari 终究也会修复这个问题,在 FireFox 43 中这个问题也已被修复。
直接测试输出是个很好的办法,看看输出的顺序是更像 Promise 仍是更像 setTimeout,趋向于 Promise 的则是 microtask,趋向于 setTimeout 的则是 task。
还有一种明确的方式是查看标准。例如,timer-initialisation-steps 标准的第 16 步指出 “Queue the task task”。(注意原文中指出的是 14 步,正确是应该是 16 步。)而 queue-a-mutation-record 标准的第 5 步指出 “Queue a mutation observer compound microtask”。
同时须要注意的是,在 ES 当中称 microtask 为 “jobs”。好比 ES6标准 8.4节当中的 “EnqueueJob” 意思指添加一个 microtask。
如今,让咱们来一个更复杂的例子...
在此以前,你须要了解 MutationObserver 的使用方法
1 <div class="outer"> 2 <div class="inner"></div> 3 </div>
1 var outer = document.querySelector('.outer'); 2 var inner = document.querySelector('.inner'); 3 4 // 给 outer 添加一个观察者 5 new MutationObserver(function() { 6 console.log('mutate'); 7 }).observe(outer, { 8 attributes: true 9 }); 10 11 // click 回调函数 12 function onClick() { 13 console.log('click'); 14 15 setTimeout(function() { 16 console.log('timeout'); 17 }, 0); 18 19 Promise.resolve().then(function() { 20 console.log('promise'); 21 }); 22 23 outer.setAttribute('data-random', Math.random()); 24 } 25 26 inner.addEventListener('click', onClick); 27 outer.addEventListener('click', onClick);
先试着猜猜看程序将会如何输出,你能够在这里查看输出结果:
猜对了吗?不过在这里不一样的浏览器可能会有不一样的结果。
Chrome | FireFox | Safari | Edge |
click | click | click | click |
promise | mutate | mutate | click |
mutate | click | click | mutate |
click | mutate | mutate | timeout |
promise | timeout | promise | promise |
mutate | promise | promise | timeout |
timeout | promise | timeout | promise |
timeout | timeout | timeout |
click 的回调函数是一个 task,而 Promise 和 MutationObserver 是一个 microtask,setTimeout 是一个 task,因此让咱们一步一步的来:
经过以上 demo 咱们能够看出,Chrome 给出的是正确答案,这里有一点与以前 demo 不一样之处在于,这里的 task 是一个回调函数而不是当前执行的脚本,因此咱们能够得出结论:用户操做的回调函数也是一个 task ,而且只要一个 task 执行结束且 JS stack 为空时,这时便检查 microtask ,若是不为空,则执行 microtask 队列。咱们能够参见 HTML 标准:
If the stack of script settings objects is now empty, perform a microtask checkpoint
— HTML: Cleaning up after a callback step 3
Execution of a Job can be initiated only when there is no running execution context and the execution context stack is empty…
注意在 ES 当中称 microtask 为 jobs。
经过上面的例子能够测试出,FireFox 和 Safari 可以正确的执行 microtask 队列,这一点能够经过 MutationObserver 的表现中看出,不过 Promise 被添加至事件队列中的方式好像有些不一样。 这一点也是可以理解的,因为 jobs 和 microtasks 的关系以及概念目前还比较模糊,不过人们都广泛的指望他们都可以在两个事件监听器之间执行。这里有 FireFox 和 Safari 的 BUG 记录。(目前 Safari 已经修复了这一 BUG)
在 Edge 中咱们能够明显的看出其压入 Promise 的方式是错误的,同时其执行 microtask 队列的方式也不正确,它没有在两个事件监听器之间执行,反而是在全部的事件监听器以后执行,因此才会只输出了一次 mutate 。Edge bug ticket (目前已修复)
到了这里,相信你们已经习得了 microtask 的运行机制了吧,不过咱们用以上的例子再作一点点小变化,好比咱们运行一个:
1 inner.click();
看看会发生什么?
一样,这里不一样的浏览器表现也是不同的:
Chrome | FireFox | Safari | Edge |
click | click | click | click |
click | click | click | click |
promise | mutate | mutate | mutate |
mutate | timeout | promise | timeout |
promise | promise | promise | promise |
timeout | promise | timeout | timeout |
timeout | timeout | timeout | promise |
奇怪的是,在 Chrome 的个别版本里可能会获得不一样的结果,究竟谁是正确答案?让咱们一步一步的分析:
从上面 demo 能够看出,正确的答案应该是:'click'、'click'、'promise'、'mutate'、'promise'、'timeout'、'timeout'。因此看来 Chrome 给出的是正确答案。
在前一个 demo 中,microtask 将会在两个 click 时间监听器之间运行,可是在这个 demo 中,因为咱们调用 .click() ,使得事件监听器的回调函数和当前运行的脚本同步执行而不是异步,因此当前脚本的执行栈会一直压在 JS 执行栈 当中。因此在这个 demo 中 microtask 不会在每个 click 事件以后执行,而是在两个 click 事件执行完成以后执行。因此在这里咱们能够再次的对 microtask 的检查点进行定义:当执行栈(JS Stack)为空时,执行一次 microtask 检查点。这也确保了不管是一个 task 仍是一个 microtask 在执行完毕以后都会生成一个 microtask 检查点,也保证了 microtask 队列可以一次性执行完毕。
关于 microtask 的讲解就到此结束了,同窗们有没有一种渐入佳境的感受呢?如今咱们来对 microtask 进行一下总结:
JS 的 Event Loop 一直以来都是一个比较重要的部分,虽然在学完了事后一会儿感受不出有什么具体的卵用...可是,一旦 Event Loop 的运行机制印入了你的脑海里以后,对你的编程能力和程序设计能力的提升是帮助很大的。关于 Event Loop 的知识不多有相关的书籍有写到,一是由于这一块比较晦涩难懂,短期内没法领略其精髓,二是由于具体能力提高不明显,不如认识几个 API 来的快,可是这倒是咱们编程的内力,他能在潜意识中左右着咱们编程时思考问题的方式。
本文的 demo 都放在了 jsfiddle 上面,可随意转载(仍是注明一下出处吧...)。