浏览器

JS分为同步任务和异步任务
同步任务都在主线程(这里的主线程就是JS引擎线程)上执行,会形成一个执行栈
主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放一个事件回调
一旦执行栈中的所有同步任务执行完毕(也就是JS引擎线程空闲了),系统就会读取任务队列,将可运行的异步任务(任务队列中的事件回调,只要任务队列中有事件回调,就说明可以执行)添加到执行栈中,开始执行
image.png

宏任务(macrotask)

在ECMAScript中,macrotask也被称为task
我们可以将每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 每一个宏任务会从头到尾执行完毕,不会执行其他
由于JS引擎线程GUI渲染线程是互斥的关系,浏览器为了能够使宏任务DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染

  1. 宏任务 -> GUI渲染 -> 宏任务 -> ..

常见的宏任务

  • 主代码块
  • setTimeout
  • setInterval
  • setImmediate ()-Node
  • requestAnimationFrame ()-浏览器

微任务(microtask)

ES6新引入了Promise标准,同时浏览器实现上多了一个microtask微任务概念,在ECMAScript中,microtask也被称为jobs
我们已经知道宏任务结束后,会执行渲染,然后执行下一个宏任务, 而微任务可以理解成在当前宏任务执行后立即执行的任务
当一个宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完

  1. 宏任务 -> 微任务 -> GUI渲染 -> 宏任务 -> ...

常见微任务

  • process.nextTick ()-Node
  • Promise.then()
  • async/await本质上还是基于Promise的一些封装,而Promise是属于微任务的一种

所以在使用await关键字与Promise.then效果类似

  • catch
  • finally
  • Object.observe
  • MutationObserver

image.png

简单区分宏任务与微任务

找一个空白的页面,在console中输入以下代码

  1. document.body.style = 'background:black';
  2. document.body.style = 'background:red';
  3. document.body.style = 'background:blue';
  4. document.body.style = 'background:pink';

根据上文里讲浏览器会先执行完一个宏任务,再执行当前执行栈的所有微任务,然后移交GUI渲染,上面四行代码均属于同一次宏任务,全部执行完才会执行渲染,渲染时GUI线程会将所有UI改动优化合并,所以视觉上,只会看到页面变成粉红色

再接着看

  1. document.body.style = 'background:blue';
  2. setTimeout(()=>{
  3. document.body.style = 'background:black'
  4. },200)

之所以会卡一下蓝色,是因为以上代码属于两次宏任务,第一次宏任务执行的代码是将背景变成蓝色,然后触发渲染,将页面变成蓝色,再触发第二次宏任务将背景变成黑色

再来看

  1. document.body.style = 'background:blue'
  2. console.log(1);
  3. Promise.resolve().then(()=>{
  4. console.log(2);
  5. document.body.style = 'background:black'
  6. });
  7. console.log(3);

页面的背景色直接变成粉色,没有经过蓝色的阶段,是因为,我们在宏任务中将背景设置为蓝色,但在进行渲染前执行了微任务, 在微任务中将背景变成了粉色,然后才执行的渲染

图解完整的Event Loop

浏览器/node事件循环 - 图3

node

https://www.cnblogs.com/nanianqiming/p/12199433.html
微任务
在谈论Node的事件循环机制之前,先补充说明一下 Node 中的“微任务”。这里说的微任务(microtasks)其实是一个统称,包含了两部分:

  • process.nextTick() 注册的回调 (nextTick task queue)
  • promise.then() 注册的回调 (promise task queue)

Node 在执行微任务时, 会优先执行 nextTick task queue 中的任务,执行完之后会接着执行 promise task queue 中的任务。所以如果 process.nextTick 的回调与 promise.then 的回调都处于主线程或事件循环中的同一阶段, process.nextTick 的回调要优先于 promise.then 的回调执行。


Timers(计时器阶段):从图可见,初次进入事件循环,会从计时器阶段开始。此阶段会判断是否存在过期的计时器回调(包含 setTimeout 和 setInterval),如果存在则会执行所有过期的计时器回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Pending callbacks 阶段。
Pending callbacks:执行推迟到下一个循环迭代的I / O回调(系统调用相关的回调)。
Idle/Prepare:仅供内部使用。(详略)
Poll(轮询阶段)
当回调队列不为空时:
会执行回调,若回调中触发了相应的微任务,这里的微任务执行时机和其他地方有所不同,不会等到所有回调执行完毕后才执行,而是针对每一个回调执行完毕后,就执行相应微任务。执行完所有的回到后,变为下面的情况。
当回调队列为空时(没有回调或所有回调执行完毕):
但如果存在有计时器(setTimeout、setInterval和setImmediate)没有执行,会结束轮询阶段,进入 Check 阶段。否则会阻塞并等待任何正在执行的I/O操作完成,并马上执行相应的回调,直到所有回调执行完毕。
Check(查询阶段):会检查是否存在 setImmediate 相关的回调,如果存在则执行所有回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Close callbacks 阶段。
Close callbacks:执行一些关闭回调,比如 socket.on('close', ...)等。

setImmediate、setTimeout/setInterval 和 process.nextTick 执行时机对比
setImmediate:触发一个异步回调,在事件循环的 Check 阶段立即执行。
setTimeout:触发一个异步回调,当计时器过期后,在事件循环的 Timers 阶段执行,只执行一次(可用 clearTimeout 取消)。
setInterval:触发一个异步回调,每次计时器过期后,都会在事件循环的 Timers 阶段执行一次回调(可用 clearInterval 取消)。
process.nextTick:触发一个微任务(异步)回调,既可以在主线程(mainline)中执行,可以存在事件循序的某一个阶段中执行。
image.png