是把双刃剑,ES6中的异步

精读《async/await 是把双刃剑》

2018/05/12 · JavaScript · 1 评论 · async, await

原文出处: 黄子毅   

本周精读内容是 《逃离 async/await 地狱》。

Async/Await替代Promise的6个理由

2017/04/02 · JavaScript · async, await

原文出处: Mostafa Gaafar   译文出处:Fundebug   

译者按: Node.js的异步编程方式有效提高了应用性能;然而回调地狱却让人望而生畏,Promise让我们告别回调函数,写出更优雅的异步代码;在实践过程中,却发现Promise并不完美;技术进步是无止境的,这时,我们有了Async/Await。

Node.js 7.6已经支持async/await了,如果你还没有试过,这篇博客将告诉你为什么要用它。

图片 1

1 引言

终于,async/await 也被吐槽了。Aditya Agarwal 认为 async/await 语法让我们陷入了新的麻烦之中。

其实,笔者也早就觉得哪儿不对劲了,终于有个人把实话说了出来,async/await 可能会带来麻烦。

Async/Await简介

对于从未听说过async/await的朋友,下面是简介:

  • async/await是写异步代码的新方式,以前的方法有回调函数Promise
  • async/await是基于Promise实现的,它不能用于普通的回调函数。
  • async/await与Promise一样,是非阻塞的。
  • async/await使得异步代码看起来像同步代码,这正是它的魔力所在。

async.jpg

2 概述

下面是随处可见的现代化前端代码:

