前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

ES6系列之async await的用法与实现原理

首页2018-12-21 16:50:43Front-End
JavaScriptES6async异步编程

async/await 大概是 ES2017 里最没有学习门槛的特性,看一眼就会用。但真正上手写业务,坑集中在两个地方:一个是错误处理,一个 try...catch 到底该包多大范围,包小了漏,包大了整个函数糊成一团;另一个是并发,三个互不相干的请求被顺手写成三行 await,页面白屏时间直接翻三倍。

这篇把 async 的语法规则捋一遍,重点讲清楚 await 遇到 reject 时的四种处理方式各自适用什么场景,以及什么时候该把 await 换成 Promise.all。最后拆开它的实现原理,看看引擎内部那个自动执行器长什么样。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • async 函数相对 Generator 改进的四个点
  • async 函数的五种书写形式
  • 返回值与状态变化的规则,什么时候 then 才会触发
  • await 后面跟非 Promise、跟 thenable 对象时的行为
  • 四种错误处理写法的取舍
  • 继发与并发的判断标准,以及 forEach 里 await 为什么不行
  • 实现原理:spawn 执行器逐行拆解

# 一、含义

async 函数是什么?一句话,它就是 Generator 函数的语法糖。

先看一个用 Generator 依次读取两个文件的例子:

// 有一个 Generator 函数,依次读取两个文件
const fs = require('fs');

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
@前端进阶之旅: 代码已经复制到剪贴板

上面代码的函数 gen 可以写成 async 函数,就是下面这样:

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
@前端进阶之旅: 代码已经复制到剪贴板

async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。

两段代码放在一起看,形状几乎没变。但可用性上差了一大截,具体体现在四点。

第一,内置执行器。 Generator 函数的执行必须靠执行器,所以才有了 co 模块,而 async 函数自带执行器。也就是说,async 函数的执行与普通函数一模一样,只要一行:

asyncReadFile();
@前端进阶之旅: 代码已经复制到剪贴板

上面的代码调用了 asyncReadFile 函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要手动调用 next 方法,或者引入 co 模块,才能真正跑到底。Generator 那套手动推进的写法和自动执行器的实现,在 ES6系列之 Generator 函数的暂停与自动执行 里有完整的推导过程。

第二,更好的语义。 async 和 await 比起星号和 yield,语义清楚太多。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。yield 是「产出」,这个词跟异步没有半点关系,它只是被借用了而已。

第三,更广的适用性。 co 模块约定 yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以是 Promise 对象,也可以是原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。

第四,返回值是 Promise。 async 函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用 then 方法指定下一步的操作,也可以在另一个 async 函数里继续 await 它。

进一步说,async 函数完全可以看作多个异步操作包装成的一个 Promise 对象,而 await 命令就是内部 then 命令的语法糖。

# 二、基本用法

async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

async 函数执行流程,遇到 await 让出主线程,异步完成后恢复执行

「先返回」这三个字很关键。async 函数不会阻塞调用它的代码,它在第一个 await 处就把控制权还给了调用方,剩下的部分排在微任务里。所以下面这个函数虽然内部要等两次网络请求,调用方拿到 Promise 是同步的、立刻的。

// 调用该函数时,会立即返回一个 Promise 对象
async function getStockPriceByName(name) {
  const symbol = await getStockSymbol(name);
  const stockPrice = await getStockPrice(symbol);
  return stockPrice;
}

getStockPriceByName('goog').then(function (result) {
  console.log(result);
});
@前端进阶之旅: 代码已经复制到剪贴板

async 函数有多种使用形式,声明、表达式、对象方法、类方法、箭头函数都支持:

// 函数声明
async function foo() {}

// 函数表达式
const foo = async function () {};

// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jake').then(…);

// 箭头函数
const foo = async () => {};
@前端进阶之旅: 代码已经复制到剪贴板

这里有个坑要注意,构造函数 constructor 不能是 async 的,因为 new 必须同步返回实例。上面那个 Storage 类的写法是标准解法:构造函数里存一个 Promise,真正需要的时候再 await 它。

# 三、语法

async 函数的语法规则总体上比较简单,难点是错误处理机制。

# 3.1 返回 Promise 对象

async 函数返回一个 Promise 对象,函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。

// 函数 f 内部 return 命令返回的值,会被 then 方法回调函数接收到
async function f() {
  return 'hello world';
}

f().then(v => console.log(v))
// "hello world"
@前端进阶之旅: 代码已经复制到剪贴板

async 函数内部抛出错误,会导致返回的 Promise 对象变为 reject 状态,抛出的错误对象会被 catch 方法回调函数接收到。

async function f() {
  throw new Error('出错了');
}

f().then(
  v => console.log(v),
  e => console.log(e)
)
// Error: 出错了
@前端进阶之旅: 代码已经复制到剪贴板

这条规则有个很实用的推论:async 函数把同步抛错和异步 reject 统一成了同一种东西。普通函数里 throw 出来的异常要用 try...catch 接,Promise 的 reject 要用 .catch() 接,两套机制。加上 async 之后,两者都变成了返回 Promise 的 reject,外面一个 catch 全接住。

# 3.2 Promise 对象的状态变化

async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非中途遇到 return 语句或者抛出错误。也就是说,只有 async 函数内部的异步操作执行完,才会执行 then 方法指定的回调函数。

async function getTitle(url) {
  let response = await fetch(url);
  let html = await response.text();
  return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
@前端进阶之旅: 代码已经复制到剪贴板

# 3.3 await 命令

正常情况下,await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

async function f() {
  // 等同于
  // return 123;
  return await 123;
}

f().then(v => console.log(v))
// 123
@前端进阶之旅: 代码已经复制到剪贴板

这两种写法结果一样,但过程不完全一样。await 123 会先把 123 包成一个已完成的 Promise,然后让出一次微任务队列,所以它比直接 return 123 多排一轮微任务。业务代码里感知不到,做那种「输出顺序」的面试题时就要算清楚了。

另一种情况是,await 命令后面是一个 thenable 对象(即定义了 then 方法的对象),那么 await 会将其等同于 Promise 对象。这一条让 await 可以直接消费 jQuery 的 deferred、axios 的返回值这类「长得像 Promise」的东西,不用手动转换。

await 命令后面的 Promise 对象如果变为 reject 状态,则 reject 的参数会被 catch 方法的回调函数接收到。

async function f() {
  await Promise.reject('出错了');
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出错了
@前端进阶之旅: 代码已经复制到剪贴板

上面代码中 await 语句前面没有 return,但是 reject 的参数依然传入了 catch 方法的回调函数。这里如果在 await 前面加上 return,效果是一样的。

fe
  • 一、含义
  • 二、基本用法
  • 三、语法
    • 3.1 返回 Promise 对象
    • 3.2 Promise 对象的状态变化
    • 3.3 await 命令
    • 3.4 错误处理
    • 3.5 三个使用注意点
  • 四、async 函数的实现原理
  • 总结
  • 参考

← JavaScript深浅拷贝原理与手写实现ES6系列之Generator函数的暂停与自动执行 →