一文搞懂JS系列(六)之微任务与宏任务,Event Loop

5,164 阅读7分钟

大家好,我是辉夜真是太可爱啦。这是我写的一个一文搞懂JS系列专题。文章清晰易懂,会将会将关联的只是串联在一起,形成自己独立的知识脉络整个合集读完相信你也一定会有所收获。写作不易,希望您能给我点个赞

合集地址:一文搞懂JS系列专题

概览

  • 食用时间: 10-15分钟
  • 难度: 简单,别跑,看完再走
  • 食用价值: 了解JS为什么是单线程,搞懂为什么会有同步异步的任务划分,搞懂微任务与宏任务都有哪些,Event Loop中微任务和宏任务哪一个先执行,以及最后小试牛刀,文末一题

单线程的JS

大家应该都知道 JS 有一个特性,在刚开始学习的时候应该就知道了,那就是 JS单线程的。

那么,为什么 JS 是单线程的呢,明明多线程能提升效率啊。

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

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

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

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

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

所以,是单线程的出现,才引发了同步和异步的出现,接下来,让我们来引入一个生活中的例子,方便大家更好地理解同步和异步

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

现实生活中的同步与异步

就比方说我们平时吃KFC,我们都要去收银台排队,(别跟我说扫码点餐!),假设我们点了一份炸鸡 + 付款一分钟,取餐需要五分钟,这个时候店员说,按照我们店里的规定,我们只能一个接一个的服务客户,后面的客户必须等当前这个客户取完餐,才能换下一个客户点餐,而这种情况,那就是所谓的同步,就是按顺序执行,一件事情做完了,才能做下一件事情。

但是结果是很明显的,这种接客方式也未免效率太低了,那KFC估计也支撑不到今天就已经倒闭了。

为了提升自己的服务效率,后来,KFC推出了 点餐区 以及 取餐区 。在你付款完成以后,给你一张取餐小票,就可以从收银台的队列中出去啦,让下一个客户赶紧点餐,而你只需要等前台通知你,你要的套餐做好啦,快来取餐区取餐啦。

JS中的同步与异步

  • 同步

    任务从上往下按顺序执行,后一个任务必须等待前一个任务执行完(后一个点餐的人必须要等前一个人取完餐)

  • 异步

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

经过同步任务和异步任务的划分,程序的运行效率明显提高了(KFC的接待效率)

微任务与宏任务

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

那么,异步任务也是有它的执行顺序的,它也是从上往下,但是,异步任务里,对于异步类型还有进一步的划分,那就是接下来我们要讲的微任务宏任务,切记微任务比宏任务先执行

  • 微任务(micro-task

    process.nextTick、Promise、MutationObserver等

  • 宏任务(macro-task

    setTimeout、setInterval、 setImmediate、script(整体代码)、I/O 操作等

值得注意的是, Promise 是有一点特殊性的,因为Promise构造函数中函数体的代码都是立即执行的 , 而 Promise.then()Promise.catch() 属于微任务,也就是 resolve()reject()

对于上面这句话的理解,可以来看下下面的例子

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

上面这段实例代码的 1 ,是直接输出的,属于同步任务,虽然它确实在 Promise

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

调用栈

这是最后要介绍的一个角色,也就是真正执行代码,执行任务的地方

Event Loop

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

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

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

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

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

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

虽然整体代码确实是一开始作为宏任务执行的,但是,希望大家还是要切记,在异步任务中,微任务队列比宏任务队列先执行(方便记忆)

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

文末一题

console.log(1)

setTimeout(function() {
  console.log(2)
})

new Promise(function (resolve) {
  console.log(3)
  resolve()
}).then(function () {
  console.log(4)
}).then(function() {
  console.log(5)
})

console.log(6)

通过上面的学习,这道题就显得十分简单了,答案就是 1 3 6 4 5 2

不明白的话,可以看看我下面的这一段分析,我们从上往下,将代码抽离成三部分,同步任务,微任务队列以及宏任务队列

  • 同步任务

console.log(1)
console.log(3)
console.log(6)
  • 微任务队列

console.log(4)    //Promise.then()
console.log(5)    //Promise.then()
  • 宏任务队列

console.log(2)    //setTimeout

所以,答案一眼就能看的出来是 1 3 6 4 5 2

目录