内容引用自Understanding the Event Loop, Callbacks, Promises, and Async/Await in JavaScript | DigitalOcean
EventLoop
有这么一段代码
function first(){
console.log(1)
}
function second(){
setTimeout(()=>{
console.log(2)
}, 0)
}
function third(){
console.log(3)
}
first()
second()
third()
// 依次输出:1,3,2
原理很简单:异步代码总会在同步代码执行完成后再执行。
发生这种情况的原因就在于名为Event Loop的机制去处理并发(或叫做并行)事件。
因为JS一次只能运行一个任务(单线程),所以就需要event loop机制去告知何时执行特定的指令。event loop的实现是基于栈(称之为调用栈)和队列(称之为任务队列)。
上段代码的运行过程如下:
- 把
first()推入调用栈,执行该函数,然后出栈 - 再把
second()推入调用栈,执行该函数。 - 把
setTimeout推入调用栈,执行该函数,该函数启动了一个计时器然后把匿名函数推入到任务队列,执行完毕,setTimeout出栈。 third()入栈,执行,出栈- event loop会检查任务队列里是否有待执行的任务,发现有一个打印出2的匿名函数,则把该匿名函数从任务队列推出,任务队列清空。再推入到调用栈,再执行,然后出栈,调用栈清空。
小结
- event loop是基于(调用)栈和(任务)队列实现的
- 任务队列可以看作是函数的等待区,用于存储待执行的任务。
- 当调用栈清空后,会检查任务队列里是否有待执行的任务,根据队列FIFO的原则,清空任务队列,依次推入调用栈。最终实现调用栈和任务队列的双清空。
setTimeout的第二个参数0代表的含义并不是0秒后执行,而是指在0秒内推入到任务队列当中。- 调用栈和任务队列不是简单的上下层级结构,可以是多层。可以看如下代码:
function first() {
console.log(1);
}
function second() {
setTimeout(() => {
console.log(4);
setTimeout(() => {
console.log(2);
setTimeout(() => {
console.log(7);
}, 0);
}, 0);
}, 2000);
}
function third() {
console.log(3);
setTimeout(() => {
console.log(5);
setTimeout(() => {
console.log(6);
}, 0);
}, 0);
}
first();
second();
third();
// 输出:1,3,5,6,4,2,7
上面的代码别因为看着很烧脑而灰心。因为这多少有点回调地狱,正常人都会绕进去,但根据上述小总结,稍稍花点时间一定能想明白。
Promise
这不是面向新手的教程,主要是自己学习的总结,所以跳过回调和回调地狱的概念,直接了解下Promise。
Promise代表一个异步函数的完成。It is an object that might return a value in the future. It accomplishes the same basic goal as a callback function, but with many additional features and a more readable syntax. As a JavaScript developer, you will likely spend more time consuming promises than creating them, as it is usually asynchronous Web APIs that return a promise for the developer to consume.
我们创建一个Promise对象
const promise = new Promise((resolve, reject)=>{
// resolve('We did it!')
})
console.log(promise)
// 输出
// Promise{<pending>}
// [[Prototype]]: Promise
// [[PromiseState]]: "pending"
// [[PromiseResult]]: undefined
Promise对象有三个状态值:
- pending: Initial state before being resolved or rejected
- fulfilled: Successful operation, promise has resolved
- rejected: Failed operation, promise has rejected
当promise被resolve了之后,就可以使用then方法,该方法会返回PromiseResult值作为参数。
模拟异步请求
我们使用Promise和setTimeout模拟一个异步请求,请求数据
const promise = new Promise((resolve, reject)=>{
setTimeout(() => resolve('Resolving an asynchronous request!'), 2000)
})
promise.then((response)=>{
console.log(response)
})
通过使用then方法,保证了这个response只会在2秒后打印出来,不需要使用内嵌的回调函数。
如果then方法return了一个值,那么就可以在末尾再使用then方法,实现链式调用。
// Chain a promise
promise
.then((firstResponse) => {
// Return a new value for the next then
return firstResponse + ' And chaining!'
})
.then((secondResponse) => {
console.log(secondResponse)
})
Promise和容错处理
一个异步请求,通常可能会因为服务端的错误而返回与预期相悖的结果,这时候就需要容错处理。看代码:
function getUsers(onSuccess){
return new Promise((resolve, reject)=>{
setTimeout(()=>{
if(onSuccess){
resolve([
{id: 1, name: 'Jerry'},
{id: 2, name: 'Elaine'},
{id: 3, name: 'George'},
])
} else {
reject('Failed to fetch data')
}
}, 1000)
})
}
getUsers(false)
.then((response)=>{
console.log(response)
})
.catch((error)=>{
console.error(error)
})
.finally(()=>{
console.log('request finished')
})
getUsers函数模拟了一个异步请求用户列表的功能,请求成功则返回用户数据,请求失败则返回抓取失败的响应。
如果我们传入一个false值,则会调用catch方法,把响应信息打印出来,如果传入true,则会把用户数据打印出来。但不管传入true或false,最终都会执行finally方法。
Promise代码案例
下面的fetch方法是个两步过程,需要做链式调用(当时面试问Promise就是因为这个东西搞混了,犯了一个很低级的错误...)
// Fetch a user from the GitHub API
fetch('https://api.github.com/users/octocat')
.then((response) => {
return response.json()
})
.then((data) => {
console.log(data)
})
.catch((error) => {
console.error(error)
})
async和await
这两个关键字是ES7引入的,其实就是写异步代码的语法糖,可以用写同步任务的逻辑去处理异步任务。上面的代码使用语法糖就可以这么写:
// Handle fetch with async/await
async function getUser() {
const response = await fetch('https://api.github.com/users/octocat')
const data = await response.json()
console.log(data)
}
// Execute async function
getUser()
then方法的链式调用就不再需要了,直接两个await解决,有了语法糖的存在,上述异步代码就看着非常直观了。
容错处理的话也不需要用catch方法了,加上finally的话直接使用try-catch-finally结构:
// Handling success and errors with async/await
async function getUser() {
try {
// Handle success in try
const response = await fetch('https://api.github.com/users/octocat')
const data = await response.json()
console.log(data)
} catch (error) {
// Handle error in catch
console.error(error)
} finally {
console.log('finished')
}
}
总结
现在的异步js代码通常直接使用async/await语法糖,但还是要了解Promise的原理,Promise有语法糖无法取代的额外特征,详情还是翻阅mdn文档。