浅谈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!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,313评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,369评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,916评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,333评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,425评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,481评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,491评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,268评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,719评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,004评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,179评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,832评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,510评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,153评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,402评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,045评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,071评论 2 352

推荐阅读更多精彩内容