[译文] Javascript 异步探奇: Aysn/Await

2017-12-12 16:38:08 +08:00
 darluc

点击查看完整版

Async/Await 为何物?

Async/Await 即是异步函数( Async Functions ),是 JavaScript 中控制异步流程的特殊语法。目前,大多数主流浏览器已经支持了此语法。它的诞生灵感来源于 C# 和 F# 编程语言。目前 Aysnc/Await 已进入 JavaScript/EcmaScript 2017 标准。

简单来说,一个异步函数 async function 就是一个返回值为 Promise 对象的函数 function。在 async function 异步函数中才可以使用 await 关键字。Await 关键字应放在返回值为 Promise 对象的表达式之前,然后它会从这个 Promise 中取得解决值,虽然看上去像是一条同步执行的语句,但实际 Promise 的执行过程仍然是异步的。举个例子往往能解释得更清楚😁。

// 这是一个普通的函数,它会返回一个 promise。
// 该 promise 于 2 秒后解决为 "MESSAGE"。
function getMessage() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve("MESSAGE"), 2000);
  });
}

async function start() {
  const message = await getMessage();
  return `The message is: ${message}`;
}

start().then(msg => console.log(msg));
// "the message is: MESSAGE"

为何使用 Async/Await ?

Async/Await 为异步执行的代码提供了一种与同步代码相同的书写方式。它还为异步错误处理提供了一种简洁直接的处理方式,因为它利用了 try..catch 的语法结构,这与一般的同步代码处理错误的方式一致。

在进一步深入之前,我们必须强调一个前提:Async/Await 是绝对依赖于 JavaScript Promises 的,想要完全理解 Async/Await 就必须先了解它。

语法

Async 函数

创建一个异步函数 async function ,只需要在函数申明前加上 async 关键字即可,如下:

async function fetchWrapper() {
  return fetch('/api/url/');
}

const fetchWrapper = async () => fetch('/api/url/');

const obj = {
  async fetchWrapper() {
    // ...
  }
}

Await 关键字

async function updateBlogPost(postId, modifiedPost) {
  const oldPost = await getPost(postId);
  const updatedPost = { ...oldPost, ...modifiedPost };
  const savedPost = await savePost(updatedPost);
  return savedPost;
}

在这里 await 被用在返回 promise 的函数(现在也可称之为异步函数 aync functions)前。在函数的第一行 oldPost 被赋予了异步函数 getPost 返回的解决值( resolved value )。接下来,我们使用了对象展开操作符oldPostmodifiedPost 进行了一次浅层合并( shallow merge )。最后,我们保存修改过的文章,再一次使用 await 关键词接收 savePost 函数异步执行返回的 promise 对象。

例子 /常见问题

✋ “怎样进行错误处理的?”

好问题!在 async/await 的帮助下,我们可以使用与同步代码一样的语法,try … catch。下面的代码中,如果我们的异步调用 fetch 返回了某种错误,比如 404,它将会被 catch 代码捕获,之后我们就可以对这个错误进行处理了。

async function tryToFetch() {
  try {
    const response = await fetch('/api/data', options);
    return response.json();
  } catch(err) {
    console.log(`An error occured: ${err}`);
    // Instead of rethrowing the error
    // Let's return a regular object with no data
    return { data: [] };
  }
}

tryToFetch().then(data => console.log(data));

✋ “我还是不明白为什么 async/await 语法比 callbacks/promises 语法好。”

问得好!下面有个代码例子可展现出它们之间的区别。假设我们要异步地 fetchSomeData 获取某些数据,得到数据后再异步地 processSomeData 处理这些数据,如果有错误出现,只简单地返回一个对象。

// we have fetchSomeDataCB, and processSomeDataCB
// NOTE: CB stands for callback

function doWork(callback) {
  fetchSomeDataCB((err, fetchedData) => {
    if(err) {
      callback(null, [])
    }
    
    processSomeDataCB(fetchedData, (err2, processedData) => {
      if(err2) {
        callback(null, []);
      }
      
      // return the processedData outside of doWork
      callback(null, processedData);
    });
  });
}

