async函式,瞭解一下

夏頓天發表於2019-03-02

今天,帶大家來談談ES6中的async函式,我們在理解一個概念的時候,無外乎這是三個方面

  • 是什麼
  • 為什麼
  • 怎麼用

如果感覺文章太長,可以直接拉到下面,看小結?

sync是什麼

ES7提供了async函式,使得非同步操作變得更加方便。async函式是什麼?一句話,async函式就是Generator函式的語法糖。

我們來個案例,取讀檔案

Generator函式

var fs = require(`fs`);

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

var gen = function* (){
  var f1 = yield readFile(`/etc/fstab`);
  var f2 = yield readFile(`/etc/shells`);
  console.log(f1.toString());
  console.log(f2.toString());
};
複製程式碼

寫成async函式,就是下面這樣。

var asyncReadFile = async function (){
  var f1 = await readFile(`/etc/fstab`);
  var f2 = await readFile(`/etc/shells`);
  console.log(f1.toString());
  console.log(f2.toString());
};
複製程式碼

一比較就會發現,async函式就是將Generator函式的星號(*)替換成async,將yield替換成await,僅此而已。

我們會想,為什麼明明有Generator函式,還需要async函式

為什麼需要async函式

async函式對 Generator 函式的改進,體現在以下四點。

(1)內建執行器。Generator函式的執行必須靠執行器,所以才有了co模組,而async函式自帶執行器。也就是說,async函式的執行,與普通函式一模一樣,只要一行。

var result = asyncReadFile();
複製程式碼

上面的程式碼呼叫了asyncReadFile函式,然後它就會自動執行,輸出最後結果。這完全不像Generator函式,需要呼叫next方法,或者用co模組,才能得到真正執行,得到最後結果。

(2)更好的語義。async和await,比起星號和yield,語義更清楚了。async表示函式裡有非同步操作,await表示緊跟在後面的表示式需要等待結果。

(3)更廣的適用性。 co模組約定,yield命令後面只能是Thunk函式或Promise物件,而async函式的await命令後面,可以是Promise物件和原始型別的值(數值、字串和布林值,但這時等同於同步操作)。

(4)返回值是Promise。async函式的返回值是Promise物件,這比Generator函式的返回值是Iterator物件方便多了。你可以用then方法指定下一步的操作。

進一步說,async函式完全可以看作多個非同步操作,包裝成的一個Promise物件,而await命令就是內部then命令的語法糖。

語法

async怎麼使用呢?

一個函式前面加上async,就可以讓這個函式數成為非同步函式,跳出原本的執行順序

console.log(1)
async function asyfun () {
   console.log(2)
}
asyfun();
console.log(3)

// 列印結果:1,3,2
複製程式碼

(1)async函式返回一個Promise物件。

async函式內部return語句返回的值,會成為then方法回撥函式的引數。

async function f() {
  return `hello world`;
}

f().then(v => console.log(v))
// "hello world"
複製程式碼

上面程式碼中,函式f內部return命令返回的值,會被then方法回撥函式接收到。

async函式內部丟擲錯誤,會導致返回的Promise物件變為reject狀態。丟擲的錯誤物件會被catch方法回撥函式接收到。

async function f() {
  throw new Error(`出錯了`);
}

f().then(
  v => console.log(v),
  e => console.log(e)
)
// Error: 出錯了
複製程式碼

(2)async函式返回的Promise物件,必須等到內部所有await命令的Promise物件執行完,才會發生狀態改變。也就是說,只有async函式內部的非同步操作執行完,才會執行then方法指定的回撥函式。

下面是一個例子。

async function getTitle(url) {
  let response = await fetch(url);
  let html = await response.text();
  return html.match(/<title>([sS]+)</title>/i)[1];
}
getTitle(`https://tc39.github.io/ecma262/`).then(console.log)
// "ECMAScript 2017 Language Specification"

複製程式碼

(3)正常情況下,await命令後面是一個Promise物件。如果不是,會被轉成一個立即resolve的Promise物件。

async function f() {
  return await 123;
}

f().then(v => console.log(v))
// 123
複製程式碼

上面程式碼中,await命令的引數是數值123,它被轉成Promise物件,並立即resolve。

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,效果是一樣的。

只要一個await語句後面的Promise變為reject,那麼整個async函式都會中斷執行。

async function f() {
  await Promise.reject(`出錯了`);
  await Promise.resolve(`hello world`); // 不會執行
}
複製程式碼

上面程式碼中,第二個await語句是不會執行的,因為第一個await語句狀態變成了reject。

為了避免這個問題,可以將第一個await放在try…catch結構裡面,這樣第二個await就會執行。

async function f() {
  try {
    await Promise.reject(`出錯了`);
  } catch(e) {
  }
  return await Promise.resolve(`hello world`);
}

f()
.then(v => console.log(v))
// hello world

複製程式碼

另一種方法是await後面的Promise物件再跟一個catch方面,處理前面可能出現的錯誤。

async function f() {
  await Promise.reject(`出錯了`)
    .catch(e => console.log(e));
  return await Promise.resolve(`hello world`);
}

f()
.then(v => console.log(v))
// 出錯了
// hello world
複製程式碼

如果有多個await命令,可以統一放在try…catch結構中。

async function main() {
  try {
    var val1 = await firstStep();
    var val2 = await secondStep(val1);
    var val3 = await thirdStep(val1, val2);

    console.log(`Final: `, val3);
  }
  catch (err) {
    console.error(err);
  }
}
複製程式碼

(4)如果await後面的非同步操作出錯,那麼等同於async函式返回的Promise物件被reject。

async function f() {
  await new Promise(function (resolve, reject) {
    throw new Error(`出錯了`);
  });
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出錯了
複製程式碼

上面程式碼中,async函式f執行後,await後面的Promise物件會丟擲一個錯誤物件,導致catch方法的回撥函式被呼叫,它的引數就是丟擲的錯誤物件。具體的執行機制,可以參考後文的“async函式的實現”。

防止出錯的方法,也是將其放在try…catch程式碼塊之中。

async function f() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error(`出錯了`);
    });
  } catch(e) {
  }
  return await(`hello world`);
}
複製程式碼

小結

  1. async比generstor更好,語法上更語義化,內建執行器
  2. async返回值是Promise,函式內部的return值,會成為then方法回撥函式的引數。
  3. await只能在async函式內部使用
  4. 如果await後面的非同步操作出錯,那麼等同於async函式返回的Promise物件被reject

本文首發於微信公眾號:node前端

相關文章