浅谈Generator函数的异步应用之async函数
1.异步编程的终极处理方案
前文结尾时提到,async/await是异步编程的’终极’处理方案,而终极二字就表现在,使用async/await来操作异步无论是逻辑上还是语义上都与同步操作无限接近(当然只是形式上像,没有改变异步的本质,后面会解释)。
先来看一下之前使用Generator函数控制异步流程的代码
function* gen() { const res1 = yield promisify_readFile("./text1.txt"); console.log(res1.toString()); const res2 = yield promisify_readFile("./text2.txt"); console.log(res2.toString());}co(gen);
下面使用async/await实现
async function asyncReadFile() { const res1 = await promisify_readFile("./text1.txt"); console.log(res1.toString()); const res2 = await promisify_readFile("./text2.txt"); console.log(res2.toString());}asyncReadFile()
可以看到,从形式上看使用async/await进行异步流程解决无需执行器,函数可以像普通函数一样执行,这意味着async函数内置了Generator函数的执行器。从语义上看,async关键字表示函数内部有异步操作,await关键字表示要等待异步操作执行完毕,相比于Generator函数用*公告以及yield表达式划分状态要更加友好。
下面具体详情async函数和await关键字的特点。
2.async函数和await关键字的特点
2.1 async函数返回值
async函数返回的是Promise对象,因而可以为async函数指定then,catch等方法。
asyncReadFile().then(() => { console.log("end");});
既然async函数返回的是Promise对象,那其结果和状态由什么决定呢
- 当async函数内部的return有返回值时,该参数会成为then方法成功回调的参数(即Promise的结果值),状态变为成功。
const promisifyTimeOut = () => { return new Promise((resolve) => { setTimeout(() => { resolve('timeOut') }, 500); })}const asyncTimeOut = async () => { const res = await promisifyTimeOut() return res};asyncTimeOut().then( (res) => { console.log('success' + res); }, (r) => { console.log('err' + r); });//success timeOut
- 当async函数内部抛出错误时,状态会立即变为失败,并执行then方法的失败回调或者catch方法。
const asyncTimeOut = async () => { const res = await promisifyTimeOut() throw res};asyncTimeOut().then( (res) => { console.log('success' + res); }, (r) => { console.log('err' + r); });// err timeOut
利用这一点可以,我们可以进行对async函数的错误解决,后面会详情。
2.2 await关键字的特点
- await命令只能用在async函数之中,用在普通函数中会报错。
- await命令后面假如是一个 Promise 对象,返回该Promise 对象的结果值,假如不是 Promise 对象,就直接返回对应的值 。
(async function(){ const res1 = await Promise.resolve('foo') console.log(res1) const res2 = await 'bar' console.log(res2)})()// foo// bar
3.async函数的错误解决
前面提到,async函数内部抛出错误时,其状态会立即变为失败并执行失败回调(假设指定了失败回调)。因而任何一个await关键字后面的Promise状态变为rejected都会导致async函数立即中断执行。
const promisifyTimeOut = () => { return new Promise((resolve, reject) => { setTimeout(() => { reject("some err"); }, 500); });};const asyncTimeOut = async () => { await promisifyTimeOut(); console.log("foo");};asyncTimeOut().catch((r) => console.log(r));// some err
上面代码,await后的异步抛出错误,async函数中断执行导致foo没有被打印。假如不想让async函数内部一抛出错误就终止执行,可以将可能抛出错误的Promise包在try…catch代码块中 ,或者者为可能抛出错误的Promise指定失败回调(指定then方法或者catch方法),下面以try…catch为例演示。
const asyncTimeOut = async () => { try { await promisifyTimeOut() } catch (error) { console.log(error) } console.log("foo");};asyncTimeOut().catch((r) => console.log(r))// some err// foo
假如使用上述两种方法进行错误解决,则async函数指定的失败回调将不生效(假设不在catch语句或者Promise失败回调中将错误抛出)。另外,多个await语句可以一起包在try…catch中进行统一错误解决。
4.async函数的实现原理
其实,经过前面对co模块的探讨,以及上面对async函数特点的详情,我们可以知道,async/await就是Generator函数的语法糖,我们只要根据其特点进行封装,具体如下。
- async函数内置Generator函数执行器。
- async函数返回Promise,要等内部所有Promise执行完后再改变状态,函数内部抛出错误,状态立即变为rejected。
我们假设async的内置执行器叫做spawn函数,那么async函数的结构就是这样的
const async = (gen) => { return () => { return spawn(gen); };};
接下来实现执行器,其原理与前面探讨的co模块基本一致
function spawn(genF) { return new Promise(function (resolve, reject) { const gen = genF(); function step(data) { let res; try { res = gen.next(data); } catch (e) { // 内部抛出错误 状态变为rejet return reject(e); } if (res.done) { return resolve(res.value); } // 为异步指定成功/失败回调 成功则继续执行 失败则立即rejected Promise.resolve(res.value).then(step, (r) => reject(r)); } step(); });}
下面简单测试一下
const promisify = (data) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(data); }, 300); });};function* testGen() { const res1 = yield promisify(1); console.log(res1); const res2 = yield promisify(2); console.log(res2); return res2;}const async = (gen) => { return () => { return spawn(gen); };};// 得到async函数const asyncFoo = async(testGen);// 得到async函数的执行结果const res = asyncFoo();setTimeout(() => { console.log(res);}, 1000);// 1// 2// Promise { 2 }
5.async函数与执行环境栈
在前面对JavaScript执行上下文的探讨时我们知道,JavaScript引擎在执行代码之前, 会创立一个执行环境栈,之后创立全局执行上下文并将它压入栈中作为栈底。每遇到一个函数执行时,都会为该函数创立执行上下文,并将其推入执行环境栈中,形成一个由执行上下文构成的堆栈(context stack)。每个上下文都有一个与之相关联的变量对象,包含了当前上下文的变量,函数,形参等。栈是“后进先出”的数据结构,因而最后产生的上下文环境首先执行完成并出栈,而后再执行它下层的上下文,栈底永远是全局上下文,当浏览器窗口关闭,全局上下文才会出栈。
Generator函数不是这样,执行Generator函数产生的上下文,遇到yield命令时,会暂时退出堆栈,但是并不消失,变量对象里面的所有变量和对象会冻结在当前状态。等到执行next命令时,执行上下文会重新加入执行环境栈,冻结的变量和对象恢复执行。而async函数是Generator函数的语法糖,因而他也有一样的特性,即async 函数可以保留运行堆栈。
下面用一个例子进行比照说明
const timeOut = () => { return new Promise((resolve) => { setTimeout(() => { resolve(); }, 500); });};(function () { for (let i = 0; i < 3; i++) { timeOut().then(() => { console.log(i); }); } console.log("end");})();// end// 0 1 2
上面代码会先打印end 之后012几乎同时打印,起因不难分析,因为promise.then方法不会将当前上下文冻结,因而循环的进行不受影响,而由于then方法中的回调会异步执行,因而三个log语句会几乎同时被加入任务队列,最终造成上述的执行结果。
下面用async/await重写上面代码
(async function () { for (let i = 0; i < 3; i++) { await timeOut(); console.log(i); } console.log("end");})();// 0 1 2 end
上面代码会依次打印0 1 2 end。
分析起因,因为async 函数可以保留当前上下文环境,当遇到await命令,当前上下文的所有状态都被冻结,包括for循环在内的所有代码都会暂停执行,因而造成上述执行结果。
其实,这条特性可以了解为, await命令后面的所有代码都会进入异步任务队列。 await相当于then的语法糖,其后面的代码都进入了promise.then的回调函数中,会进入任务队列异步执行。
利用这一点,我们可以实现休眠器。
function sleep(interval) { return new Promise((resolve) => { setTimeout(resolve, interval); });}// 用法async function Async(timeOut) { await sleep(timeOut); console.log("foo!");}Async(1000);// 一秒后打印foo!
关于上述特性,有两点需要说明
1.await语句冻结的只是async函数的上下文,即async函数后面的代码执行不会被阻塞。这也就说明,async/await只是写起来像同步代码,异步的本质没有改变。
async function Async(timeOut) { await sleep(timeOut); console.log("foo!");}Async(0)console.log('end!')// end!// foo!
2. 上面说到,遇到await关键字,其后所有代码都将被冻结,因而await语句下面的异步任务也会等到await语句的异步结束后再执行。这点对于具备依赖关系的异步(继发关系)的解决是非常友好的。但同样的,假如两个异步没有继发关系,则尽量不要这么写,由于会造成阻塞。可以使用Promise.all()等方式让他们并发执行,而不是继发执行。
function sleep(interval) { return new Promise((resolve) => { setTimeout(() => { console.log("foo!"); resolve(); }, interval); });}async function Async() { await Promise.all([sleep(500), sleep(500)]); console.log("end");}Async();// 两个异步并发执行 几乎同时打印foo!
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 浅谈Generator函数的异步应用之async函数