前言
今天刷到一道事件循环Event Loop
面试题,我发现实际运行的顺序与我所写出的答案有一些不一致。
控制台打印输出时11 先于 8
我不禁思考:script标签在js执行的优先级是怎样的 ? 查阅资料后,对JS代码运行机制有了新的认识,下面和大家分享一下。
面试题源码
<script>
console.log(1);
async function fnOne() {
console.log(2);
await fnTwo();
console.log(3);
}
async function fnTwo() {
console.log(4);
}
fnOne();
setTimeout(() => {
console.log(5);
}, 2000);
let p = new Promise((resolve, reject) => {
console.log(6);
resolve();
console.log(7);
})
setTimeout(() => {
console.log(8)
}, 0)
p.then(() => {
console.log(9);
})
console.log(10);
</script>
<script>
console.log(11);
setTimeout(() => {
console.log(12);
let p = new Promise((resolve) => {
resolve(13);
})
p.then(res => {
console.log(res);
})
console.log(15);
}, 0)
console.log(14);
</script>
知识梳理
首先我们要先清楚,Event Loop
(以下称为事件循环)是什么?
js代码所在运行环境(浏览器、nodejs)编译器的一种解析执行规则称之为事件循环。这里我们不涉及nodejs,
事件循环是浏览器内核解析执行js代码的一种执行规则
js代码分为同步代码
、异步代码
两大类;异步代码又分为微任务
、宏任务
。
常见异步任务有 :
任务 | 宏/微 任务 | 环境 |
---|---|---|
事件 | 宏任务 | 浏览器 |
网络请求(Ajax) | 宏任务 | 浏览器 |
fs.readFile() 读取文件 | 宏任务 | Node |
setTimeout() 定时器 | 宏任务 | 浏览器 / Node |
Promise.then() | 微任务 | 浏览器 / Node |
async/await | 微任务 | 浏览器 / Node |
简单阐述事件循环的执行机制
-
进入到
script
标签,默认进入到第一次事件循环。 -
遇到同步代码,立即执行。
-
遇到异步宏任务,放入到宏任务队列里。
-
遇到异步微任务,放入到微任务队列里。
-
当前事件队列
所有同步执行完毕
,开始执行异步队列代码 -
先执行异步微任务代码。
-
执行异步宏任务代码。
-
宏任务执行完毕,
本次事件循环结束
,执行下一次事件队列。 -
重复步骤2。 以此反复直到清空所以宏任务,这种不断重复的执行机制,就叫做
事件循环
面试题解析
-
先执行同步代码
-
然后执行微任务队列
- 执行性宏任务队列
最终执行的输出顺序为 : 1 2 4 6 7 10 3 9 11 14 8 12 15 13 5
关于script优先级测试
我一直在思考script的特殊性,如果只是简单的宏任务就不会插队
执行了,我进行了如下测试
<script>
console.log(1)
new Promise((resolve,reject)=>{
resolve(2)
}).then(res=>{
console.log(res)
})
console.log(3)
</script>
<script>
console.log(4)
</script>
输出顺序为1 3 2 4 由此得出结论:
script标签
优先级低于异步微任务
为了比较script标签
与定时器
的优先级我又进行如下测试
<script>
console.log(1)
setTimeout(() => {
console.log(2);
}, 0)
console.log(3)
</script>
<script>
console.log(4)
</script>
输出顺序为1 3 4 2 由此得出结论:
script标签
优先级高于定时器
应用
为了更加清晰的理解,我又分析了一道事件循环的练习题。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
console.log(1)
new Promise((resolve, reject) => {
resolve(2)
}).then(res => {
console.log(res)
})
setTimeout(() => {
console.log(3)
})
console.log(4)
</script>
<script>
console.log(5)
</script>
<script>
new Promise((resolve, reject) => {
resolve(6)
}).then(res => {
console.log(res)
})
setTimeout(() => {
console.log(7)
})
console.log(8)
</script>
<script>
console.log(9)
setTimeout(() => {
console.log(10)
})
</script>
</body>
</html>
做题思路
- 先将页面所有script标签当做宏任务。
- 根据js执行机制,开始解析第一个宏任务(第一个script标签)。
<!-- h1 -->
<script>
console.log(1)
new Promise((resolve, reject) => {
resolve(2)
}).then(res => {
console.log(res)
})
setTimeout(() => {
console.log(3)
})
console.log(4)
</script>
- h1宏任务里的定时器,是一个异步
宏任务
,而现在我们的宏任务列表里面已经有h1 h2 h3 h4 四个宏任务了;此处将定时器暂时命名为h5
宏任务,放入到宏任务队列里。
- h1宏任务执行完毕,进入下一个宏任务
h2
<!-- h2 -->
<script>
console.log(5)
</script>
- h2宏任务执行完毕,进入下一个宏任务
h3
<!-- h3 -->
<script>
new Promise((resolve, reject) => {
resolve(6)
}).then(res => {
console.log(res)
})
setTimeout(() => {
console.log(7)
})
console.log(8)
</script>
- h3宏任务里面的定时器是一个宏任务,命名为
h6
放入宏任务队列里。
- h3宏任务执行完毕,进入下一个宏任务
h4
<!-- h4 -->
<script>
console.log(9)
setTimeout(() => {
console.log(10)
})
</script>
- h4宏任务里面的定时器是一个宏任务,命名为
h7
放入宏任务队列里。
-
依次执行h5 h6 h7
-
输出结果
总结
- promise本质是一个同步的代码(容器),只有等同步走完了,才会走微任务then()。
- promise嵌套promise : 先走嵌套的then,后走外层then (
promise嵌套哪个then在上面就走哪个then
) - await下面的才是微任务, 右边的代码还是立即执行
- await微任务可以转换成等价的promise微任务分析
- script标签本身是一个
宏任务
- 同步特点: 当页面出现多个script标签的时候, 浏览器会把script标签作为宏任务来解析,
解析不代表执行
。 - 宏任务特点: 等微任务执行完毕之后,才会执行宏任务。
- 可以把script标签当做一个优先级最高的定时器。 比微任务后执行,比定时器先执行
- 同步特点: 当页面出现多个script标签的时候, 浏览器会把script标签作为宏任务来解析,