Blog
Blog copied to clipboard
执行机制 - JavaScript异步编程及Event Loop
JavaScript异步编程及Event Loop
异步编程
为何需要异步编程
JavaScript是单线程语言,也就是说同一时间只能运行一个任务。一般来说这没什么问题,但是如果运行耗时过长的任务,将会阻塞后续任务的执行,包括UI渲染。
所以一些非密集计算的任务,比如文件I/O,HTTP Request,定时器等任务,完全没必要在主线程等待其完成,而是应该在创建任务后交出主线程控制权,去执行其他任务,待其完成后再处理。这就引出了异步编程。
需要注意的是,ECMAScript(JavsScript的语言规范)并没有定义这些异步特性,所以异步特性的实现都需要依赖于JavaScript运行环境,例如浏览器、Node等。
如何实现异步编程
浏览器提供了JS引擎不具备的特性,我们称之为Web API
,例如我们常见的DOM事件监听、Ajax、定时器等。通过这些特性可以实现异步、非阻塞的行为。其执行机制会在下个部分 Event Loop 中详细讲解。
异步编程语法
JavaScript发展至今,异步编程语法主要有以下几种:
- 回调函数Callback
- Promise
- Generator
- Async / Await
/* ------------------- Callback ---------------- */
function asyncFn(callback) {
setTimeout(() => {
callback()
}, 1000)
}
const callbackFn = () => console.log('Callback has been invoked')
asyncFn(callbackFn)
/* ------------------- Promise ---------------- */
function asyncFn() {
return new Promise(resolve => {
setTimeout(() => resolve(), 1000)
})
}
asyncFn().then(name => console.log('Promise fulfilled')
/* ------------------- Generator ---------------- */
// Generator生成器函数执行时会返回一个Generator迭代器
// 也就是说Generator本身只是一个状态机,需要由调用者来改变它的状态
function *fetchUser () {
const user = yield ajax()
console.log(user)
}
const f = fetchUser()
// 加入的控制代码
const result = f.next()
result.value.then((d) => {
f.next(d)
})
/* ------------------- Async/Await ---------------- */
// Async/Await 可以理解为是 Generator + Promise 的语法糖
// async 对应 *,await 对应 yield,然后自动实现Generator的流程控制
async function getUser() {
const user = await ajax()
return user
}
// getUser用Generator + Promise表示并执行
function *getUser() {
const user = yeild ajax()
return user
}
const g = getUser()
const result = g.next()
result.value.then(res => {
g.next(res)
})
Event Loop
首先推荐一个Event Loop可视化执行的网站。
基本概念
- 调用栈(Call Stack):也叫执行栈,用于执行任务,是一个栈数据结构,具有后进先出的特点(Last in, first out. LIFO)。
-
宏任务:包括
Script标签中的直接运行代码
、Web API
加入任务队列的回调函数。 -
微任务:包括
Promise
、process.nextTick
、MutaionObserver
、queueMicrotask
的回调函数。 - 任务队列(Task Queue):用于存放等待执行的宏任务,具有先进先出的特点(First in, first out. FIFO),调用栈为空时会取出任务队列中第一个任务执行(若存在)。
- 微任务队列(MicroTask Queue):用于存放等待执行的微任务,具有先进先出的特点(First in, first out. FIFO),每个宏任务执行完结时,会依次执行微任务队列中的微任务。
-
Web API:包括
定时器
、xhr
、DOM Event
等,提供了处理异步任务的能力,异步任务完成后,将回调函数放入对应的任务队列(即微任务会放入微任务队列)。
执行机制
- 执行同步代码,碰到异步代码交由
Web API
处理(异步任务完成后Web API
将回调函数加入相应任务队列/微任务队列); - 同步代码执行完成后,检查微任务队列中是否存在待执行微任务,存在则取出第一个微任务执行,执行完成后再次检查执行,直至微任务队列为空;
需要注意的是,若微任务执行过程中往微任务队列加入了新的微任务,也会在本步骤内被执行。
- 此时调用栈为空,检查任务队列中是否存在待执行宏任务,存在则取出第一个宏任务执行,也就是回调了第1步循环进行。
巩固练习
// 题目出自https://juejin.im/post/5a6155126fb9a01cb64edb45#heading-2
console.log(1)
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(3)
resolve()
}).then(() => {
console.log(4)
})
})
new Promise(resolve => {
console.log(5)
resolve()
}).then(() => {
console.log(6)
})
- 执行同步代码,打印
1
; - 执行setTimeout,交由Web API处理,Web API将其加入任务队列;
- 执行new Promise,其参数函数为同步执行,打印
5
,然后该Promise变为fulfilled状态,将then内回调加入微任务队列; - 当前任务执行完毕,查看微任务队列,有一个待执行微任务,取出执行,打印
6
; - 微任务队列为空,查看任务队列,有一个待执行宏任务,及setTimeout的回调,取出执行;
-
打印
2
,同步执行new Promise中函数,打印3
,Promise置为fulfilled,将then内回调加入为任务队列; - 当前任务执行完毕,查看微任务队列,有一个待执行微任务,取出执行,打印
4
; - 所有任务执行完毕,打印顺序为
1 5 6 2 3 4
。
加强练习
理解了JS的执行机制,碰到类似题目就可以轻松应对:
// 题目出自https://juejin.im/post/5a6155126fb9a01cb64edb45
console.log(1)
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(3)
resolve()
}).then(() => {
console.log(4)
})
})
new Promise(resolve => {
console.log(5)
resolve()
}).then(() => {
console.log(6)
}).then(() => {
console.log(7)
})
new Promise(resolve => {
console.log(8)
resolve()
}).then(() => {
console.log(9)
}).then(() => {
console.log(10)
})
答案为1 5 8 6 9 7 10 2 3 4
。