@gyyin
2020-04-03T15:23:15.000000Z
字数 7019
阅读 565
慕课专栏
上篇文章,我们介绍过了最常见的两种解决异步的方式 —— 回调函数和 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 两个新成员。