解决回调地狱的异步操做,Async 函数是终极办法,但了解生成器和 Promise 有助于理解 Async 函数原理。因为内容较多,分三部分进行,这是第三部分,介绍 Async 函数相关。第一部分介绍 Generator,第二部分介绍 Promise。ios
在这部分中,咱们会先介绍 Async 函数的基本使用,而后会结合前两部分介绍的生成器和 Promise 实现一个 async 函数。git
经过在普通函数前加async
操做符能够定义 Async 函数:github
// 这是一个 async 函数
async function() {}
复制代码
Async 函数体中的代码是异步执行的,不会阻塞后面代码执行,但它们的写法和同步代码类似。ajax
Async 函数会 返回一个已完成的 promise 对象,实际在使用的时候会和await
操做符配合使用,在介绍await
以前,咱们先看看 async 函数自己有哪些特色。express
若是 async 函数体内若是没有await
操做符,那么它返回的 promise 对象状态和他的函数体内代码怎么写有关系,具体和 promise 的then()
方法的处理方式相同:axios
1)没有显式 return 任何数据segmentfault
此时默认返回Promise.resolve()
:promise
var a = (async () => {})();
复制代码
至关于网络
var a = (async () => {
return Promise.resolve();
})();
复制代码
此时 a 的值:异步
a {
[[PromiseStatus]]: 'resolved',
[[PromiseValue]]: undefined
}
复制代码
2)显式 return 非 promise
至关于返回Promise.resolve(data)
var a = (async () => {
return 111;
})();
复制代码
至关于
var a = (async () => {
return Promise.resolve(111);
})();
复制代码
此时 a 的值:
a {
[[PromiseStatus]]: 'resolved',
[[PromiseValue]]: 111
}
复制代码
3)显式 return promise 对象
此时 async 函数返回的 promise 对象状态由显示返回的 promise 对象状态决定,这里以被拒绝的 promise 为例:
var a = (async () => Promise.reject(111))();
复制代码
此时 a 的值:
a {
[[PromiseStatus]]: 'rejected',
[[PromiseValue]]: 111
}
复制代码
但实际使用中,咱们不会向上面那样使用,而是配合await
操做符一块儿使用,否则像上面那样,和 promise 相比,并无优点可言。特别的,没有await
操做符,咱们并不能用 async 函数解决相互依赖的异步数据的请求问题。
换句话说:咱们不关心 async 返回的 promise 状态(一般状况,async 函数不会返回任何内容,即默认返回Promise.resolve()
),咱们关心的是 async 函数体内的代码怎么写,由于里面的代码能够异步执行且不阻塞 async 函数后面代码的执行,这就为写异步代码创造了条件,而且书写形式上和同步代码同样。
await
操做符使用方式以下:
[rv] = await expression;
复制代码
expression:能够是任何值,但一般是一个 promise;
rv: 可选。若是有且 expression 是非 promise 的值,则 rv 等于 expression 自己;否则,rv 等于 兑现 的 promise 的值,若是该 promise 被拒绝,则抛个异常(因此await
通常被 try-catch 包裹,异常能够被捕获到)。
但注意await
必须在 async 函数中使用,否则会报语法错误。
看下面代码例子:
1)expression 后为非 promise
(async () => {
const b = await 111;
console.log(b); // 111
})();
复制代码
直接返回这个 expression 的值,即,打印 111
。
2)expression 为兑现的 promise
(async () => {
const b = await Promise.resolve(111);
console.log(b); // 111
})();
复制代码
返回兑现的 promise 的值,因此打印111
。
3)expression 为拒绝的 promise
(async () => {
try {
const b = await Promise.reject(111);
// 前面的 await 出错后,当前代码块后面的代码就不执行了
console.log(b); // 不执行
} catch (e) {
console.log("出错了:", e); // 出错了:111
}
})();
复制代码
若是await
后面的 promise 被拒绝或自己代码执行出错都会抛出一个异常,而后被 catch 到,而且,和当前await
同属一个代码块的后面的代码再也不执行。
在 promise 中咱们处理相互依赖的异步数据使用链式调用的方式,虽然相比回调函数已经优化不少,但书写及理解上仍是没有同步代码直观。咱们看下 async 函数如何解决这个问题。
先回顾下需求及 promise 的解决方案:
需求:请求 URL1 获得 data1;请求 URL2 获得 data2,但 URL2 = data1[0].url2;请求 URL3 获得 data3,但 URL3 = data2[0].url3。
使用 promise 链式调用能够这样写代码:
promiseAjax 在 第二部分介绍 promise 时在 3.1 中定义的,经过 promise 封装的 ajax GET 请求。
promiseAjax('URL1')
.then(data1 => promiseAjax(data1[0].url2))
.then(data2 => promiseAjax(data2[0].url3);)
.then(console.log(data3))
.catch(e => console.log(e));
复制代码
若是使用 Async 函数则能够像同步代码的同样写:
async function() {
try {
const data1 = await promiseAjax('URL1');
const data2 = await promiseAjax(data1[0].url);
const data3 = await promiseAjax(data2[0].url);
} catch (e) {
console.log(e);
}
}
复制代码
之因此能够这样用,是由于只有当前await
等待的 promise 兑现后,它后面的代码才会执行(或者抛出错误,后面代码都不执行,直接去到 catch 分支)。
这里有两点值得关注:
1)await
帮咱们处理了 promise,要么返回兑现的值,要么抛出异常; 2)await
在等待 promise 兑现的同时,整个 async 函数会挂起,promise 兑现后再从新执行接下来的代码。
对于第 2 点,是否是想到了生成器?在 1.4 节中咱们会经过生成器 + promise 本身写一个 async 函数。
Async 函数没有Promise.all()
之类的方法,咱们须要写多几个 async 函数。
能够借助Promise.all()
在同一个 async 函数中并行处理多个无依赖关系的异步数据,以下:
async function fn1() {
try {
const arr = await Promise.all([
promiseAjax("URL1"),
promiseAjax("URL2"),
]);
// ... do something
} catch (e) {
console.log(e);
}
}
复制代码
但实际开发中若是异步请求的数据是业务不相关的,不推荐这样写,缘由以下:
把全部的异步请求放在一个 async 函数中至关于手动增强了业务代码的耦合,会致使下面两个问题:
1)写代码及获取数据都不直观,尤为请求多起来的时候; 2)Promise.all
里面写多个无依赖的异步请求,若是 其中一个被拒绝或发生异常,全部请求的结果咱们都获取不到。
若是业务场景是不关心上面两点,能够考虑使用上面的写法,否则,每一个异步请求都放在不一样的 async 函数中发出。
下面是分开写的例子:
async function fn1() {
try {
const data1 = await promiseAjax("URL1");
// ... do something
} catch (e) {
console.log(e);
}
}
async function fn2() {
try {
const data2 = await promiseAjax("URL2");
// ... do something
} catch (e) {
console.log(e);
}
}
复制代码
咱们先看下 async 处理异步的原理:
await
操做符会挂起;await
后面的表达式求值(一般是个耗时的异步操做)前 async 函数一直处于挂起状态,避免阻塞 async 函数后面的代码;await
后面的表达式求值求值后(异步操做完成),await
能够对该值作处理:若是是非 promise,直接返回该值;若是是 promsie,则提取 promise 的值并返回。同时告诉 async 函数接着执行下面的代码;await
后面的那个异步操做,每每是返回 promise 对象(好比 axios),而后交给 await
处理,毕竟,async-await 的设计初衷就是为了解决异步请求数据时的回调地狱问题,而使用 promise 是关键一步。
async 函数自己的行为,和生成器相似;而await
等待的一般是 promise 对象,也正因如此,常说 async 函数是 生成器 + promise 结合后的语法糖。
既然咱们知道了 async 函数处理异步数据的原理,接下来咱们就简单模拟下 async 函数的实现过程。
这里只模拟 async 函数配合await
处理网络请求的场景,而且请求最终返回 promise 对象,async 函数自己返回值(已完成的 promise 对象)及更多使用场景这里没作考虑。
因此接下来的 myAsync 函数只是为了说明 async-await 原理,不要将其用在生产环境中。
/** * 模拟 async 函数的实现,该段代码取自 Secrets of the JavaScript Ninja (Second Edition),p159 */
// 接收生成器做为参数,建议先移到后面,看下生成器中的代码
var myAsync = generator => {
// 注意 iterator.next() 返回对象的 value 是 promiseAjax(),一个 promise
const iterator = generator();
// handle 函数控制 async 函数的 挂起-执行
const handle = iteratorResult => {
if (iteratorResult.done) return;
const iteratorValue = iteratorResult.value;
// 只考虑异步请求返回值是 promise 的状况
if (iteratorValue instanceof Promise) {
// 递归调用 handle,promise 兑现后再调用 iterator.next() 使生成器继续执行
// ps.原书then最后少了半个括号 ')'
iteratorValue
.then(result => handle(iterator.next(result)))
.catch(e => iterator.throw(e));
}
};
try {
handle(iterator.next());
} catch (e) {
console.log(e);
}
};
复制代码
myAsync
接收的一个生成器做为入参,生成器函数内部的代码,和写原生 async 函数相似,只是用yield
代替了await
myAsync(function*() {
try {
const a = yield Promise.resolve(1);
const b = yield Promise.resolve(a + 10);
const c = yield Promise.resolve(b + 100);
console.log(a, b, c); // 输出 1,11,111
} catch (e) {
console.log("出错了:", e);
}
});
复制代码
上面会打印1 11 111
。
若是第二个yield
语句后的 promise 被拒绝Promise.reject(a + 10)
,则打印出错了:11
。
handle
函数,控制生成器的 挂起-执行。具体过程以下:
1)首先调用generator()
生成它的控制器,即迭代器iterator
,此时,生成器处于挂起状态; 2)第一次调用handle
函数,并传入iterator.next()
,这样就完成生成器的第一次调用的; 3)执行生成器,遇到yield
生成器再次挂起,同时把yield
后表达式的结果(未完成的 promise)传给 handle; 4)生成器挂起的同时,异步请求还在进行,异步请求完成(promise 兑现)后,会调用handle
函数中的iteratorValue.then()
; 5)iteratorValue.then()
执行时内部递归调用handle
,同时把异步请求回的数据传给生成器(iterator.next(result)
),生成器更新数据再次执行。若是出错直接结束; 6)三、四、5 步重复执行,直到生成器结束,即iteratorResult.done === true
,myAsync 结束调用。
【1】[美]JOHN RESIG,BEAR BIBEAULT and JOSIP MARAS 著(2016),Secrets of the JavaScript Ninja (Second Edition),p159,Manning Publications Co.
【2】async function-MDN
【3】await-MDN
【4】理解 JavaScript 的 async/await