原文:Tasks, microtasks, queues and schedules
任务、微任务,队列和调度
先看下面这道题?思考下 打印顺序?
console.log('script start');setTimeout(function () {console.log('setTimeout');}, 0);Promise.resolve().then(function () {console.log('promise1');}).then(function () {console.log('promise2');});console.log('script end');
正确的答案是:script start, promise1,promise2,script end,setTimeout。但是由于浏览器的支持,会有一些区别。
Microsoft Edge, Firefox 40, iOS Safari and desktop Safari 8.0.8打印setTimeout在promise1,promise2之前,尽管这看起来有一些混乱。但在FireFox 39和Safari 8.0.7总是正确的。
为何会如此?
要理解这些,你需要知道event loop是如何处理task和microtasks的。
每一个‘线程’, 都有它自己的 event loop,每一个web worker也有它自己的,他可以独立的执行,然而同一个origin下的所有窗口会分享同一个event loop这样他们可以同步通信。event loop会持续的运行,会一直执行队列里的任务。一个event loop 有多个任务源并且在资源内部保证执行顺序(例如IndexedDB定义自己的),但是浏览器会选择哪一个资源去执行一个任务在每一轮的循环中。这会导致浏览器会优先安排对性能要求高的任务例如用户输入。
Tasks是预先安排好的,所以浏览器能从他们的内部进入到JavaScript/DOM领域,并且确保这些动作按照顺序发生。在任务中间,浏览器会渲染更新。从用户的鼠标点击到到一个事件回调需要安排一个任务,紧接着解析HTML。
setTimeout会等待给定的延迟时间后为回调函数安排一个新的任务。这也是为什么setTimeout会在script end之后打印,这是因为script end是第一个任务的一部分,而setTimeout是在单独的任务里打印。
MicroTasks通常是被直接在当前执行脚本的后面,紧接着执行, 例如对批量动作的反应,或者异步执行一些动作而不需要花费一个完整的新任务。微任务队列执行是在回调之后,只要没有其他的JS脚本在执行,会在每一个任务的完成后。在微任务执行期间,一些额外的微任务队列也会被添加到微任务队列的尾部并被执行。微任务包含observer的变更回调,promise回调等。
一旦promise被设置,或者它已经被安排,他就会为他的回调函数安排一个微任务队列。这确保了promise的回调是异步的即使这个promise早就被安排。调用.then(yey, nay)在一个已完成的promise之后会立即添加一个微任务到队列中。这也是为什么promise1和 promsie2在script end之后打印,微任务处理必须在当前的执行脚本完成之后。promise1和promise2在setTimeout之前打印,微任务在下一个任务之前发生。
一些浏览器有什么不同?
一些浏览器会打印script start, script end, setTimeout, promise1, promise2。他们在执行setTimeout之后执行promise1和promise2。可能的原因是他们运行promsie的回调作为一个新的任务而不是微任务。
这是有一定道理的,promise是来自于ECMAScript而不是HTML。ECMAScript有‘jobs’的概念,和微任务类似,但是他们之间的联系并不是很清晰。然而,出于共识,promise应该是微任务队列的一部分。
把promsie作为宏任务会导致性能问题,这是由于一些和任务相关的回调函数没必要做延迟,例如渲染。这会引起不能决定论由于和其他任务源的交互,这会打断和其他API的交互。