(async () => { const pizzaData = await getPizzaData(); // async call const drinkData = await getDrinkData(); // async call const chosenPizza = choosePizza(); // sync call const chosenDrink = chooseDrink(); // sync call await addPizzaToCart(chosenPizza); // async call await addDrinkToCart(chosenDrink); // async call orderItems(); // async call })();

1
2
3
4
5
6
7
8
9
(async () => {
  const pizzaData = await getPizzaData(); // async call
  const drinkData = await getDrinkData(); // async call
  const chosenPizza = choosePizza(); // sync call
  const chosenDrink = chooseDrink(); // sync call
  await addPizzaToCart(chosenPizza); // async call
  await addDrinkToCart(chosenDrink); // async call
  orderItems(); // async call
})();

await 语法本身没有问题,有时候可能是使用者用错了。当 pizzaData 与 drinkData 之间没有依赖时,顺序的 await 会最多让执行时间增加一倍的 getPizzaData 函数时间,因为 getPizzaData 与 getDrinkData 应该并行执行。

回到我们吐槽的回调地狱,虽然代码比较丑,带起码两行回调代码并不会带来阻塞。

看来语法的简化,带来了性能问题,而且直接影响到用户体验,是不是值得我们反思一下?

正确的做法应该是先同时执行函数,再 await 返回值,这样可以并行执行异步函数:

(async () => { const pizzaPromise = selectPizza(); const drinkPromise = selectDrink(); await pizzaPromise; await drinkPromise; orderItems(); // async call })();

1
2
3
4
5
6
7
(async () => {
  const pizzaPromise = selectPizza();
  const drinkPromise = selectDrink();
  await pizzaPromise;
  await drinkPromise;
  orderItems(); // async call
})();

或者使用 Promise.all 可以让代码更可读:

(async () => { Promise.all([selectPizza(), selectDrink()]).then(orderItems); // async call })();

1
2
3
(async () => {
  Promise.all([selectPizza(), selectDrink()]).then(orderItems); // async call
})();

看来不要随意的 await,它很可能让你代码性能降低。

Async/Await语法

示例中,getJSON函数返回一个promise,这个promise成功resolve时会返回一个json对象。我们只是调用这个函数,打印返回的JSON对象,然后返回”done”。

使用Promise是这样的:

JavaScript

const makeRequest = () => getJSON() .then(data => { console.log(data) return "done" }) makeRequest()

1
2
3
4
5
6
7
const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })
makeRequest()

使用Async/Await是这样的:

JavaScript

const makeRequest = async () => { console.log(await getJSON()) return "done" } makeRequest()

1
2
3
4
5
const makeRequest = async () => {
  console.log(await getJSON())
  return "done"
}
makeRequest()

它们有一些细微不同:

  • 函数前面多了一个aync关键字。await关键字只能用在aync定义的函数内。async函数会隐式地返回一个promise,该promise的reosolve值就是函数return的值。(示例中reosolve值就是字符串”done”)
  • 第1点暗示我们不能在最外层代码中使用await,因为不在async函数内。

JavaScript

// 不能在最外层代码中使用await await makeRequest() // 这是会出事情的 makeRequest().then((result) => { // 代码 })

1
2
3
4
5
6
// 不能在最外层代码中使用await
await makeRequest()
// 这是会出事情的
makeRequest().then((result) => {
  // 代码
})

await getJSON()表示console.log会等到getJSON的promise成功reosolve之后再执行。

上个寒假又又又···不知道第几遍重温ES6,以下对几个异步实现写个总结。

3 精读

仔细思考为什么 async/await 会被滥用,笔者认为是它的功能比较反直觉导致的。

首先 async/await 真的是语法糖,功能也仅是让代码写的舒服一些。先不看它的语法或者特性,仅从语法糖三个字,就能看出它一定是局限了某些能力。

举个例子,我们利用 html 标签封装了一个组件,带来了便利性的同时,其功能一定是 html 的子集。又比如,某个轮子哥觉得某个组件 api 太复杂,于是基于它封装了一个语法糖,我们多半可以认为这个便捷性是牺牲了部分功能换来的。

功能完整度与使用便利度一直是相互博弈的,很多框架思想的不同开源版本,几乎都是把功能完整度与便利度按照不同比例混合的结果。

那么回到 async/await 它的解决的问题是回调地狱带来的灾难:

a(() => { b(() => { c(); }); });

1
2
3
4
5
a(() => {
  b(() => {
    c();
  });
});

为了减少嵌套结构太多对大脑造成的冲击,async/await 决定这么写:

await a(); await b(); await c();

1
2
3
await a();
await b();
await c();

虽然层级上一致了,但逻辑上还是嵌套关系,这不是另一个程度上增加了大脑负担吗?而且这个转换还是隐形的,所以许多时候,我们倾向于忽略它,所以造成了语法糖的滥用。

为什么Async/Await更好?

在之前,我们最常见的异步实现莫过于回调函数,但是如果嵌套太深,代码就会变得难以阅读与维护。而在ES6中,我们有了更多更加优美的异步实现方式,正因如此,我们才得以走出回调地狱。

理解语法糖

虽然要正确理解 async/await 的真实效果比较反人类,但为了清爽的代码结构,以及防止写出低性能的代码,还是挺有必要认真理解 async/await 带来的改变。

首先 async/await 只能实现一部分回调支持的功能,也就是仅能方便应对层层嵌套的场景。其他场景,就要动一些脑子了。

比如两对回调:

a(() => { b(); }); c(() => { d(); });

1
2
3
4
5
6
7
a(() => {
  b();
});
 
c(() => {
  d();
});

如果写成下面的方式,虽然一定能保证功能一致,但变成了最低效的执行方式:

await a(); await b(); await c(); await d();

1
2
3
4
await a();
await b();
await c();
await d();

因为翻译成回调,就变成了:

a(() => { b(() => { c(() => { d(); }); }); });

1
2
3
4
5
6
7
a(() => {
  b(() => {
    c(() => {
      d();
    });
  });
});

然而我们发现,原始代码中,函数 c 可以与 a 同时执行,但 async/await 语法会让我们倾向于在 b 执行完后,再执行 c

所以当我们意识到这一点,可以优化一下性能:

const resA = a(); const resC = c(); await resA; b(); await resC; d();

1
2
3
4
5
6
7
const resA = a();
const resC = c();
 
await resA;
b();
await resC;
d();

但其实这个逻辑也无法达到回调的效果,虽然 a 与 c 同时执行了,但 d 原本只要等待 c 执行完,现在如果 a 执行时间比 c 长,就变成了:

a(() => { d(); });

1
2
3
a(() => {
  d();
});

看来只有完全隔离成两个函数:

(async () => { await a(); b(); })(); (async () => { await c(); d(); })();

1
2
3
4
5
6
7
8
9
(async () => {
  await a();
  b();
})();
 
(async () => {
  await c();
  d();
})();

或者利用 Promise.all:

async function ab() { await a(); b(); } async function cd() { await c(); d(); } Promise.all([ab(), cd()]);

1
2
3
4
5
6
7
8
9
10
11
async function ab() {
  await a();
  b();
}
 
async function cd() {
  await c();
  d();
}
 
Promise.all([ab(), cd()]);

这就是我想表达的可怕之处。回调方式这么简单的过程式代码,换成 async/await 居然写完还要反思一下,再反推着去优化性能,这简直比回调地狱还要可怕。

而且大部分场景代码是非常复杂的,同步与 await 混杂在一起,想捋清楚其中的脉络,并正确优化性能往往是很困难的。但是我们为什么要自己挖坑再填坑呢?很多时候还会导致忘了填。

原文作者给出了 Promise.all 的方式简化逻辑,但笔者认为,不要一昧追求 async/await 语法,在必要情况下适当使用回调,是可以增加代码可读性的。

1. 简洁

由示例可知,使用Async/Await明显节约了不少代码。我们不需要写.then,不需要写匿名函数处理Promise的resolve值,也不需要定义多余的data变量,还避免了嵌套代码。这些小的优点会迅速累计起来,这在之后的代码示例中会更加明显。

Promise

Promise是异步编程的一种优良的解决方案,它比传统的回调更加合理。在ES6中,Promise是一个对象,它有三种状态:进行中(pending)、已成功(resolved)、已失败(rejected)。所以Promise可能的改变就只有两种,从进行中转变到已成功或者从进行中转变到失败,并且一旦状态发生改变,之后便会一直保持这个状态,不会也无法改变。

4 总结

async/await 回调地狱提醒着我们,不要过渡依赖新特性,否则可能带来的代码执行效率的下降,进而影响到用户体验。同时,笔者认为,也不要过渡利用新特性修复新特性带来的问题,这样反而导致代码可读性下降。

当我翻开 redux 刚火起来那段时期的老代码,看到了许多过渡抽象、为了用而用的代码,硬是把两行代码能写完的逻辑,拆到了 3 个文件,分散在 6 行不同位置,我只好用字符串搜索的方式查找线索,最后发现这个抽象代码整个项目仅用了一次。

写出这种代码的可能性只有一个,就是在精神麻木的情况下,一口气喝完了 redux 提供的全部鸡汤。

就像 async/await 地狱一样,看到这种 redux 代码,我觉得远不如所谓没跟上时代的老前端写出的 jquery 代码。

决定代码质量的是思维,而非框架或语法,async/await 虽好,但也要适度哦。

2. 错误处理

Async/Await让try/catch可以同时处理同步和异步错误。在下面的promise示例中,try/catch不能处理JSON.parse的错误,因为它在Promise中。我们需要使用.catch,这样错误处理代码非常冗余。并且,在我们的实际生产代码会更加复杂。

JavaScript

const makeRequest = () => { try { getJSON() .then(result => { // JSON.parse可能会出错 const data = JSON.parse(result) console.log(data) }) // 取消注释,处理异步代码的错误 // .catch((err) => { // console.log(err) // }) } catch (err) { console.log(err) } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const makeRequest = () => {
  try {
    getJSON()
      .then(result => {
        // JSON.parse可能会出错
        const data = JSON.parse(result)
        console.log(data)
      })
      // 取消注释,处理异步代码的错误
      // .catch((err) => {
      //   console.log(err)
      // })
  } catch (err) {
    console.log(err)
  }
}

使用aync/await的话,catch能处理JSON.parse错误:

JavaScript

const makeRequest = async () => { try { // this parse may fail const data = JSON.parse(await getJSON()) console.log(data) } catch (err) { console.log(err) } }

1
2
3
4
5
6
7
8
9
const makeRequest = async () => {
  try {
    // this parse may fail
    const data = JSON.parse(await getJSON())
    console.log(data)
  } catch (err) {
    console.log(err)
  }
}

基础用法

let promise = new Promise(function(resolve, reject) {
    resolve('successed');
})

promise.then(function(data) {
    console.log(data);
}, function(err) {
    console.log(err);
})
//'successed'

Promise的构造函数接收一个带有两个参数的函数,这两个参数分别是Promise的状态改变为成功和失败的函数,然后使用then方法分别设置状态变为成功和失败后的操作。

resolve和reject函数都可以接受一个参数,这个参数传递会给then方法,所以上面代码,当resolve函数把promise的状态变成已成功之后,会把它的参数“successed”传递给then方法,因此最后then方法会把这个字符串打印出来。

下面我们再写一个读取文件的Promise

//ES5的写法
var fs = require('fs');

fs.readFile('./a.txt', function(err, data) {
    console.log(data.toString('utf8'));
    fs.readFile('./b.txt', function(err, data) {
        console.log(data.toString());
        fs.readFile('./c.txt', function(err, data) {
            console.log(data.toString());
        });
    });
})

//ES6的写法
function returnFilePromise(fileName) {
    return promise = new Promise((resolve, reject) => {
        fs.readFile(fileName, function(err, data) {
            if (!err) {
                resolve(data);
            } else {
                reject(err);
            }
        });
    })
}

let promise = returnFilePromise('./a.txt');

promise
    .then(function(data) {
        console.log(data.toString());
        return returnFilePromise('./b.txt');
    })
    .then(function (data) {
        console.log(data.toString());
        return returnFilePromise('./c.txt');
    })
    .then(function (data) {
        console.log(data.toString());
    })
    .catch(function (err) {
        console.log(err);
    })

上面两段代码的功能一样,都是继发异步读取三个文件,但是我们可以看到,用回调函数来写,则会出现多层嵌套,使得代码很难看,要是嵌套层数更多的话,代码的可读性和可维护性就会变得很差。而Promise却可以采用链式写法(因为then可以返回一个新的Promise),整个过程非常直观,最后的一个catch方法可以捕捉到前面三个then方法抛出的错误。

当然,Promise的魅力远不止这些,它还有很多有趣的方法比如finally方法、all方法和race方法等等。

5 更多讨论

讨论地址是:精读《逃离 async/await 地狱》 · Issue #82 · dt-fe/weekly

1 赞 2 收藏 1 评论

图片 2

3. 条件语句

下面示例中,需要获取数据,然后根据返回数据决定是直接返回,还是继续获取更多的数据。

JavaScript

const makeRequest = () => { return getJSON() .then(data => { if (data.needsAnotherRequest) { return makeAnotherRequest(data) .then(moreData => { console.log(moreData) return moreData }) } else { console.log(data) return data } }) }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const makeRequest = () => {
  return getJSON()
    .then(data => {
      if (data.needsAnotherRequest) {
        return makeAnotherRequest(data)
          .then(moreData => {
            console.log(moreData)
            return moreData
          })
      } else {
        console.log(data)
        return data
      }
    })
}

这些代码看着就头痛。嵌套(6层),括号,return语句很容易让人感到迷茫,而它们只是需要将最终结果传递到最外层的Promise。

上面的代码使用async/await编写可以大大地提高可读性:

JavaScript

const makeRequest = async () => { const data = await getJSON() if (data.needsAnotherRequest) { const moreData = await makeAnotherRequest(data); console.log(moreData) return moreData } else { console.log(data) return data } }

1
2
3
4
5
6
7
8
9
10
11
const makeRequest = async () => {
  const data = await getJSON()
  if (data.needsAnotherRequest) {
    const moreData = await makeAnotherRequest(data);
    console.log(moreData)
    return moreData
  } else {
    console.log(data)
    return data    
  }
}

Generator

4. 中间值

你很可能遇到过这样的场景,调用promise1,使用promise1返回的结果去调用promise2,然后使用两者的结果去调用promise3。你的代码很可能是这样的:

JavaScript

const makeRequest = () => { return promise1() .then(value1 => { return promise2(value1) .then(value2 => { return promise3(value1, value2) }) }) }

1
2
3
4
5
6
7
8
9
const makeRequest = () => {
  return promise1()
    .then(value1 => {
      return promise2(value1)
        .then(value2 => {        
          return promise3(value1, value2)
        })
    })
}

如果promise3不需要value1,可以很简单地将promise嵌套铺平。如果你忍受不了嵌套,你可以将value 1 & 2 放进Promise.all来避免深层嵌套:

JavaScript

const makeRequest = () => { return promise1() .then(value1 => { return Promise.all([value1, promise2(value1)]) }) .then(([value1, value2]) => { return promise3(value1, value2) }) }

1
2
3
4
5
6
7
8
9
const makeRequest = () => {
  return promise1()
    .then(value1 => {
      return Promise.all([value1, promise2(value1)])
    })
    .then(([value1, value2]) => {      
      return promise3(value1, value2)
    })
}

这种方法为了可读性牺牲了语义。除了避免嵌套,并没有其他理由将value1和value2放在一个数组中。

使用async/await的话,代码会变得异常简单和直观。

JavaScript

const makeRequest = async () => { const value1 = await promise1() const value2 = await promise2(value1) return promise3(value1, value2) }

1
2
3
4
5
const makeRequest = async () => {
  const value1 = await promise1()
  const value2 = await promise2(value1)
  return promise3(value1, value2)
}

基础用法

Generator函数是ES6中另一种异步编程方式,它会返回一个遍历器对象。Generator有三个跟其他函数不一样的地方:函数名带星号(*),函数内部的yield表达式和next调用函数方法。

function * g () {
    console.log(0);
    yield 1;
    yield 2;
    console.log(666);
    yield 3;
    return 4;
    yield 5;
}
var ge = g();
ge.next();
ge.next();
ge.next();
ge.next();
ge.next();

//0
//{ value: 1, done: false }
//{ value: 2, done: false }
//666
//{ value: 3, done: false }
//{ value: 4, done: false }
//{ value: undefined, done: true }

由上面的代码可以清楚地看到,我们每调用一次遍历器对象next方法,函数就会执行一次,直到遇到并执行完一个yield表达式,函数就会暂停下来,并保留这个状态,再调用一次next方法,函数会从上次停下来的地方接着执行下去,直到执行一个yield表达式后,又停下来,等待下一个next方法。当遇到return时,整个函数执行完毕,即使后面还有yield表达式也不会执行。

next方法返回的是一个有两个属性的对象,第一个属性是yield后面的值,第二个属性是表示Generator函数是否执行完毕,当done属性为true是则表示函数已经执行完毕了。

next方法可以带一个参数,这个参数代表上一个yield的返回值

//不带参数
function * g () {
    var a = yield 1;
    console.log(a);
}
var ge = g();
console.log(ge.next())
console.log(ge.next())

//{ value: 1, done: false }
//undefined
//{ value: undefined, done: true }

//带参数
function * g () {
    var a = yield 1;
    console.log(a);
}
var ge = g();
console.log(ge.next())
console.log(ge.next(1))

//{ value: 1, done: false }
//1
//{ value: undefined, done: true }

如果next不带参数的话,那么上一个yield的返回值是undefined。

有了next带参数这个功能,我们就可以在函数运行之后,还能向函数体内输入值,用以调整函数行为。

当yield太多时,手动next未免太麻烦,这时可以用for of循环来一次性执行

function* g() {
  yield 1;
  yield 2;
  yield 3;
}

for (let n of g()) {
  console.log(n);
}
// 1 2 3 

5. 错误栈

下面示例中调用了多个Promise,假设Promise链中某个地方抛出了一个错误:

JavaScript

const makeRequest = () => { return callAPromise() .then(() => callAPromise()) .then(() => callAPromise()) .then(() => callAPromise()) .then(() => callAPromise()) .then(() => { throw new Error("oops"); }) } makeRequest() .catch(err => { console.log(err); // output // Error: oops at callAPromise.then.then.then.then.then (index.js:8:13) })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const makeRequest = () => {
  return callAPromise()
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => {
      throw new Error("oops");
    })
}
makeRequest()
  .catch(err => {
    console.log(err);
    // output
    // Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
  })

Promise链中返回的错误栈没有给出错误发生位置的线索。更糟糕的是,它会误导我们;错误栈中唯一的函数名为callAPromise,然而它和错误没有关系。(文件名和行号还是有用的)。

然而,async/await中的错误栈会指向错误所在的函数:

JavaScript

const makeRequest = async () => { await callAPromise() await callAPromise() await callAPromise() await callAPromise() await callAPromise() throw new Error("oops"); } makeRequest() .catch(err => { console.log(err); // output // Error: oops at makeRequest (index.js:7:9) })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const makeRequest = async () => {
  await callAPromise()
  await callAPromise()
  await callAPromise()
  await callAPromise()
  await callAPromise()
  throw new Error("oops");
}
makeRequest()
  .catch(err => {
    console.log(err);
    // output
    // Error: oops at makeRequest (index.js:7:9)
  })

在开发环境中,这一点优势并不大。但是,当你分析生产环境的错误日志时,它将非常有用。这时,知道错误发生在makeRequest比知道错误发生在then链中要好。

Generator的异步使用

先简单介绍一下协程。多个函数并行执行,但是只能有一个函数处于运行状态,其它函数处于暂停状态,各个函数之间可以交换函数执行权。从内存上看,协程有多个函数栈,但是只有一个栈处于运行状态,这是一种以牺牲内存来实现多任务并行的方法。也正是因为Generator可以交出和收回执行权,所以它很适合处理异步

let returnFilePromise = (fileName) => {
    return new Promise((resolve, reject) => {
        fs.readFile(fileName, function(err, data) {
            if (!err) {
                resolve(data);
            } else {
                reject(err);
            }
        })
    })
}

function * readFiles () {
    yield returnFilePromise('./a.txt');
    yield returnFilePromise('./b.txt');
    yield returnFilePromise('./c.txt');
}

let ge = readFiles();

function gogogo (fn) {
    let result = fn.next()
    if (!result.done) {
        result.value.then(function (data) {
            gogogo(fn);
            console.log(data.toString())
        }) 
    } 
}

gogogo(ge);

//This is A
//This is B
//This is C

上面依旧是一个读取三个文件的异步代码(读取完a.txt后再读取b.txt),用Generator写起来的感觉特别像是在写同步代码,实际上他是异步执行的,这里我们另外写了一个让Generator可以自动执行的函数,每个yield返回值都是一个Promise,实际上,Generator经常会使用到Promise。

为了便于管理Generator函数,除了使每个yield函数返回Promise外,我们也可以利用Thunk函数做到这一点。

6. 调试

最后一点,也是非常重要的一点在于,async/await能够使得代码调试更简单。2个理由使得调试Promise变得非常痛苦:

  • 不能在返回表达式的箭头函数中设置断点

图片 3

  • 如果你在.then代码块中设置断点,使用Step Over快捷键,调试器不会跳到下一个.then,因为它只会跳过异步代码。

使用await/async时,你不再需要那么多箭头函数,这样你就可以像调试同步代码一样跳过await语句。

图片 4

Thunk函数

一般来说,Thunk函数是一种“传名调用”的实现方法

var a = 1

function fn (n) {
    console.log(n);
}
fn(a + 1); //相当于fn(2), 参数在进入函数体之前求值,传值调用

//使用Thunk函数
function fn (th) {
    console.log(th());
}
function thunk () {
    return a + 1;
}

fn(thunk) //传名调用

在JS中,Thunk函数会用来把一个多参数的函数变成值接受一个回调函数为参数的单参数函数,任何有回调函数作参数的函数都可以写成Thunk函数的形式。

let thunk = function(fn) {
  return function (...args) {
    return function (callback) {
      return fn.call(this, ...args, callback);
    }
  }
}

在实际生产开发时,一般会用thunkify模块来进行转化,相关模块的具体使用这里不再赘述。

结论

Async/Await是近年来JavaScript添加的最革命性的的特性之一。它会让你发现Promise的语法有多糟糕,而且提供了一个直观的替代方法。

async函数

async函数是Generator的语法糖,使得异步操作变得更加方便和语义化。并且它可以跟普通函数一样调用执行(自带执行器),而不需要像Generator那样部署执行器。

let myAsync = async function  () {
    let a = await returnFilePromise('./a.txt');
    let b = await returnFilePromise('./b.txt');
    let c = await returnFilePromise('./c.txt');

    console.log(a.toString());
    console.log(b.toString());
    console.log(c.toString());

    return c.toString();
}

myAsync().then(data => console.log(data.toString()))

//This is A
//This is B
//This is C

//This is C

await后面接的是Promise,当然也可以是原始类型的值,但就变成了同步操作(转化成Promise后立即执行resolve())。整个async函数的返回值是一个Promise,因此可以在函数调用后使用then方法,函数体内的return的返回值会当做then的回调函数的参数。

await会按代码书写的顺序执行,在上面代码中,必须等a.txt读取完毕,下一个await才会执行,也就是说,在await函数体内,await表达式是同步的。但是整个async函数是异步的。如果多个异步操作之间不存在继发关系,这样的等待未免多余,我们可以用Promise.all方法把多个异步操作放在一起。

await的返回值是它后面Promise中resolve或者reject的参数,其中reject的参数会被catch方法捕获,不管有没有写return。只要有一个await的Promise的状态变为reject,那么整个async函数就会中断执行(async返回的Promise的状态变为reject),如果我们希望有一个异步操作的成功与否不影响其他操作,那么我们可以把它放在try···catch中。

async function getErr() {
    try {
        await Promise.reject("error");
    } catch (e) {
        console.log(e);
    }
    return await returnFilePromise('./a.txt');
}

getErr().then(data => console.log(data.toString()))

//error
//This is A

或者稍微暴力一点,直接在那个Promise后面接一个catch方法来捕获错误

async function getErr() {
    await Promise.reject("error")
        .catch(e => console.log(e));
    return await returnFilePromise('./a.txt');
}

getErr().then(data => console.log(data.toString()));

忧虑

对于Async/Await,也许你有一些合理的怀疑:

  • 它使得异步代码不在明显: 我们已经习惯了用回调函数或者.then来识别异步代码,我们可能需要花数个星期去习惯新的标志。但是,C#拥有这个特性已经很多年了,熟悉它的朋友应该知道暂时的稍微不方便是值得的。
  • Node 7不是LTS(长期支持版本): 但是,Node 8下个月就会发布,将代码迁移到新版本会非常简单。

 

1 赞 1 收藏 评论

图片 5

最后

Promise确实是个好东西,Generator和async函数都用到了它。虽然在ES6之前就已经有了Promise,但是直到ES6才被写进标准,并大放异彩。

还有一件事,我偷懒了没好好写前两部分的错误处理,哈哈哈哈哈哈~

本文由澳门威斯尼人平台登录发布于Web前端,转载请注明出处:是把双刃剑,ES6中的异步

相关阅读