我刚刚读完Promises/A+规范,无意中发现了术语微任务和宏任务:请参阅http://promisesaplus.com/#notes

我以前从未听说过这些术语,现在我很好奇会有什么不同?

我已经试着在网上找到了一些信息,但我只找到了来自w3.org档案的这篇文章(这并没有向我解释区别):http://lists.w3.org/Archives/Public/public-nextweb/2013Jul/0018.html

此外,我还找到了一个名为“macrotask”的npm模块:https://www.npmjs.org/package/macrotask 同样,没有明确的区别到底是什么。

我所知道的是,它与事件循环有关,如https://html.spec.whatwg.org/multipage/webappapis.html#task-queue中所述 和https://html.spec.whatwg.org/multipage/webappapis.html perform-a-microtask-checkpoint

我知道,根据WHATWG规范,理论上我应该能够自己提取差异。但我相信其他人也能从专家给出的简短解释中受益。


当前回答

我根据4个概念创建了一个事件循环伪代码:

setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering are part of the macrotask queue. One macrotask item will be processed first. process.nextTick, Promises, Object.observe, MutationObserver are part of the microtasks queue. The event loop will process all of the items in that queue including once that were processed during the current iteration. There is another queue called animation queue which holds animation changes task items which will be processed next. All tasks which exists in this queue will be processed (not including new one which were added during the current iteration). It will be called if it is time for rendering The rendering pipeline will try to render 60 times a second (every 16 ms) while (true){ // 1. Get one macrotask (oldest) task item task = macroTaskQueue.pop(); execute(task); // 2. Go and execute microtasks while they have items in their queue (including those which were added during this iteration) while (microtaskQueue.hasTasks()){ const microTask = microtaskQueue.pop(); execute(microTask); } // 3. If 16ms have elapsed since last time this condition was true if (isPaintTime()){ // 4. Go and execute animationTasks while they have items in their queue (not including those which were added during this iteration) const animationTasks = animationQueue.getTasks(); for (task in animationTasks){ execute(task); } repaint(); // render the page changes (via the render pipeline) } }

其他回答

宏任务包括键盘事件、鼠标事件、定时器事件(setTimeout)、网络事件、Html解析、修改Urletc。宏任务表示一些离散且独立的工作。微任务队列具有较高的优先级,宏任务将等待所有的微任务先执行。

微任务是更新应用程序状态的较小任务,应该在浏览器继续执行其他分配之前执行 重新渲染UI。微任务包括承诺回调和DOM突变更改。微任务使我们能够在UI重新呈现之前执行某些操作,从而避免不必要的UI呈现,从而显示不一致的应用程序状态。

宏任务和微任务的分离使 事件循环优先级的任务类型;例如,优先处理对性能敏感的任务。

在单个循环迭代中,最多处理一个宏任务 (其他任务留在队列中等待),而所有的微任务都被处理。

Both task queues are placed outside the event loop, to indicate that the act of adding tasks to their matching queues happens outside the event loop. Otherwise, any events that occur while JavaScript code is being executed would be ignored. The acts of detecting and adding tasks are done separately from the event loop. Both types of tasks are executed one at a time. When a task starts executing, it’s executed to its completion. Only the browser can stop the execution of a task; for example, if the task takes up too much time or memory. All microtasks should be executed before the next rendering because their goal is to update the application state before rendering occurs. The browser usually tries to render the page 60 times per second, It's accepted that 60 frames per second are the rate at which animations will appear smooth. if we want to achieve smooth-running applications, a single task, and all microtasks generated by that task should ideally complete within 16 ms. If a task gets executed for more than a couple of seconds, the browser shows an “Unresponsive script” message.

参考John resign - JS Ninja的秘密

规范的基本概念:

一个事件循环有一个或多个任务队列。(任务队列为宏任务队列) 每个事件循环都有一个微任务队列。 任务队列=宏任务队列!=微任务队列 一个任务可以被推入宏任务队列,也可以被推入微任务队列 当一个任务被推入队列(微观/宏观)时,我们意味着准备工作已经完成,所以任务现在可以执行了。

事件循环过程模型如下:

当调用堆栈为空时,执行步骤-

select the oldest task(task A) in task queues if task A is null(means task queues is empty),jump to step 6 set "currently running task" to "task A" run "task A"(means run the callback function) set "currently running task" to null,remove "task A" perform microtask queue (a).select the oldest task(task x) in microtask queue (b).if task x is null(means microtask queues is empty),jump to step (g) (c).set "currently running task" to "task x" (d).run "task x" (e).set "currently running task" to null,remove "task x" (f).select next oldest task in microtask queue,jump to step(b) (g).finish microtask queue; jump to step 1.

简化后的过程模型如下:

运行宏任务队列中最老的任务,然后删除它。 运行微任务队列中所有可用的任务,然后删除它们。 下一轮:在宏任务队列中运行下一个任务(跳过步骤2)

需要记住的事情:

when a task (in macrotask queue) is running,new events may be registered.So new tasks may be created.Below are two new created tasks: promiseA.then()'s callback is a task promiseA is resolved/rejected:  the task will be pushed into microtask queue in current round of event loop. promiseA is pending:  the task will be pushed into microtask queue in the future round of event loop(may be next round) setTimeout(callback,n)'s callback is a task,and will be pushed into macrotask queue,even n is 0; task in microtask queue will be run in the current round,while task in macrotask queue has to wait for next round of event loop. we all know callback of "click","scroll","ajax","setTimeout"... are tasks,however we should also remember js codes as a whole in script tag is a task(a macrotask) too.

我根据4个概念创建了一个事件循环伪代码:

setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering are part of the macrotask queue. One macrotask item will be processed first. process.nextTick, Promises, Object.observe, MutationObserver are part of the microtasks queue. The event loop will process all of the items in that queue including once that were processed during the current iteration. There is another queue called animation queue which holds animation changes task items which will be processed next. All tasks which exists in this queue will be processed (not including new one which were added during the current iteration). It will be called if it is time for rendering The rendering pipeline will try to render 60 times a second (every 16 ms) while (true){ // 1. Get one macrotask (oldest) task item task = macroTaskQueue.pop(); execute(task); // 2. Go and execute microtasks while they have items in their queue (including those which were added during this iteration) while (microtaskQueue.hasTasks()){ const microTask = microtaskQueue.pop(); execute(microTask); } // 3. If 16ms have elapsed since last time this condition was true if (isPaintTime()){ // 4. Go and execute animationTasks while they have items in their queue (not including those which were added during this iteration) const animationTasks = animationQueue.getTasks(); for (task in animationTasks){ execute(task); } repaint(); // render the page changes (via the render pipeline) } }

我认为我们不能从堆栈中分离出来讨论事件循环,所以:

JS有三个“堆栈”:

所有同步调用的标准堆栈(一个函数调用另一个函数,等等) 微任务队列(或作业队列或微任务堆栈)用于所有具有更高优先级的异步操作(进程。nextTick,承诺,对象。观察,MutationObserver) 宏任务队列(或事件队列,任务队列,宏任务队列)用于所有低优先级的异步操作(setTimeout, setInterval, setimmediation, requestAnimationFrame, I/O, UI渲染)

|=======|
| macro |
| [...] |
|       |
|=======|
| micro |
| [...] |
|       |
|=======|
| stack |
| [...] |
|       |
|=======|

事件循环是这样工作的:

执行栈中从下到上的所有操作,并且只有当栈为空时,才检查上面的队列中发生了什么 检查微堆栈并在堆栈的帮助下执行那里的所有内容(如果需要),一个微任务接一个微任务,直到微任务队列为空或不需要任何执行,然后才检查宏堆栈 检查宏堆栈并在堆栈的帮助下执行其中的所有内容(如果需要)

如果栈不为空,微栈将不会被触动。如果微堆栈不为空或不需要任何执行,宏堆栈将不会被触及。

综上所述:微任务队列与宏任务队列几乎相同,但这些任务(进程)和宏任务队列是相同的。nextTick,承诺,对象。observe, MutationObserver)的优先级高于宏任务。

微观就像宏观,但优先级更高。

这里有理解一切的“终极”代码。


console.log('stack [1]');
setTimeout(() => console.log("macro [2]"), 0);
setTimeout(() => console.log("macro [3]"), 1);

const p = Promise.resolve();
for(let i = 0; i < 3; i++) p.then(() => {
    setTimeout(() => {
        console.log('stack [4]')
        setTimeout(() => console.log("macro [5]"), 0);
        p.then(() => console.log('micro [6]'));
    }, 0);
    console.log("stack [7]");
});

console.log("macro [8]");
/* Result: stack [1] macro [8] stack [7], stack [7], stack [7] macro [2] macro [3] stack [4] micro [6] stack [4] micro [6] stack [4] micro [6] macro [5], macro [5], macro [5] -------------------- but in node in versions < 11 (older versions) you will get something different stack [1] macro [8] stack [7], stack [7], stack [7] macro [2] macro [3] stack [4], stack [4], stack [4] micro [6], micro [6], micro [6] macro [5], macro [5], macro [5] more info: https://blog.insiderattack.net/new-changes-to-timers-and-microtasks-from-node-v11-0-0-and-above-68d112743eb3 */

事件循环的一次循环将只处理一个来自宏任务队列的任务(该队列在WHATWG规范中简单地称为任务队列)。 在这个宏任务完成后,所有可用的微任务都将被处理,即在同一个循环周期内。当这些微任务被处理时,它们可以将更多的微任务排队,这些任务将一个接一个地运行,直到微任务队列耗尽。

这样做的实际后果是什么?

如果一个微任务递归地排队其他微任务,它可能需要很长时间才能处理下一个宏任务。这意味着,您最终可能会遇到阻塞的UI,或者应用程序中一些已完成的I/O处于空闲状态。

但是,至少对于Node.js的进程。nextTick函数(排队微任务),有一个内置的保护,通过process.maxTickDepth防止这种阻塞。这个值被设置为默认的1000,在达到这个限制后减少对微任务的进一步处理,允许处理下一个宏任务)

那么什么时候用什么呢?

基本上,当你需要以同步的方式异步地做一些事情时,使用微任务(即当你说在最近的将来执行这个(微)任务时)。 否则,就坚持做宏观任务。

例子

macrotasks: setTimeout, setInterval, setimmediation, requestAnimationFrame, I/O, UI渲染 microtasks:过程。nextTick承诺queueMicrotask MutationObserver