首先推荐先去看 Promise
Promise 原理和使用 了解 JS 的 Promise
解决的异步回调编码风格。
而 Promise
的问题是,这种方式充满了 .then
方法,如果处理流程比较复杂,整段代码会充斥 then
,语义化不明显,代码依然不是太容易阅读。
基于这个原因,ES7 引入了 async/await
,这是 JavaScript 异步编程的一个重大改进,提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,使得代码逻辑更加清晰。可以参考下面这段改造示例代码:
// async/await 改造前
fetch('https://xxx.com').then(res1 => {
console.log(res1)
return fetch('https://kkk.com')
}).then(res2 => {
console.log(res2)
}).catch(err => {
console.log(err)
})
// async/await 改造后
async function foo() {
try {
let res1 = await fetch('https://xxx.com')
console.log(res1)
let res2 = await fetch('https://kkk.com')
console.log(res2)
} catch(err) {
console.error(err)
}
}
整段代码的异步处理逻辑都是使用同步代码的方式来实现的,而且还支持 try catch
来捕获异常,这就是完全在写同步代码,非常符合人的线性思维。
首先我们介绍生成器(Generator)是如何工作的,接着讲解 Generator 的底层实现机制——协程(Coroutine);因为 async/await
使用了 Generator 和 Promise 两种技术。
生成器函数是一个带星号函数,而且是可以暂停执行和恢复执行的,请看下面这段示例代码:
function* getDemo() {
console.log('开始执行第一段')
yield 'generator 1'
console.log('开始执行第二段')
yield 'generator 2'
console.log('开始执行第三段')
yield 'generator 3'
console.log('执行结束')
return 'generator finish.'
}
console.log('main')
let gen = getDemo()
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')
console.log(gen.next().value)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')
// 执行结果
main
demo.html:12 开始执行第一段
demo.html:24 generator 1
demo.html:25 main 1
demo.html:14 开始执行第二段
demo.html:26 generator 2
demo.html:27 main 2
demo.html:16 开始执行第三段
demo.html:28 generator 3
demo.html:29 main 3
demo.html:18 执行结束
demo.html:30 generator finish.
demo.html:31 main 4
观察输出结果发现,全局代码和 genDemo
函数是交替执行的。生成器函数内部执行一段代码,如果遇到 yield
关键字,JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行,外部函数可以通过 next
方法恢复函数的执行。
下面简单介绍下 JavaScript 引擎 V8 是如何实现一个函数的暂停和恢复的,这也有助于我们后面理解 async/await
的概念。首先了解协程的概念。
协程是一种比线程更加轻量级的存在,你可以将其看成是跑在线程上的任务,一个线程中可以存在多个协程,但是同时只能执行一个协程。如果当前执行的是 A 协程,要启动 B 协程就需要 A 协程将主线程的控制权交给 B 协程,此时 A 协程暂停执行,B 协程恢复执行,我们将 A 协程称为 B 协程的父协程。
正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。更重要的是,协程不被操作系统内核管理,完全由程序控制。这样带来的好处是性能得到很大的提升,不会像切线程那样消耗资源。
对照上述生成器函数的协程的四点规则如下:
genDemo
来创建一个协程 gen
,创建后,gen
协程并没有立即执行gen
协程执行,需要通过调用 gen.next
yield
关键字来暂停 gen
协程的执行,并返回主要信息给父协程return
关键字,JavaScript 引擎就会结束当前协程,并将 return
后的结果返回给父协程关于父协程和 gen
协程的交互执行中,他们的调用栈切换需要关注以下两点:
gen
协程和父协程是在主线程上交互执行的,而不是并发执行的,它们之间的切换是通过 yield
和 gen.next
来配合完成的gen
协程执行中调用 yield
时,JavaScript 引擎会保存 gen
协程当前的调用栈信息,并恢复父协程的调用栈信息。同样,当在父协程中执行 gen.next
时,JavaScript 引擎会保存父协程的调用栈信息,并恢复 gen
协程的调用栈信息所以,在 JavaScript 中,生成器就是协程的一种实现方式。下面是使用生成器和 Promise 来改造开头的那段 Promise 代码:
function* foo() {
let res1 = yield fetch('https://xxx.com')
console.log(res1)
let res2 = yield fetch('https://kkk.com')
console.log(res2)
}
let gen = foo()
function getGenPromise(gen) {
return gen.next().value
}
getGenPromise(gen).then(res1 => {
console.log(res1)
return getGenPromise(gen)
}).then(res2 => {
console.log(res2)
})
foo
函数是一个生成器函数,其内部实现了用同步代码形式来实现异步操作。我们将执行生成器的代码封装成一个函数,并将这个执行生成代码的函数称为执行器(参考 co
框架):
co(foo())
通过使用生成器配合执行器,就能实现使用同步的方式写异步代码了,大大加强代码可读性。
虽然执行器已经很好地满足我们的需求了,但是程序员的追求永无止境,ES7 引入了 async/await
,这种方式能够彻底告别执行器和生成器,实现更加直观简洁的代码。
根据MDN定义,async
是一个通过异步执行并隐式返回 Promise 作为结果的函数。例如下面这段代码
async function foo() {
return 2
}
console.log(foo()) // Promise {: 2}
下面结合协程来分析这段代码:
async function foo() {
console.log(1)
let a = await 100
console.log(a)
console.log(2)
}
console.log(0)
foo()
console.log(3)
执行流程如下:
console.log(0)
,输出 0
foo
函数,由于 foo
函数被 async
标记过,所以当进入该函数时,JavaScript 引擎会保存当前的调用栈等信息,然后执行 foo
函数中的 console.log(1)
并输出 1
foo
函数中的 await 100
await 100
时,JavaScript 引擎会默认创建一个 Promise 对象let promise_ = new Promise((resolve, reject) => {
resolve(100)
})
引擎会将该 resolve
任务提交给微任务队列promise_
对象返回给父协程console.log(3)
,并输出 3
resolve(100)
等待被执行,触发 promise_.then
中的回调函数promise_.then(value => {
// 回调函数被激活,将主线程控制权交给 foo 协程,并将 value 值传给协程
})
foo
协程激活后会把值给变量 a
,结束后将控制权归还给父协程正因为 async/await
在背后为我们做了大量的工作,所以我们才能用优雅的同步方式写出异步代码来。
开始之前,需要知道 async-await
的特点
await
后面跟的是 Promise
对象时,才会异步执行,其它类型的数据会同步执行Promise
对象一个函数如果加上 async
,那么该函数就会返回一个Promise
,可以把 async
看成将函数 return
返回值使用 Promise.resolve()
包裹了下
async function Func() {
return 'Func...'
}
console.log(Func())
Func().then(res => console.log(res)) // Func...
async
函数返回一个Promise
对象,可以使用then
方法添加回调函数。
当函数执行的时候,一旦遇到await
就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
async function asyncPrint(value, ms) {
await timeout(ms)
console.log(value)
}
asyncPrint('hello world', 2000);
//hello world (2s后)
上面代码指定2000毫秒以后,输出hello world
。同时函数asyncPrint()
执行结果返回了一个promise
对象。
async
用来表示函数是异步的,定义的函数会返回一个promise
对象,可以使用then
方法添加回调函数,而async
函数内部return
语句返回的值,就会成为then
方法回调函数的参数。针对上面的asyncPrint
例子来看,如果我们直接使用then
会发现什么都没有
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
async function asyncPrint(value, ms) {
await timeout(ms)
console.log(value)
}
asyncPrint('hello world', 2000).then((value)=>{console.log(value)})
//hello world (2s后)
//undefined
因为函数asyncPrint
内部没有return
语句返回值,那我们加上return
试一下
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
async function asyncPrint(value, ms) {
await timeout(ms)
console.log(value)
return 'done'
}
asyncPrint('hello world', 2000).then((value)=>{console.log(value)})
//hello world (2s后)
//done
其实就是相当于执行了Promise.resolve('done')
,如果没有return
就相当于Promise.resolve()
。
await
后面可以跟任何JS表达式,虽然await
可以跟很多类型的东西,但是最主要的意图是用来等待Promise
对象的状态被resolved
,如果await
的是promise
对象会造成异步函数停止执行并等待promise
解决,如果等到的是正常的表达式就立即执行,还是开头的那个例子,如果await
的不是promise
对象会怎么样
function timeout(ms) {
setTimeout(()=>{}, ms)
}
async function asyncPrint(value, ms) {
await timeout(ms)
console.log(value)
}
asyncPrint('hello world', 2000);
//hello world (立即)
因为await
后面不是promise对象了,也就不需要等待结果返回了,所以到这就直接往下执行了。
再来一个例子
function sleep(second) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('enough sleep~');
}, second);
})
}
function normalFunc() {
console.log('normalFunc');
}
async function awaitDemo() {
await normalFunc();
console.log('something, ~~');
let result = await sleep(2000);
console.log(result);// 两秒之后会被打印出来
}
awaitDemo();
// normalFunc
// something, ~~
// ---2s后---
// enough sleep~
第一个await
后面是一个正常表达式所以直接执行后继续往下执行,然后再遇到第二个await
,这个await
后面是一个异步操作,所以需要等待结果返回并执行后再继续往下执行,也就是过了2s以后再打印最后的值。
举例说明,假设有三个请求需要发生,第三个请求是依赖第二个请求的解析,第二个请求是依赖第一个请求的解析
如果要用ES5实现就会有三个回调
如果用Promise就会至少有三个then
一个是横向代码很长,一个是纵向代码很长,如果用async-await
来实现呢?
//我们仍然使用 setTimeout 来模拟异步请求
function sleep(second, param) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(param);
}, second);
})
}
async function test() {
let result1 = await sleep(1000, 'req01');
console.log(result1)
let result2 = await sleep(1000, 'req02' + result1);
console.log(result2)
let result3 = await sleep(1000, 'req03' + result2);
console.log(result3)
}
test();
//req01 --1s后--
//req02req01 --2s后--
//req03req02req01 --3s后--
难怪说async-await
是promise
的语法糖了,其实还是promise
,只是代码阅读起来让你觉得它好像是一个同步请求,不用那么多回调或者then
了。amazing!!!
如果有多个await命令后面的异步操作之间不存在相互依赖的关系,那我们当然就不能使用上面实例中的方法来使用async-await了
例如有三个异步请求需要发送,但是相互之间没有关联,要做的其实就是当所有异步请求结束后清除界面上的loading,如果我们像上面那样使用当最后一个请求结束后清除loading,其实等待的时间就是三个请求时间之和:
function sleep(second) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('sth');
}, second);
})
}
async function clearLoad() {
await sleep(1000);
await sleep(1000);
await sleep(1000);
console.log('清除loading啦');
}
clearLoad();
// 清除loading啦 --3s后--
其实这里真正的需求是当最慢的那个请求发送结束了,就可以清除loading了,所以改良后是这样:
function sleep(second) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('sth');
}, second);
})
}
async function clearLoad() {
let p1 = sleep(1000);
let p2 = sleep(1000);
let p3 = sleep(1000);
await Promise.all([p1, p2, p3]);
console.log('清除loading啦');
}
clearLoad();
// 清除loading啦 --1s后--
所以,以上可以得出:async-await
是promise
的语法糖,让我们书写代码更加流畅,增强代码的可读性,它是建立在promise
机制之上的,并不能取代promise
。
async function foo() {
console.log('foo')
}
async function bar() {
console.log('bar start')
await foo()
console.log('bar end')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
bar()
new Promise(function(resolve) {
console.log('promise executor')
resolve()
}).then(function() {
console.log('promise then')
})
console.log('script end')