Event Loop

176 阅读3分钟

首先,JavaScript 是一门单线程语言,要么执行脚本要么进行浏览器渲染。

宏任务和微任务

在JavaScript中,任务被分为两种,一种宏任务(MacroTask),一种叫微任务(MicroTask)。

宏任务

# 浏览器 Node
I/O
setTimeout
setImmediate
requestAnimationFrame

以 setTimeout 为例。由于 JavaScript 是单线程,所以 setTimeout 的计时操作一定不是JavaScript来做的,否则会造成代码执行的阻塞。

那么这种操作是由谁来做的?是宿主环境。以浏览器为例子,JavaScript 在执行到 setTimeout 时会告诉浏览器:“Hey boy!这有个定时器,你帮我看着点,等到点了你告诉我一下”。这时候浏览器就会进行一个计时操作,计时完成以后,将 setTimeout 的回调放入 JavaScript 事件循环的回调队列中。这样 JavaScript 就可以在接下来的执行中处理这个回调。

宏任务便是 JavaScript 与宿主环境产生的回调,需要宿主环境配合处理并且会被放入回调队列的任务都是宏任务。

微任务

# 浏览器 Node
process.nextTick
MutationObserver
Promise.then catch finally

例如 Promise 也都能够产生异步操作,那为什么与宏任务不一样呢。这里就要涉及到事件循环的另一个队列了--作业队列(微任务队列)。

任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。 为了更高效的利用 cpu 资源,所以 在 JavaScript 中所有的任务可以分为两种:同步任务,异步任务。

JS调用栈

JS调用栈采用的是后进先出的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。

同步任务和异步任务

Javascript 单线程任务被分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),再读取到栈内等待主线程的执行。

具体来说,异步执行的运行机制如下。

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

浏览器中的表现

执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去检查微任务(microTask)队列是否为空,如果为空的话,就执行Task(宏任务),否则就一次性执行完所有微任务。

每次单个宏任务执行完毕后,检查微任务(microTask)队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务(microTask)后,设置微任务(microTask)队列为null,然后再执行宏任务,如此循环。

总结下,现在在js运行中,起码有三个东西:

  • task队列(宏任务队列)

  • Microtask队列 (微任务队列)

举个例子

setTimeout(() => {
  console.log('timer1');
  Promise.resolve().then(() => {
    console.log('promise1')
  })
});

setTimeout(() => {
  console.log('timer2');
  Promise.resolve().then(() => {
    console.log('promise2')
  })
});

执行过程

参考资料:

  1. JavaScript的宏任务与微任务      作者: T1ng4

  2. 微任务、宏任务与Event-Loop      作者: Jiasm

  3. 一次弄懂Event Loop       作者:光光同学

  4. JavaScript 运行机制详解:再谈Event Loop       作者:阮一峰