天天看點

ES6學習筆記21:async函數

含義

ES2017 标準引入 async 函數,使得一步操作變得更加友善

函數

async

對Generator 函數進行了改進,主要展現在以下方面

  • 内置執行器

Generator 函數的執行必須要有執行器,而

async

函數自帶執行器。

async

函數的執行,與普通函數一樣,隻要一行。

  • 更好的語義

函數

async

await

比起

*

yield

,語義更加清楚。

async

表示函數裡有異步操作,

await

表示其後的表達式需要更待結果。

  • 更廣的适用性

函數

async

函數的

await

後面,可以是

Promise

對象和原始類型的值(數值、字元串和布爾值但會自動轉換為resolved 的Promise 對象)

  • 傳回值是Promise

函數

async

的傳回值是Promise對象,比Generator函數傳回的值是Iterator對象友善多了。可以直接使用

then

方法執行下一步動作

基本用法

函數

async

函數傳回一個Promise對象,可以使用

then

方法添加回調函數。當函數執行的時候,一旦遇到

await

就會先傳回,等待異步操作完成,再接着執行函數體内後面的語句。

例子:指定多少秒後輸出一個值

function timeout(ms) {
  return new Promise( (resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms){
  await timeout(ms);
  console.log(value);
}

asyncPrint('hello async',1000);
           

由于

async

函數傳回的是Promise 對象,可以作為

await

指令的參數。是以,上面的代碼也可以改寫為:

async function timeout(ms){
  await new Promise( (resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms){
  await timeout(ms);
  console.log(value);
}

asyncPrint('hello async',1000);
           

函數

async

有多種使用方式

// 函數聲明
async function abc() {}

// 函數表達式
const foo = async function (){};

// 對象的方法
let obj = { async abc (){}};

// 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 () => {};
           

文法

傳回Promise 對象

函數

async

傳回一個Promise對象,

async

函數内部

return

語句傳回的值,會成為

then

方法回調函數的參數

async function f() {
  return 'hello async';
}

f().then(value=>console.log(value)); // 'hello async'
           

函數

async

函數内抛出錯誤,會導緻傳回的Promise 對象變為

reject

狀态。抛出的錯誤對象會被

catch

方法回調函數接收到。

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

f().then(
  value => console.log(value),
  error => console.log(error),
)
// 'Error'
           

Promise對象的狀态變化

函數

async

傳回的Promise對象,必須要等到内部所有

await

指令後面的

Promise

對象完成才會發生狀态改變,或者遇到

return

語句或者抛出錯誤。隻有

async

函數内部的異步操作執行完,才會執行

then

方法指定的回調函數。

async function getTitle(url) {
  let response = await fetch(url);
  let html = await response.test();
  return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
           

await 指令

  • 正常情況下,

    await

    指令後面是一個Promise對象,傳回Promise對象的結果。如果不是Promise對象,就直接傳回對應的值。
async function f() {
  return await 123;
  // 等同于 return 123;
}

f().then(value => console.log(value));
           
  • await

    指令後面是一個

    thenable

    對象(即:定義

    then

    方法的對象),此時

    await

    會将其等同于Promise對象。
  • 指令

    await

    後面的Promise對象如果變為

    reject

    狀态,則

    reject

    的參數被

    catch

    方法的回調函數接收到。
  • 任何一個

    await

    語句後面的Promise對象變為

    reject

    狀态,那麼整個

    async

    函數都會中斷執行
async function f() {
  await Promise.reject('Error');
  await Promise.resolve('Hello async');
}

           

如果希望即使第一步出錯了,也不要中斷後面的異步操作,有兩種方法解決:

  1. 将其放入

    try…catch

    代碼塊中,這樣不管這個異步操作是否成功,後面的

    await

    都會執行。
async function f(){
  try {
    await Promise.reject('Error');
  } catch(e) {
    
  }
  return await Promise.resolve('Hello async');
}

f().then(value => console.log(value));
           
  1. 指令

    await

    後面的Promise 對象再跟一個

    catch

    方法,處理前面可能會出現的錯誤
async function f(){
  await Promise.reject('Error').catch( e => console.log(e));
  return await Promise.resolve('Hello async');
}

f().then( value=>console.log(value)); // 'Hello async'
           

錯誤處理

  • 如果

    await

    後面的異步操作出錯,那麼等同于

    async

    函數傳回的Promise對象被

    reject

    .
  • 為了防止出錯,可以将其放在

    try…catch

    代碼塊中,有多個

    await

    的時候,可以統一放在

    try…catch

    代碼塊中.

注意

  1. 指令

    await

    後面的Promise對象, 運作結果可能是

    reject

    ,是以最好把

    await

    指令放在

    try…catch

    代碼塊中
  2. 多個

    await

    指令後面是異步操作,如果不存在繼發關系,最好讓他們同時觸發
let foo = await getFoo();
let bar = await getBar();
           

上面的代碼中,

getFoo

getBar

是兩個獨立的異步操作(互不依賴),被寫成繼發關系,這樣比較耗時。

同時觸發的寫法

// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise();
let bar = await barPromise();

           
  1. 指令

    await

    隻能在

    async

    函數中,如果用在其他普通函數中,就會報錯
  2. 函數

    async

    函數可以保留運作堆棧
const a = () => {
  b().then( () => c());
};
           

上面代碼中,函數

a

内部運作了一個異步操作

b()

。當

b()

運作的時候,函數

a()

不會中斷,而是繼續執行。等到

b()

運作結束,可能

a()

早就結束了,

b()

所在的上下文環境已經消失了,如果此時的

b()

c()

報錯,錯誤對戰将不包括

a()

.

改為

async

函數

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

此時,

b()

運作的時候,

a()

是暫停執行的,上下文環境都是儲存着的,一旦

b()

或者

c()

報錯,錯誤堆棧将包括

a()

.

async 函數實作原理

函數

async

實作原理就是将Generator 函數和自動執行器封裝在一個函數裡。

async function fn(args){
  // ...
}

// 等同于

function fn(args){
  return spawn(function* (){
     // ...          
   });
}

function spawn(genF){
  return new Promise(function(resolve,reject){
    const gen = genF();
    function step(nextF){
      let next;
      try{
        next = nextF();
      }catch(e){
        return reject(e);
      }
      if(next.done){
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(value){
        step(function() {return gen.next(v)});
      },function(e){
        step(function() {return gen.throw(e)});
      })
    }
    step(function() {return gen.next(undefined);});
  });
}
           

與其他異步處理方法比較

  • Promise 的寫法比回調函數的寫法有很大的改進,但是代碼完全都是Promise 的API,操作本身的語義不容易看出來
  • Generator 函數語義比Promise 寫法清晰,但是必須要有一個任務運作器,自動執行Generator函數。
  • async 函數的實作簡潔,符合語義,幾乎沒有語義不相關的代碼。
備注:本文是自己學習阮一峰老師的《ECMAScript 6 入門》所做的筆記,大部分例子來源于此書。

繼續閱讀