这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战
Event Loop
(1)基本概念
-
Event Loop即事件循环- 是一种用于解决JavaScript单线程运行时不会阻塞的执行机制
-
首先明确,JavaScript是单线程的,这意味着,所有任务都需要排队
-
前一个任务结束,才会执行后一个任务
-
这种由 主线程从 “任务队列” 中读取执行事件,不断循环重复的过程,就被称为 事件循环(Event Loop)
JS引擎存在一个进程,会持续不断的检测主线程执行栈是否为空
- 一旦为空,就会去任务队列中检查是否有等待的任务
-
-
如果前一个任务耗时很长,那么后一个任务就不得不一直等待
- 为了解决这个问题,所以JS的单线程任务有分为了同步任务和异步任务
(2)JS调用栈(执行栈)
Javascript有一个main thread主线程和call-stack调用栈- 所有的任务都会被放到调用栈等待主线程执行
- 调用栈也称为执行栈,采用的是后进先出的规则
- 当函数执行的时候,会被添加到栈的顶部(push)
- 当执行栈中执行完该函数,就会将其从栈顶移除(pop)
- 直到栈内被清空
(3)任务队列(Task Queue)
- 任务队列是一个先进先出(
类比排队)的数据结构- 排在前面的任务,优先被主线程读取
- 任务队列会在执行栈被清空后,依次进入主线程
- 注意任务队列上一些函数触发的条件
- 有些事件,如定时器,只有在事件到了之后才会被触发
(4)同步任务和异步任务
-
前面提到过,JS中的单线程任务分为了同步任务和异步任务
-
同步任务:
- 在主线程执行时,同步任务会被放入执行栈中,等待主线程依次执行
-
异步任务:
- 在执行栈之外,存在一个任务队列
- 在主线程执行时,异步任务会被放入这个任务队列中
- 只有等待执行栈中的所有同步任务执行完毕后
- 系统才会开始读取任务队列并将其放入执行栈中
- 然后等待主线程执行
(5)宏任务和微任务
-
而在JavaScript中,异步任务又被分为两种:宏任务(
MacroTask)、微任务(MicroTask) -
宏任务
script的全部代码、setTimeout、setInterval、I/O、UI渲染
-
微任务
-
Promise、MutationObserver
注意:
Promise本身是同步任务,但其回调函数.then()是异步任务的微任务 -
-
宏任务和微任务都是异步任务,同属于一个队列
-
其内部又划分为了宏任务队列和微任务队列
-
-
关于
async / awaitasync function foo() { // await 前面的代码 await bar(); // await的代码 // await 后面的代码 }-
其中
await 前面的代码是同步的,调用此函数时会直接执行 -
而
await bar();这句可以被转换成Promise.resolve(bar()) -
await 后面的代码则会被放到 Promise 的 then() 方法里
// 所以上面的代码可以转换成 function foo() { // await 前面的代码 Promise(resolve => { bar() // await的代码 resolve() }).then(() => { // await 后面的代码 }); } -
(6)执行过程
先看一个简单版的任务执行过程
a)简单版的
- JS开始执行,执行过程中会将遇到的代码分为两类
- 如果是同步任务,则放入执行栈中,等待主线程执行
- 如果是异步任务,则放入任务队列中
- 执行栈上的任务执行完毕后,即执行栈被清空了,系统就会去任务队列上读取一个任务
- 并将这个任务放入执行栈中,等待主线程执行
- 然后再清空执行栈,再从任务队列上读取一个任务,循环往复
现在我们来看一个具体一点的,图源网络,出处不明
b)具体一点的:
通过图,发现同步任务没有什么变化,而异步任务更具体了,所以我们主要看异步任务
- 异步任务会先进入
Event Table- 然后等待其触发条件,(比如定时器时间到了、事件点击触发了),触发成功后,就会注册它的回调函数为一个任务
Event Table会将这个任务移入Event Queue任务队列
- 主线程任务全部执行完毕后,就会去
Event Queue读取一个任务队列- 并将这个任务放入执行栈中,由主线程来执行
我们知道,异步任务又会分为宏任务和微任务,
那么宏任务和微任务又是如何执行的呢?
我们再上一张图,图源网络,出处不明
c)再具体一点的
-
根据遇到的任务不同,任务队列又可以分为:宏任务队列、微任务队列
-
执行顺序:
-
最开始的时候,script标签内的代码将作为宏任务。向调用栈中压入全局上下文开始执行
-
执行过程:
- 遇到宏任务,则加入下一次宏任务队列中;
- 遇到微任务,则加入当前宏任务的微任务队列中
- 注意,当前执行栈中是宏任务
- 每个宏任务会创建自己的微任务队列
-
JS同步代码执行完之后,宏任务即将结束之前,即执行栈被清空时
- 会将该宏任务对应的微任务队列的任务全部取出依次执行
- 若执行过程中又遇到微任务则加入当前微任务队列队尾
- 若执行过程中又遇到宏任务,则将其加入下一轮宏任务队列中
-
微任务队列清空后本轮
event loop结束。- 然后从任务队列取出一个宏任务,将宏任务中的代码取出,初始化全局上下文开始执行
- 有回到第2步的执行过程
-
(7)实例理解
// 位置1
console.log('script start(同)')
// 位置2 -- setTimeout1
setTimeout(function () {
console.log('setTimeout1(宏)')
})
// 位置3 -- setTimeout2
setTimeout(function () {
console.log('setTimeout2 —— 200s(宏)')
// setTimeout2 -- setTimeout
setTimeout(function () {
console.log('setTimeout2 —— 200s —> setTimeout(宏)')
})
// setTimeout2 -- promise
Promise.resolve().then(function () {
console.log('setTimeout2 —— 200s —> promise(微)')
})
}, 200)
// 位置4 -- promise1
new Promise(resolve => {
console.log('promise1(同)')
resolve()
}).then(function () {// 位置5
console.log('promise1 -> then(微)')
// promise1 -- setTimeout
setTimeout(function () {
console.log('promise1 -> then —> setTimeout(宏)')
})
})
// 位置6 -- promise2
Promise.resolve()
.then(function () { // 位置7
console.log('promise2 -> then(微)')
// promise2 -- then1 -- setTimeout
setTimeout(function () {
console.log('promise2 -> then —> setTimeout(宏)')
})
})
.then(function () {
console.log('promise2 -> then —> then(微)')
// promise2 -- then2 -- setTimeout
setTimeout(function () {
console.log('promise2 -> then —> then —> setTimeout(宏)')
})
})
// 位置8
console.log('script end(同)')
输出结果为:(分析过程略)
# 输出结果
script start(同)
promise1(同)
script end(同)
promise1 -> then(微)
promise2 -> then(微)
promise2 -> then —> then(微)
setTimeout1(宏)
promise1 -> then —> setTimeout(宏)
promise2 -> then —> setTimeout(宏)
promise2 -> then —> then —> setTimeout(宏)
setTimeout2 —— 200s(宏)
setTimeout2 —— 200s —> promise(微)
setTimeout2 —— 200s —> setTimeout(宏)
本人前端小菜鸡,如有不对请谅解