doWork((err, processedData) => console.log(processedData));
// we have fetchSomeDataP, and processSomeDataP
// NOTE: P means that this function returns a Promise

function doWorkP() {
  return fetchSomeDataP()
    .then(fetchedData => processSomeDataP(fetchedData))
    .catch(err => []);
}

doWorkP().then(processedData => console.log(processedData));
// we have fetchSomeDataP, and processSomeDataP
// NOTE: P means that this function returns a Promise

async function doWork() {
  try {
    const fetchedData = await fetchSomeDataP();
    return processSomeDataP(fetchedData);
  } catch(err) {
    return [];
  }
}

doWork().then(processedData => console.log(processedData));
<center><small>Callback vs Promise vs Async/Await</small></center>

“如何处理并发”

如果我们想要异步过程被顺序地处理,只需要简单地使用多行 await 语句,并可将一个异步调用的输出结果传递给另一个异步调用,就像平时使用 promises 那样。不过为了理解并发,我们就必须使用 Promise.all 方法。如果想让 3 个异步动作同时执行(并发地),我们就要在 await 这些 promises 之前,让它们全部开始执行。

// Not ideal: This will happen sequentially
async function sequential() {
  const output1 = await task1();
  const output2 = await task2();
  const output3 = await task3();
  return combineEverything(output1, output2, output3);
}

上面的代码并不好使,因为 task2 会一直等到 task1 完成之后才开始执行,而 task3 则一直要等到 task1 和 task2 都完成之后才开始,其实这些任务之间并没有依赖关系。理想的做法是,我们让三个任务同时开始执行,进入 Promise.all 中等待它们完成。

// Ideal: This will happen concurrently
async function parallel() {
  const promises = [
    task1(),
    task2(),
    task3(),
  ];
  const [output1, output2, output3] = await Promise.all(promises);

  return combineEverything(output1, output2, output3);
}

在这段代码中,我们同时启动了三个异步任务,并将它们返回的 promises 存入一个数组。然后,将这组已执行的 promises 传入 Promise.all(),并 await 等待其返回输出。

其它说明

const timeoutP = async (s) => new Promise((resolve, reject) => {
  setTimeout(() => resolve(s*1000), s*1000)
});

[1, 2, 3].forEach(async function(time) {
  const ms = await timeoutP(time);
  console.log(`This took ${ms} milliseconds`);
});

console.log('wanna race?');

当这些 promises 进入等待状态 awaited-ed 之后,执行流程会回到主线程, forEach 语句外面的 console.log 就会被执行。

浏览器支持

目前浏览器的支持情况,你可以查看这个浏览器兼容表

Node 支持

node 7.6.0 向上都支持 Aysnc/Await 语法!

点击查看完整版

2588 次点击
所在节点    JavaScript
6 条回复
dablwow
2017-12-12 17:23:06 +08:00
呃标题没写对...async
duan602728596
2017-12-12 22:20:44 +08:00
aysn......
darluc
2017-12-13 14:53:11 +08:00
@dablwow @duan602728596 谢谢,找不到编辑按钮了
ThomasChan
2017-12-23 15:20:33 +08:00
哈哈 medium 这些作者是不是还互相通知这周要写关于啥的文章啊 我昨晚也刚翻了一篇

http://chenjunhao.cn/2017/12/22/6-%E4%B8%AA%E5%8E%9F%E5%9B%A0%E5%91%8A%E8%AF%89%E4%BD%A0-Async-Await-%E6%AF%94-Promise-%E5%A5%BD%E7%94%A8%EF%BC%88%E6%95%99%E7%A8%8B%EF%BC%89/

原文是在 Frontend Focus 周刊里看到的,也是发在 medium 上的
ThomasChan
2017-12-23 15:22:25 +08:00
好吧记错了是 Hacker Noon 上不是 medium
darluc
2017-12-25 02:13:32 +08:00

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/414133

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX