前言
我们都知道 JavaScript 是一门 单线程 的语言:同一时间只能运行一个任务。通常情况下这没什么问题,但是如果你有一个任务需要耗费 30 秒的时间,那其他任务难道都要等它 30 秒么?(由于 JS 运行在浏览器的主线程,所以这 30 秒的时间里,整个页面都会处于卡死状态)
幸运的是,浏览器提供了一些 JS 引擎不具备的功能:Web API。它包括 DOM API,setTimeout,HTTP 请求 等等。这些功能都可以帮助我们处理 异步、非阻塞 的操作。
什么是Event loop?
Event Loop 即事件循环,是指浏览器或Node的一种解决Javascript单线程运行时不会阻塞的一种机制,也就是异步的原理。
为啥要弄懂Event loop?
- 增加技术深度,也就是懂得Javascript的运行机制。
- 前端领域技术层出不穷,掌握底层原理可以以不变应万变。
- 应对大场面试,懂其原理,题目任意发挥。
数据结构前置知识
堆:利用完全二叉树维护的一组数据,是线性数据结构,相当于一维数组,有唯一后继。
栈:后进先出。类似桶装薯片,包装的时候只能从顶部放入,而吃的时候也只能从顶部拿出。
队列:先进先出。类似排队办理业务,排在最前面的最先办理。
调用栈:本质上当然还是个,它里面装的东西是一个个待执行的函数,记住,是函数!先拿两个函数来说:
- 栈空
- 现在执行到一个 函数A,函数A 入栈
- 函数A 又调用了 函数B,函数B 入栈
- 函数B 执行完后 出栈
- 然后继续执行 函数A,执行完后A也 出栈
- 栈空
Event loop
同步任务、异步任务
Js单线程分为同步任务和异步任务。
同步任务会在调用栈中按照顺序等待主线程依次执行。
异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中,等调用栈为空时,会被读取到调用栈中等待主线程执行。
回调函数
很多人看到“回调函数”就准备发懵了,但其实很简单。打个比方,有一家旅馆提供叫醒服务,但是要求旅客自己决定叫醒的方法。可以是打客房电话,也可以是派服务员去敲门,睡得死怕耽误事的,还可以要求往自己头上浇盆水。这里,“叫醒的方式”是由旅客决定并告诉旅馆的,旅馆会在第二天早上执行。这个“叫醒的方式”就是回调函数。而旅客告诉旅馆怎么叫醒自己的动作,也就是把回调函数传入库函数的动作,称为注册回调函数(to register a callback function)。
宏任务、微任务
在Js中,任务被分为两种,宏任务(Macro Task,Task)和微任务(Micro Task)。
宏任务包括:
- script全部代码
- setTimeout
- setInterval
- setImmediate
- I/O
- UI Rendering
微任务包括:
- Process.nextTick(Node 独有)
- Promise.then之后的任务
- Mutation Observer
可以看到,宏任务中既有同步任务,也有异步任务。
Event Table
Event Table 可以理解成一张 事件->回调函数 对应表
它就是用来存储 JavaScript 中的异步事件 (request, setTimeout, IO等) 及其对应的回调函数的列表
Event Queue
Event Queue 简单理解就是 回调函数 队列,所以它也叫 Callback Queue
当 Event Table 中的事件被触发,事件对应的 回调函数 就会被 push 进这个 Event Queue,然后等待被执行
Event Loop
一个流程图秒懂:
Event Loop规则,记住规则,万事easy~
-
js执行时,用到了两个数据结构,执行栈(call Stack)和任务队列(Event Queue)(都是软件范畴哦,其实都是数组,只不过按照特性抽象成了栈和队列)。
-
执行之前,首先会按照代码顺序将同步任务会被依次加入执行栈中依次执行,将异步任务分为宏任务和微任务分别放入宏任务队列和微任务队列。
-
在执行栈执行完同步任务(算作第一个宏任务)后,查看执行栈是否为空,如果执行栈为空,检查微任务队列是否为空,如果不为空,会按照先进先出的规则全部执行完微任务后,就会去执行第二个宏任务,每一个宏任务执行完毕后,检查微任务队列是否为空,如果不为空,会按照先进先出的规则全部执行完微任务后,再执行下一个宏任务,如此循环。
-
Promise在.then之前的代码是同步任务,直接执行,.then之后的才是异步任务,也是微任务,需要被放入微任务队列。
例子-经典面试题,举一反三
setTimeout(function () {
console.log(1)
}, 0);
new Promise(function (resolve, reject) {
console.log(2);
resolve();
}).then(function () {
console.log(3)
}).then(function () {
console.log(4)
});
console.log(6);
复制代码
- 执行主线程同步任务:Promise.then前面的console.log(2);和 console.log(6),输出 2 6
- setTimeout丢入宏任务队列,Promise.then丢入微任务队列。
- 因为第1步执行了第一个宏任务,现在开始执行微任务,输出 3 4
- 执行下一个宏任务:输出 1
最终答案;2 6 3 4 1
setTimeout(function () {
console.log(1)
}, 0);
new Promise(function (resolve, reject) {
console.log(2)
for (var i = 0; i < 10000; i++) {
if (i === 10) {
console.log(10)
}
i == 9999 && resolve();
}
console.log(3)
}).then(function () {
console.log(4)
})
console.log(5);
// 2 10 3 5 4 1
复制代码