@gyyin
2020-04-03T23:23:15.000000Z
字数 7019
阅读 356
慕课专栏
上篇文章,我们介绍过了最常见的两种解决异步的方式 —— 回调函数和 Promise,这篇文章我们进一步介绍两种终极解决异步的方法 —— generator 和 async/await。
generator 是一个状态机,内部封装了状态。generator 返回了一个遍历器对象,可以遍历函数内部的每一个状态。
generator 函数声明的时候,在 function 和函数名之间会有一个星号*用来说明当前是一个 generator 函数,同时在函数体内会有 yield 关键字,用于定义状态。我们通过 next 方法不断地调用。
function* test() {
yield 1;
yield 2;
yield 3;
}
从图上可以看到,每次执行完 next 方法后,会返回一个对象,里面有 value 和 done 两个值。
value 就是 yield 表达式后面的返回值。done 则表示函数是否终止。当执行完所有的 yield 后,最后一次返回的 done 就是 true。
同时,next 也可以接受一个参数,作为上一个 yield 的返回值。
function* test() {
const a = yield 1;
const b = yield a * 2;
return b;
}
const gen = test()
gen.next() // {value: 1, done: false}
gen.next(10) // {value: 20, done: false}
gen.next() // {value: undefined, done: true}
给第二个 next 方法传了参数 10,这个 10 就是第一个 yield 1 执行后返回的结果,赋值给了a,因此第二个 yield 得到的结果是20。
由于我们可以在外部获得 generator 的执行控制权,也能通过 value 拿到执行后的结果,所以 generator 也可以被用来处理异步。
我们以前面的红绿灯为例:
function* lightGenerator() {
yield green(60);
yield red(60);
yield green(60);
yield red(60);
yield green(60);
yield red(60);
}
在这种场景下看,generator 比 Promise 和回调函数都要更加简洁。但是在调用的时候又会比较繁琐,我们每次都需要手动调用 next 方法,而不是自动执行。
const gen = lightGenerator();
gen.next();
gen.next();
gen.next();
gen.next();
gen.next();
gen.next();
如果是在网络请求中,generator 调用会更加复杂。
function* fetchGenerator(){
const url = 'https://baidu.com'; // 假设请求的是百度
const res = yield fetch(url);
return res;
}
我们在调用 gen 函数获得返回结果的时候,就需要这么做。
const gen = fetchGenerator(),
result = gen.next();
result.value
.then(data => data.json())
.then(data => gen.next(data))
因为 fetch 函数执行后返回的是一个 Promise,所以 result.value
是一个 Promise,需要通过 then 来获取到请求到的数据,再将 data 传给 gen.next
,让 yield 后面的代码可以继续正常执行。
这是只有一个请求的情况,如果有多个请求呢?那岂不是要多次调用 then?这样代码的可读性非常差了。
function* fetchGenerator(){
const res1 = yield fetch('https://baidu.com');
const res2 = yield fetch('https://google.com');
const res3 = yield fetch('https://bing.com');
return [res1, res2, res3];
}
const gen = fetchGenerator(),
result = gen.next();
result.value
.then(data => data.json())
.then(data => gen.next(data).value)
.then(data => data.json())
.then(data => gen.next(data).value)
.then(data => data.json())
.then(data => gen.next(data).value)
那么有没有一种方法,不需要我们手动调用 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);
其实著名的 co 模块也是为了解决这个问题而出现的,这个则是 co 模块的简化版实现。
在 ES2017 中引入了 async 函数,async 函数是处理异步的终极方案。相比 generator,async 不需要我们一步步调用 next 方法。同时,async 返回了一个 Promise,而不是一个 Iterator。
async function foo(){
const res1 = await fetch('https://baidu.com');
const res2 = await fetch('https://google.com');
const res3 = await fetch('https://bing.com');
}
foo();
我们可以清楚地看到,async 和 generator 写法很像,用 async 关键字代替星号,await 关键字代替 yield。
我们使用 async 来解决上面那个红绿灯的例子(当然了,green 和 red 方法都是用 Promise 来实现的)。
const green = (time) => {
return new Promise(resolve => {
setTimeout(() => {
console.log('green')
resolve()
}, time)
})
}
const red = (time) => {
return new Promise(resolve => {
setTimeout(() => {
console.log('red')
resolve()
}, time)
})
}
async function light() {
await green(60);
await red(60);
await green(60);
await red(60);
await green(60);
await red(60);
}
light();
从这个例子看着,async 是不是比 generator 方便了很多?除此之外,相对于 Promise,async 在错误捕获方面更加优秀。
由于 Promise 异步错误无法通过 try...catch
来捕获,所以我们一般会用 try...catch 来捕获 Promise 构造函数中的错误,用
.catch` 来捕获异步错误,这样实际上非常繁琐。
function test() {
try {
new Promise((resolve) => {
// ...
}).then(() => {
}).catch(() => {
})
} catch (err) {
console.log(err)
}
}
而在 async 里面,捕获错误变得如此简单,只需要用 try...catch
就能够捕获到所有异常。
async function test() {
try {
const res = await fetch('www.baidu.com');
} catch (err) {
console.log(err)
}
}
除此之外,我们还可以让 await 后面的 Promise 直接调用 catch,这样可以避免 `try...catch 处理多个不同错误时导致的问题。
async function test() {
const res = await fetch('www.baidu.com')
.catch(err => {
console.log(err)
})
}
同时,由于 Promise then
的异步缘故,导致打断点的时候,经常会先走后面的代码,再回到 then 里面,这样对于断点调试来说非常不方便。
但是在 async 里面,断点调试表现就像同步一样,让调试更加方便。
在使用 async 的时候,经常会有人滥用 await,导致原本没有依赖关系的两个操作必须按顺序才能执行,比如:
async function test() {
const getUserInfo = await fetchUserInfo();
const getHotelList = await fetchHotelList();
}
可以看到原本没有关联的两个接口 fetchUserInfo 和 fetchHotelList,现在 fetchHotelList 必须要等 fetchUserInfo 接口获取到数据后才开始调用,大大提高了原本要花费的时间。
有两种方式可以解决这个问题。
我们可以改变一下 await 的写法,让两个接口同时调用,再用 await 去等待返回值,这样耗时是请求时间最长的那个。
async function test() {
const getUserInfo = fetchUserInfo();
const getHotelList = fetchHotelList();
await getUserInfo;
await getHotelList;
}
还可以用 Promise.all
来解决这个问题,原理和上面的例子差不多。
function test() {
Promise.all([fetchUserInfo(), fetchHotelList()])
.then(dataArr => {
})
}
如果在循环中使用 async/await
,那么就要注意一下,在 for 循环和 each/map 中完全是不同的意思。
在 for 循环中表现的是继发,后面的一定会等待前面的返回后才会执行。
async function execute(promises) {
const len = promises.length;
for(let i = 0; i < len; i++) {
await promises[i]()
}
}
而在 forEach 和 map 中则表现为并发。这是为什么呢?
async function execute(promises) {
promises.forEach(async p => {
await p()
})
}
究其原因是 forEach 并不是一个 async 函数,执行 callback 的时候没有用await等待返回,这样自然是同时执行,而非后面的执行需要依赖前面的执行完成。
// 实际上 forEach 并不是一个 async 函数
const forEach = (arr, callback) => {
let len = arr.length;
for(let i = 0; i < len; i++) {
callback(arr[i], i, arr);
}
}
await 的原理就是返回了一个 Promise,使用一个函数让 generator 自动迭代,而执行的时机则是由 Promise 来控制,其实原理和上面的 run 方法很相似。
首先定义一下 myAsync 方法,让其返回一个 Promise 对象。
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
}
}
如果你比较细心,还会发现虽然这个 generator 可以自执行了,但还缺少了错误处理,如果在 await 函数后面跟着一个失败的 Promise,该怎么处理呢?
所以我们来修改一下 next 函数,兼容错误处理,加上一个 error 函数来捕获。
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
在平时使用中,Promise 和 async 使用最多,generator 比较不常用。async 在一定程度上可以简化代码,提高可读性和方便调试。但 Promise.all
和 Promise.race
在一些场景下会更加适用。
在未来的一些新特性和提案上,Promise 家族还增加了 Promise.allSettled
和 Promise.any
两个新成员。