🍎 JavaScript 事件循环相关
相关问题:
- 对事件循环的理解。
# 概念
JavaScript 是一门单线程的语言。意味着同一时间内只做一件事情,但不意味着单线程就是阻塞,而实现单线程不阻塞的方法就是事件循环。
JavaScript 中的所有的任务都可以分为 同步任务 和 异步任务:
- 同步任务:立即执行的任务,同步任务一般直接进入主线程中;
- 异步任务:异步执行的任务。例如 AJAX 网络请求;
setTimeout
定时函数。
如下图,同步任务与异步任务的运行流程图:
同步任务进入主线程,即主执行栈;异步任务进入任务队列,主线程内的任务执行完毕为空,就会去任务队列中读取对应的任务,推入到主线程执行,上述过程不断重复形成 事件循环。
#
🌰 例子:
::: demo[vanilla]
<html>
<button id="btn">
启动定时器
</button>
</html>
<script>
document.getElementById("btn").onclick = function () {
var start = Date.now();
alert("启动定时器前");
setTimeout(function () {
console.log("定时器执行了", Date.now() - start);
});
alert("启动定时结果");
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
:::
定时器真的是定时执行吗:定时器并不能保证真正定时执行,一般会存在可以接受的延迟,但也有可能受代码量的影响带来不能接受的很长的延迟时间。
定时器回调函数是在分线程执行的吗:在主线程执行的,JavaScript 是单线程的。
定时器是如何实现的:事件循环模型。
如何证明 JavaScript 是单线程的:
setTimeout()
的回调函数是在主线程执行的。- 定时器回调数只有运行栈中的代码全部运行完才有可能执行。
为什么 JavaScript 要用单线程模式,而不用多线程模式:
- JavaScript 的单线程 ** 与它的用途有关:** 作为浏览器的脚本语言,JavaScript 的主要用途是与用户互动以及操作 DOM,这决定了它只能是单线程,否则会带来很复杂的同步问题。
# 微任务与宏任务
🌰 例子:
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)
2
3
4
5
6
7
8
9
10
11
12
13
14
按照上面的 事件循环机制:
console.log(1)
,同步任务,主线程中执行;setTimeout()
,异步任务,放到Event Table
,0 毫秒后console.log(2)
回调推入Event Queue
中;new Promise
,同步任务,主线程直接执行;.then
,异步任务,放到Event Table
;console.log(3)
,同步任务,主线程执行;所以按照这个分析,输出的结果应该是:
1
=>new Promise
=>3
=>2
=>then
。但是实际的结果应该是:
1
=>new Promise
=>3
=>then
=>2
。因为异步任务的执行也有顺序。事件队列其实是一个 先进先出的数据结构,排在前面的事件优先被主线程读取。但是这里
.then
的执行却早于setTimeout
的回调事件。
原因在于异步任务还可以细分为 微任务与宏任务。
# 微任务
一个需要异步执行的函数,执行时机是在主函数执行结束之后,当前宏任务结束之前。
常见的微任务有:
Promise.then
MutationObserver
(opens new window)Object.observe()
(opens new window) (已废弃,被 Proxy 对象替代)process.nextTick
(NodeJS)
# 宏任务
宏任务的时间粒度较大,执行的时间间隔不能精确控制,对于一些高实时性的需求不太符合。
常见的宏任务有:
script
setTimeout
- UI 渲染事件 / UI rendering
postMessage
/MessageChannel
setImmediate
、I/O
(NodeJS)
# 微任务与宏任务的关系
事件循环中:
执行一个宏任务,如果遇到微任务就将它放进微任务的事务队列中。
当前宏任务执行完成之后,会查看微任务的时间队列,然后将里面的所有微任务依次执行完。
回到上面的例子:
- 遇到
console.log(1)
,直接打印1
; - 遇到定时器,属于新的宏任务,留着后面执行;
- 遇到
new Promise
,这个是直接执行的,打印'new Promise'
; - 遇到
.then
属于微任务,放入微任务队列,后面再执行; - 遇到
console.log(3)
直接打印3
; - 本轮宏任务执行完毕,然后去微任务列表中查看是否有微任务,发现
.then
的回调,执行它,打印‘.then’
。 - 当一次宏任务执行完,再去执行新的宏任务,这里就剩下一个宏任务,执行它,打印
2
。
# async
与 await
async
是异步的意思, await
则可以理解为 async wait
。所以可以理解 async
就是用来声明一个异步方法,而 await
是用来等待异步方法执行。
# async
async
函数返回一个 promise
对象。
🌰 例子:
function f() {
return Promise.resolve('TEST');
}
// asyncF is equivalent to f!
async function asyncF() {
return 'TEST';
}
2
3
4
5
6
7
8
两种方法等效。
# await
正常情况下, await
命令后面是一个 Promise
对象,返回该对象的结果。如果不是 Promise
对象,就直接返回对应的值。
🌰 例子:
async function f() {
return await 123
// return 123
}
f().then(v => console.log(v)) // 123
2
3
4
5
6
不管 await
后面跟着什么, await
都会阻塞后面的代码:
async function fn1() {
console.log(1)
await fn2()
console.log(2)
}
async function fn2 (){
console.log('fn2')
}
fn1()
console.log(3)
2
3
4
5
6
7
8
9
10
11
12
await
会阻塞下面的代码(即加入微任务队列),先执行async
外面的同步代码,同步代码执行完,再回到async
函数中,再执行之前阻塞的代码。所以输出的结果的顺序为
1
=>fn2
=>3
=>2