在异步处理方案中,目前最为简洁优雅的即是async
函数(如下简称A函数)。通过必要的分块包装后,A函数能使多个相关的异步操做如同同步操做同样聚合起来,使其相互间的关系更为清晰、过程更为简洁、调试更为方便。它本质是Generator
函数的语法糖,通俗的说法是使用G函数进行异步处理的加强版。segmentfault
学习A函数必须有Promise
基础,最好还了解Generator
函数,有须要的可查看延伸小节。数组
为了直观的感觉A函数的魅力,下面使用Promise
和A函数进行了相同的异步操做。该异步的目的是获取用户的留言列表,须要分页,分页由后台控制。具体的操做是:先获取到留言的总条数,再更正当前须要显示的页数(每次切换到不一样页时,总数目可能会发生变化),最后传递参数并获取到相应的数据。并发
let totalNum = 0; // Total comments number. let curPage = 1; // Current page index. let pageSize = 10; // The number of comment displayed in one page. // 使用A函数的主代码。 async function dealWithAsync() { totalNum = await getListCount(); console.log('Get count', totalNum); if (pageSize * (curPage - 1) > totalNum) { curPage = 1; } return getListData(); } // 使用Promise的主代码。 function dealWithPromise() { return new Promise((resolve, reject) => { getListCount().then(res => { totalNum = res; console.log('Get count', res); if (pageSize * (curPage - 1) > totalNum) { curPage = 1; } return getListData() }).then(resolve).catch(reject); }); } // 开始执行dealWithAsync函数。 // dealWithAsync().then(res => { // console.log('Get Data', res) // }).catch(err => { // console.log(err); // }); // 开始执行dealWithPromise函数。 // dealWithPromise().then(res => { // console.log('Get Data', res) // }).catch(err => { // console.log(err); // }); function getListCount() { return createPromise(100).catch(() => { throw 'Get list count error'; }); } function getListData() { return createPromise([], { curPage: curPage, pageSize: pageSize, }).catch(() => { throw 'Get list data error'; }); } function createPromise( data, // Reback data params = null, // Request params isSucceed = true, timeout = 1000, ) { return new Promise((resolve, reject) => { setTimeout(() => { isSucceed ? resolve(data) : reject(data); }, timeout); }); }
对比dealWithAsync
和dealWithPromise
两个简单的函数,能直观的发现:使用A函数,除了有await
关键字外,与同步代码无异。而使用Promise
则须要根据规则增长不少包裹性的链式操做,产生了太多回调函数,不够简约。另外,这里分开了每一个异步操做,并规定好各自成功或失败时传递出来的数据,近乎实际开发。异步
A函数也是函数,因此具备普通函数该有的性质。不过形式上有两点不一样:一是定义A函数时,function
关键字前须要有async
关键字(意为异步),表示这是个A函数。二是在A函数内部可使用await
关键字(意为等待),表示会将其后面跟随的结果当成异步操做并等待其完成。async
如下是它的几种定义方式。函数
// 声明式 async function A() {} // 表达式 let A = async function () {}; // 做为对象属性 let o = { A: async function () {} }; // 做为对象属性的简写式 let o = { async A() {} }; // 箭头函数 let o = { A: async () => {} };
执行A函数,会固定的返回一个Promise
对象。学习
获得该对象后即可监设置成功或失败时的回调函数进行监听。若是函数执行顺利并结束,返回的P对象的状态会从等待转变成成功,并输出return
命令的返回结果(没有则为undefined
)。若是函数执行途中失败,JS会认为A函数已经完成执行,返回的P对象的状态会从等待转变成失败,并输出错误信息。spa
// 成功执行案例 A1().then(res => { console.log('执行成功', res); // 10 }); async function A1() { let n = 1 * 10; return n; } // 失败执行案例 A2().catch(err => { console.log('执行失败', err); // i is not defined. }); async function A2() { let n = 1 * i; return n; }
只有在A函数内部才可使用await
命令,存在于A函数内部的普通函数也不行。调试
引擎会统一将await
后面的跟随值视为一个Promise
,对于不是Promise
对象的值会调用Promise.resolve()
进行转化。即使此值为一个Error
实例,通过转化后,引擎依然视其为一个成功的Promise
,其数据为Error
的实例。code
当函数执行到await
命令时,会暂停执行并等待其后的Promise
结束。若是该P对象最终成功,则会返回成功的返回值,至关将await xxx
替换成返回值
。若是该P对象最终失败,且错误没有被捕获,引擎会直接中止执行A函数并将其返回对象的状态更改成失败,输出错误信息。
最后,A函数中的return x
表达式,至关于return await x
的简写。
// 成功执行案例 A1().then(res => { console.log('执行成功', res); // 约两秒后输出100。 }); async function A1() { let n1 = await 10; let n2 = await new Promise(resolve => { setTimeout(() => { resolve(10); }, 2000); }); return n1 * n2; } // 失败执行案例 A2().catch(err => { console.log('执行失败', err); // 约两秒后输出10。 }); async function A2() { let n1 = await 10; let n2 = await new Promise((resolve, reject) => { setTimeout(() => { reject(10); }, 2000); }); return n1 * n2; }
对于存在于JS语句(for
, while
等)的await
命令,引擎遇到时也会暂停执行。这意味着能够直接使用循环语句处理多个异步。
如下是处理继发的两个例子。A函数处理相继发生的异步尤其简洁,总体上与同步代码无异。
// 两个方法A1和A2的行为结果相同,都是每隔一秒输出10,输出三次。 async function A1() { let n1 = await createPromise(); console.log('N1', n1); let n2 = await createPromise(); console.log('N2', n2); let n3 = await createPromise(); console.log('N3', n3); } async function A2() { for (let i = 0; i< 3; i++) { let n = await createPromise(); console.log('N' + (i + 1), n); } } function createPromise() { return new Promise(resolve => { setTimeout(() => { resolve(10); }, 1000); }); }
接下来是处理并发的三个例子。A1函数使用了Promise.all
生成一个聚合异步,虽然简单但灵活性下降了,只有都成功和失败两种状况。A3函数相对A2仅仅为了说明应该怎样配合数组的遍历方法使用async
函数。重点在A2函数的理解上。
A2函数使用了循环语句,实际是继发的获取到各个异步值,但在整体的时间上至关并发(这里须要好好理解一番)。由于一开始建立reqs
数组时,就已经开始执行了各个异步,以后虽然是逐一继发获取,但总花费时间与遍历顺序无关,恒等于耗时最多的异步所花费的时间(不考虑遍历、执行等其它的时间消耗)。
// 三个方法A1, A2和A3的行为结果相同,都是在约一秒后输出[10, 10, 10]。 async function A1() { let res = await Promise.all([createPromise(), createPromise(), createPromise()]); console.log('Data', res); } async function A2() { let res = []; let reqs = [createPromise(), createPromise(), createPromise()]; for (let i = 0; i< reqs.length; i++) { res[i] = await reqs[i]; } console.log('Data', res); } async function A3() { let res = []; let reqs = [9, 9, 9].map(async (item) => { let n = await createPromise(item); return n + 1; }); for (let i = 0; i< reqs.length; i++) { res[i] = await reqs[i]; } console.log('Data', res); } function createPromise(n = 10) { return new Promise(resolve => { setTimeout(() => { resolve(n); }, 1000); }); }
一旦await
后面的Promise
转变成rejected
,整个async
函数便会终止。然而不少时候咱们不但愿由于某个异步操做的失败,就终止整个函数,所以须要进行合理错误处理。注意,这里所说的错误不包括引擎解析或执行的错误,仅仅是状态变为rejected
的Promise
对象。
处理的方式有两种:一是先行包装Promise
对象,使其始终返回一个成功的Promise
。二是使用try.catch
捕获错误。
// A1和A2都执行成,且返回值为10。 A1().then(console.log); A2().then(console.log); async function A1() { let n; n = await createPromise(true); return n; } async function A2() { let n; try { n = await createPromise(false); } catch (e) { n = e; } return n; } function createPromise(needCatch) { let p = new Promise((resolve, reject) => { reject(10); }); return needCatch ? p.catch(err => err) : p; }
前言中已经说起,A函数是使用G函数进行异步处理的加强版。既然如此,咱们就从其改进的方面入手,来看看其基于G函数的实现原理。A函数相对G函数的改进体如今这几个方面:更好的语义,内置执行器和返回值是Promise
。
更好的语义。G函数经过在function
后使用*
来标识此为G函数,而A函数则是在function
前加上async
关键字。在G函数中可使用yield
命令暂停执行和交出执行权,而A函数是使用await
来等待异步返回结果。很明显,async
和await
更为语义化。
// G函数 function* request() { let n = yield createPromise(); } // A函数 async function request() { let n = await createPromise(); } function createPromise() { return new Promise(resolve => { setTimeout(() => { resolve(10); }, 1000); }); }
内置执行器。调用A函数便会一步步自动执行和等待异步操做,直到结束。若是须要使用G函数来自动执行异步操做,须要为其建立一个自执行器。经过自执行器来自动化G函数的执行,其行为与A函数基本相同。能够说,A函数相对G函数最大改进即是内置了自执行器。
// 二者都是每隔一秒钟打印出10,重复两次。 // A函数 A(); async function A() { let n1 = await createPromise(); console.log(n1); let n2 = await createPromise(); console.log(n2); } // G函数,使用自执行器执行。 spawn(G); function* G() { let n1 = yield createPromise(); console.log(n1); let n2 = yield createPromise(); console.log(n2); } function spawn(genF) { 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); }); }); } function createPromise() { return new Promise(resolve => { setTimeout(() => { resolve(10); }, 1000); }); }
在了解A函数内部与包含它外部间的执行顺序前,须要明白两点:一为Promise
的实例方法是推迟到本轮事件末尾才执行的后执行操做,详情请查看连接。二为Generator
函数是经过调用实例方法来切换执行权进而控制程序执行顺序,详情请查看连接。理解好A函数的执行顺序,能更加清楚的把握此三者的存在。
先看如下代码,对比A一、A2和A3方法的结果。
F(A1); // 接连打印出:1 3 4 2 5。 F(A2); // 接连打印出:1 3 2 4 5。 F(A3); // 先打印出:1 3 2,隔两秒后打印出:4 9。 function F(A) { console.log(1); A().then(console.log); console.log(2); } async function A1() { console.log(3); console.log(4); return 5; } async function A2() { console.log(3); let n = await 5; console.log(4); return n; } async function A3() { console.log(3); let n = await createPromise(); console.log(4); return n; } function createPromise() { return new Promise(resolve => { setTimeout(() => { resolve(9); }, 2000); }); }
从结果上可概括出一些表面形态。执行A函数,会即刻执行其函数体,直到遇到await
命令。遇到await
命令后,执行权会转向A函数外部,即无论A函数内部执行而开始执行外部代码。执行完外部代码(本轮事件)后,才继续执行以前await
命令后面的代码。
概括到此已成功一半,以后着手分析其成因。若是客官您对本楼有所了解,那必定不会忘记‘自执行器’这位大婶吧?估计是忘记了。A函数的本质就是带有自执行器的G函数,因此探究A函数的执行原理就是探究使用自执行器的G函数的执行原理。想起了?
再看下面代码,使用相同逻辑的G函数会获得与A函数相同的结果。
F(A); // 先打印出:1 3 2,隔两秒后打印出:4 9。 F(() => { return spawn(G); }); // 先打印出:1 3 2,隔两秒后打印出:4 9。 function F(A) { console.log(1); A().then(console.log); console.log(2); } async function A() { console.log(3); let n = await createPromise(); console.log(4); return n; } function* G() { console.log(3); let n = yield createPromise(); console.log(4); return n; } function createPromise() { return new Promise(resolve => { setTimeout(() => { resolve(9); }, 2000); }); } function spawn(genF) { 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); }); }); }
自动执行G函数时,遇到yield
命令后会使用Promise.resolve
包裹其后的表达式,并为其设置回调函数。不管该Promise
是马上有告终果仍是过某段时间以后,其回调函数都会被推迟到在本轮事件末尾执行。以后再是下一步,再下一步。一样的道理适用于A函数,当遇到await
命令时(此处略去三五字),因此有了如此这般的执行顺序。谢幕。