# async/await 如何通过同步的方法实现异步
- 是一种语法糖,基于 Generator 函数和自执行器实现
# generator 函数
- Generator 函数是一种状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,可以依次遍历Generator 函数内部的每一个状态,但是只有调用 next 方法才会遍历下一个内容状态,所以其实提供了一种可以暂停执行的函数,yield 表达式就是暂停标志。
function* testGenerator() {
yield 'hello';
yield 'world';
yield 'test';
var t = testGenerator()
1
2
3
4
5
6
7
2
3
4
5
6
7
t.next()
{value: "hello", done: false}
t.next()
{value: "world", done: false}
t.next()
{value: "test", done: false}
t.next()
{value: undefined, done: true}
t.next()
{value: undefined, done: true}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
- 从结果可以看出,Generator 函数被调用并不会执行,但还有调用了 next 方法、内部指针指向该语句才会执行,即”函数可以暂停,也可以恢复执行“。每次调用遍历器对象的 next 方法,就会返回一个有着 ”value“ 和 done 两个属性的对象。value 属性表示当前的内部状态值,是 yield 表达式后面那个表达式的值; done 属性是一个布尔值,表示是否遍历结束。
# Generator 函数暂停恢复执行原理
- 先了解一下协程的概念。
一个线程或函数执行到一半,可以暂停执行,将执行权交给另一个线程或函数,当到稍后收回执行权的时候,在恢复执行。这样可以并行执行,交换执行权的线程或函数,就称为协程。
- 协程是一个比线程更加轻量级的存在。普通线程是抢先式的,会争夺 CPU 资源,而协程是合作的,可以把协程看成是跑在线程上的任务,一个线程可以存在多个协程,但是在线程上同时只能执行一个协程。运行流程大致如下:
- 协程 A 开始执行
- 协程A执行到某个阶段,进入暂停,执行权转移到协程B
- 协程B执行完成或暂停,将执行权交还A
- 协程A恢复执行
- 协程遇到 yield 命令就会暂停,等到执行权返回,再从暂停的地方继续往后执行。它最大的优点,就是代码的写法非常像同步操作。
# 执行器
- 通常把执行器代码封装成一个函数,并把这个执行生成器代码的函数称为执行器, 著名的就是 co 模块。
Generator 是一个异步操作的容器,它的自动执行需要一种机制,当异步操作有了结果,就能自动交回执行权。有两种方法可以做到这一点:
- 回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
- Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。
- 基于 Promise 对象的简单自动执行器:
function run(gen){
var g = gen();
function next(data){
var result = g.next(data);
if (result.done) return result.value;
result.value.then(function(data){
next(data);
});
next();
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
使用
function* foo() {
let response1 = yield fetch('https://xxx') //返回promise对象
console.log('response1')
console.log(response1)
let response2 = yield fetch('https://xxx') //返回promise对象
console.log('response2')
console.log(response2)
run(foo);
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
- 上面的代码中, 只要 Generator 函数还没执行到最后一步,next 函数就会调用自身,以此实现自动执行。通过使用生成器配合执行器,就能实现使用同步的方式写出异步代码了,这样也大大加强了代码的可读性。
# async await
- ES7 中引入了 async/await,这种方式能够彻底告别执行器和生成器,实现更加直观简洁的代码。根据 MDN 定义, async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。可以说async 是Generator函数的语法糖,并对Generator函数进行了改进。 改造前面的代码,用 async 实现:
const foo = async () => {
let response1 = await fetch('https://xxx')
console.log('response1')
console.log(response1)
let response2 = await fetch('https://xxx')
console.log('response2')
console.log(response2)
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
- 比较发现,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await。 async 函数对 Generator 函数的改进,体现在下面几点:
- 内置执行器。 Generator 函数的执行必须依靠执行器,而 async 函数自带执行器,无需手动执行 next 方法。
- 更好的语义。 async 和 await,相比于 * yield,语义更清楚了。async 表示函数里面有异步操作,await 表示紧跟在后面的表达式需要等待结果。
- 更广的适用性。 co 模块约定,yield 命令后面只能是 thunk 函数或者 Promise 对象,而 async 函数的 await 命令后面,可以是 Promise 对象和原始类型的值(数值,字符串和布尔值,但这时会自动转成立即 resolve 的 Promise)
- 返回值是 Promise 。 async 函数返回值是 Promise 对象,比 Generator 函数返回的 Iterator 对象方便,可以直接使用 then 方法进行调用。
- 这里的重点是自带了执行器,相当于把我们要额外做的(写执行器/依赖co模块)都封装了在内部。比如:
async function fn(args) {
// ...
1
2
3
2
3
等同于:
function fn(args) {
return spawn(function* () {
// ...
});
function spawn(genF) { //spawn函数就是自动执行器,跟简单版的思路是一样的,多了Promise和容错处理
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
if(next.done) {
return resolve(next.value);
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
step(function() { return gen.next(undefined); });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# async / await 执行顺序
- 通过上面的分析,async 隐式返回 Promise 作为结果的函数,那么简单理解为, await 后面的函数执行完毕时,await 会产生一个 微任务 。但是我们需要注意这个微任务执行的时机, 它是执行完await之后,直接跳出 async 函数,执行其他代码(此处就是协程的运作,A暂停执行,控制权给 B).其他代码执行完毕后,再回到 async 函数去执行剩下的代码,然后把 await后面的代码注册到微任务队列中。
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
async function async2() {
console.log('async2 end')
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
.then(function() {
console.log('promise1')
.then(function() {
console.log('promise2')
console.log('script end')
script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
- 按照常理感觉应该是
script start
async2 end
async1 end
Promise
script end
promise1
promise2
setTimeout
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
- 但新版的 v8 规范修改以后,执行的结果发生了改变
- 如果 await 后面直接跟一个变量,await 1 相当于把 await 后面的代码注册了一个微任务(可以理解为 promise.then(await 下面的代码))。然后跳出 async 函数,执行其他代码,当遇到 promise 函数时,先执行同步代码,然后会注册 promise.then标记为 then2 函数到微任务队列,此时 then2 在微任务 await 后面,所以执行完同步后,执行微任务会先执行 async1 end 然后再去执行 promise.then
# 如果 await 后面跟的是一个异步函数的调用
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
async function async2() {
console.log('async2 end')
return Promise.resolve().then(()=>{
console.log('async2 end1')
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
.then(function() {
console.log('promise1')
.then(function() {
console.log('promise2')
console.log('script end')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
结果是:
script start
async2 end
Promise
script end
async2 end1
promise1
promise2
async1 end
setTimeout
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
-
分析:
- 此时 await 并不先把 await后面的代码注册到 微任务队列中去,而是执行完 await 后,直接跳出 async1 函数,执行其他同步代码,遇到 Promise 把 .then 注册为微任务,其它代码执行完后,”回到 async1 函数中去执行剩下的代码“(也就是 async2 返回的异步代码),然后再把 await 后面的代码注册到微任务队列中,但此时注意”前面已经注册了 微任务.then“所以会出现执行完 Promise.then 后,才会执行 async1 end。
# JS 异步解决方案的发展历程以及优缺点
# 1 回调函数 callback
function test(cb) {
cb()
function cb() {
test()
1
2
3
4
5
6
7
2
3
4
5
6
7
-
缺点:回调嵌套太多 容易引起回调地狱,不能用 try catch 捕获错误,不能 return
-
优点:解决了同步的问题
# Promise
- Promise 就是为了解决 callback 的问题而产生的。
- Promise 实现了链式调用,也就是每次 then 返回的都是一个全新的 promise,如果在 then 中 return ,return 的结果会被 promise.resolve 包装。
- 优点:解决了回调地狱的问题
featch().then()
1
- 缺点: 无法取消 Promise,错误需要通过回调函数来捕获。
# Generator
- 优点:可以控制函数执行,可以配合 co 函数库使用
function *fetch() {
yield ajax('XXX1', () => {})
yield ajax('XXX2', () => {})
yield ajax('XXX3', () => {})
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# async/await
- 异步的终极解决方案
- 优点:代码清晰,不用像 Promise 写一堆 then 链,处理了回调地狱的问题
- 缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。
JavaScript 事件循环 宏任务微任务 Promise