那么有没有一种方法,不需要我们手动调用 next,可以让 generator 自动执行呢?
你可能会想到,既然可以通过 done 来判断是否执行结束,那么用 while 循环不就行了?
let g = gen(),
res = g.next();
while(!res.done){
console.log(res.value);
res = g.next();
}
这样看起来是可以一下子全部执行了,但对于需要上个请求结束后再发送下个请求的场景,这里是无法保证顺序的。
那么我们是否可以利用 Promise 来保证前一步执行完,才能执行后一步呢?参考上述代码,我们可以用递归来实现。
在每次请求拿到 data 后,将这个 data 通过 next 传给下一次的 yield,这样就实现了自动执行。
function run(gen) {
const g = gen();
function next(data) {
const result = g.next(data);
if (result.done) return;
result.value
.then(data => data.json())
.then(data => next(data))
}
next();
}
run(fetchGenerator);
function myAsync(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
const gen = fn.apply(self, args);
gen.next();
})
}
}
这样一个大体的结构就实现了,但现在这个 myAsync 函数只能够让 generator 执行一次,还需要做到让它自动迭代。参考前文我们实现的那个 run 方法,这里考虑使用递归实现。
可以设计一个 next 方法,来递归调用这个 next,在 next 中去执行 generator 的 next 方法。是否执行结束则通过 done 属性来判断。
function next(value) {
try {
let result = gen.next(value);
let value = result.value;
if (result.done) {
return resolve(value);
}
return next(value);
} catch (err) {
reject(err)
return
}
}
next 函数是成功实现了,可是还有一些问题,比如 await 后面是可以跟一个原始类型的值的,会默认用 Promise.resolve 将其包起来。这里还需要将返回值用 Promise.resolve 再进行一次封装。
function next(value) {
try {
let result = gen.next(value);
let value = result.value;
if (result.done) {
return resolve(value);
}
return Promise.resolve(value).then(next);
} catch (err) {
reject(err);
return
}
}
function error(err) {
try {
let result = gen.throw(err);
let value = result.value;
} catch (err) {
reject(err);
return
}
}
function next(value) {
try {
let result = gen.next(value);
let value = result.value;
if (result.done) {
return resolve(value);
}
return Promise.resolve(value).then(next, error);
} catch (err) {
reject(err);
return
}
}
最终的完成版如下:
function myAsync(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
const gen = fn.apply(self, args);
function run(gen, resolve, reject, next, error, key, arg) {
try {
let result = gen[key](arg);
let value = result.value;
if (result.done) {
return resolve(value);
}
return Promise.resolve(value).then(next, error);
} catch (err) {
reject(err);
return
}
}
function next(value) {
run(gen, resolve, reject, next, error, 'next', value);
}
function error(err) {
run(gen, resolve, reject, next, error, 'throw', err);
}
next();
})
}
}
找几个例子来测试一下这个 myAsync 函数到底行不行。
const test = myAsync(function *() {
const a = yield 1;
const b = yield Promise.resolve(2);
const c = yield Promise.reject(3);
return [a, b, c]
});
test()
.then(res => console.log('res', res))
.catch(err => console.log('err', err))
// err 3
1. 前言
上篇文章,我们介绍过了最常见的两种解决异步的方式 —— 回调函数和 Promise,这篇文章我们进一步介绍两种终极解决异步的方法 —— generator 和 async/await。
2. generator
generator 是一个状态机,内部封装了状态。generator 返回了一个遍历器对象,可以遍历函数内部的每一个状态。 generator 函数声明的时候,在 function 和函数名之间会有一个星号*用来说明当前是一个 generator 函数,同时在函数体内会有 yield 关键字,用于定义状态。我们通过 next 方法不断地调用。
从图上可以看到,每次执行完 next 方法后,会返回一个对象,里面有 value 和 done 两个值。 value 就是 yield 表达式后面的返回值。done 则表示函数是否终止。当执行完所有的 yield 后,最后一次返回的 done 就是 true。 同时,next 也可以接受一个参数,作为上一个 yield 的返回值。
给第二个 next 方法传了参数 10,这个 10 就是第一个 yield 1 执行后返回的结果,赋值给了a,因此第二个 yield 得到的结果是20。
2.1 generator 的异步用法
由于我们可以在外部获得 generator 的执行控制权,也能通过 value 拿到执行后的结果,所以 generator 也可以被用来处理异步。 我们以前面的红绿灯为例:
在这种场景下看,generator 比 Promise 和回调函数都要更加简洁。但是在调用的时候又会比较繁琐,我们每次都需要手动调用 next 方法,而不是自动执行。
如果是在网络请求中,generator 调用会更加复杂。
我们在调用 gen 函数获得返回结果的时候,就需要这么做。
因为 fetch 函数执行后返回的是一个 Promise,所以
result.value
是一个 Promise,需要通过 then 来获取到请求到的数据,再将 data 传给gen.next
,让 yield 后面的代码可以继续正常执行。2.2 自动执行
这是只有一个请求的情况,如果有多个请求呢?那岂不是要多次调用 then?这样代码的可读性非常差了。
那么有没有一种方法,不需要我们手动调用 next,可以让 generator 自动执行呢? 你可能会想到,既然可以通过 done 来判断是否执行结束,那么用 while 循环不就行了?
这样看起来是可以一下子全部执行了,但对于需要上个请求结束后再发送下个请求的场景,这里是无法保证顺序的。 那么我们是否可以利用 Promise 来保证前一步执行完,才能执行后一步呢?参考上述代码,我们可以用递归来实现。 在每次请求拿到 data 后,将这个 data 通过 next 传给下一次的 yield,这样就实现了自动执行。
其实著名的 co 模块也是为了解决这个问题而出现的,这个则是 co 模块的简化版实现。
3. async
在 ES2017 中引入了 async 函数,async 函数是处理异步的终极方案。相比 generator,async 不需要我们一步步调用 next 方法。同时,async 返回了一个 Promise,而不是一个 Iterator。
我们可以清楚地看到,async 和 generator 写法很像,用 async 关键字代替星号,await 关键字代替 yield。 我们使用 async 来解决上面那个红绿灯的例子(当然了,green 和 red 方法都是用 Promise 来实现的)。
3.1 异常捕获
从这个例子看着,async 是不是比 generator 方便了很多?除此之外,相对于 Promise,async 在错误捕获方面更加优秀。 由于 Promise 异步错误无法通过
try...catch
来捕获,所以我们一般会用try...catch 来捕获 Promise 构造函数中的错误,用
.catch` 来捕获异步错误,这样实际上非常繁琐。而在 async 里面,捕获错误变得如此简单,只需要用
try...catch
就能够捕获到所有异常。除此之外,我们还可以让 await 后面的 Promise 直接调用 catch,这样可以避免 `try...catch 处理多个不同错误时导致的问题。
3.2 断点调试
同时,由于
Promise then
的异步缘故,导致打断点的时候,经常会先走后面的代码,再回到 then 里面,这样对于断点调试来说非常不方便。但是在 async 里面,断点调试表现就像同步一样,让调试更加方便。
3.3 注意
在使用 async 的时候,经常会有人滥用 await,导致原本没有依赖关系的两个操作必须按顺序才能执行,比如:
可以看到原本没有关联的两个接口 fetchUserInfo 和 fetchHotelList,现在 fetchHotelList 必须要等 fetchUserInfo 接口获取到数据后才开始调用,大大提高了原本要花费的时间。 有两种方式可以解决这个问题。 我们可以改变一下 await 的写法,让两个接口同时调用,再用 await 去等待返回值,这样耗时是请求时间最长的那个。
还可以用
Promise.all
来解决这个问题,原理和上面的例子差不多。3.4 在循环中使用
如果在循环中使用
async/await
,那么就要注意一下,在 for 循环和 each/map 中完全是不同的意思。 在 for 循环中表现的是继发,后面的一定会等待前面的返回后才会执行。而在 forEach 和 map 中则表现为并发。这是为什么呢?
究其原因是 forEach 并不是一个 async 函数,执行 callback 的时候没有用await等待返回,这样自然是同时执行,而非后面的执行需要依赖前面的执行完成。
3.5 实现一个 await
await 的原理就是返回了一个 Promise,使用一个函数让 generator 自动迭代,而执行的时机则是由 Promise 来控制,其实原理和上面的 run 方法很相似。 首先定义一下 myAsync 方法,让其返回一个 Promise 对象。
这样一个大体的结构就实现了,但现在这个 myAsync 函数只能够让 generator 执行一次,还需要做到让它自动迭代。参考前文我们实现的那个 run 方法,这里考虑使用递归实现。 可以设计一个 next 方法,来递归调用这个 next,在 next 中去执行 generator 的 next 方法。是否执行结束则通过 done 属性来判断。
next 函数是成功实现了,可是还有一些问题,比如 await 后面是可以跟一个原始类型的值的,会默认用
Promise.resolve
将其包起来。这里还需要将返回值用Promise.resolve
再进行一次封装。如果你比较细心,还会发现虽然这个 generator 可以自执行了,但还缺少了错误处理,如果在 await 函数后面跟着一个失败的 Promise,该怎么处理呢? 所以我们来修改一下 next 函数,兼容错误处理,加上一个 error 函数来捕获。
最终的完成版如下:
找几个例子来测试一下这个 myAsync 函数到底行不行。
4. 总结
在平时使用中,Promise 和 async 使用最多,generator 比较不常用。async 在一定程度上可以简化代码,提高可读性和方便调试。但
Promise.all
和Promise.race
在一些场景下会更加适用。 在未来的一些新特性和提案上,Promise 家族还增加了Promise.allSettled
和Promise.any
两个新成员。