Skip to content

JS 中微任务与宏任务与事件循环(Event Loop)

单线程的 JS

大家应该都知道 JS 有一个特性,就是 单线程

JS 的主要用途是与 用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 JS 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

如果排队是因为计算量大,CPU 忙不过来,倒也算了,但是很多时候 CPU 是闲着的,因为 IO 设备(输入输出设备)很慢(比如 Ajax 操作从网络读取数据),不得不等着结果出来,再往下执行。

JS 语言的设计者意识到,这时主线程完全可以不管 IO 设备,挂起处于等待中的任务,先运行排在后面的任务。等到 IO 设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是,所有的任务可以分成两种,一种是 同步任务(synchronous),另一种是 异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入 "任务队列"(task queue) 的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

所以,是 单线程的出现,才引发了同步和异步的出现,我们可以看下面的例子,方便大家 更好理解同步和异步

参考于 阮一峰 JavaScript 运行机制详解:再谈 Event Loop

现实生活中的同步和异步

比方说我们吃 KFC,我们都要去收银台排队(假设不能进行扫码点餐),假设我们进行点餐+付款+取餐需要十分钟,这个时候店员说,按照店内的规定,我们只能一个接一个的服务客户,后面的客户需要等待前面客户点餐完成,付款完成,取餐完成,才能进行点餐,付款,取餐。这种情况就是所谓的同步,就是按顺序执行,一件事情做完了,才能做下一件事情。

这种结果导致最后的问题就是效率太过于低下,因为店员需要一个一个的进行服务,如果顾客很多,那么店员就需要等待很久才能服务下一个顾客。

为了提升服务效率,KFC 推出了 点餐区 以及 取餐区。在你付款完毕以后,给你一张取餐码,那么你就可以从点餐区的队列中离开,下一个客户就可以接着点餐,而你只需要等前台通知,你要的套餐做好了,快来取餐区取餐。

JS 中的同步与异步

同步

任务从上往下按顺序执行,只有前一个任务执行完毕,才能执行后一个任务(后一个点餐的客户必须等前一个客户取餐完毕)。

异步

前一个任务还没执行完毕(未取餐),也没任何关系,直接执行下一个任务(下一个客户点餐),等到前台通知取餐,在执行(取餐)。

经过同步任务和异步任务的划分,程序的运行效率明显提高

JS 中的宏任务与微任务

上面已经对同步任务和异步任务进行了划分,我们都知道,同步任务就是按顺序从上往下依次执行。

异步任务也是有它的执行顺序的,它也是 从上往下,但是,异步任务里,对于异步类型还有进一步的划分,分为 宏任务微任务微任务比宏任务先执行

微任务(micro-task)

process.nextTick、Promise、MutationObserver 等

宏任务(macro-task)

setTimeout、setInterval、setImmediate、I/O、UI rendering 等

值得注意的是,Promise 是有一点特殊的,它既可以是同步的,也可以是异步的,具体是同步还是异步,取决于 Promise 的构造函数中是否传入了 回调函数,如果传入了回调函数,那么 Promise 就是同步的,否则就是异步的。

js
new Promise(function (resolve) {
  console.log(1);
});

上面这段代码,Promise 的回调函数是同步的,所以 Promise 是同步的,所以 1 会先打印出来。

学会如何区分微任务与宏任务之后,我们也就对异步任务的执行顺序划分有了进一步的了解。

事件循环(Event Loop)

  1. 初始状态下,调用栈空。微任务队列空,宏任务队列里只有一个 script(整体代码)。这时首先执行并出队的就是 script(整体代码)。

  2. 整体代码作为宏任务进入调用栈,进行同步任务和异步任务的区分。

  3. 同步任务 直接执行 并且在执行完之后 出栈,异步任务进行微任务和宏任务的划分,分别被推入 微任务队列宏任务队列

  4. 等同步任务执行完(调用栈为空)以后,在处理微任务队列,将微任务队列压入调用栈

  5. 当调用栈中的微任务队列被处理完毕(调用栈为空)之后,再将宏任务队列压入调用栈,直至调用栈再一次为空,一次轮回结束。

整体的运行流程可以查看下图,红色箭头为主要的执行流程整体代码(宏任务) => 同步任务 => 异步任务 => 微任务队列 => 宏任务队列

eventLoop

关于这个 Event Loop,其实涉及了很多的知识点,包括 微任务宏任务调用栈执行上下文同步与异步任务队列

基于 MIT 许可发布