JavaScript async和await 非同步操作

admin發表於2019-05-08

由於JavaScript的執行環境是單執行緒的,所以非同步操作尤為重要,否則不知道會卡成什麼樣子。

非同步操作通俗的講,就是將一件事情分為兩個階段來執行,最典型的就是ajax請求,當向伺服器發起請求之後,可以先去執行其他操作,無需等待這個請求過程,當請求完成,再去繼續處理請求發回的資料。

一.原有的非同步程式設計方式:

在ES2015之前,常見的非同步程式設計方式有以下幾種:

(1).回撥函式。

(2).釋出訂閱。

(3).事件監聽。

(4).Promise物件。

特別說明:Promise物件在ES2015之前是自定義實現的,並沒有被標準化。

二.ES2015新增非同步程式設計方式:

(1).Promise物件(ES2015對其進行了標準化)。

(2).Generator 函式。

三.優缺點簡單介紹:

簡單介紹一下原有非同步程式設計方式的優缺點。

JavaScript非同步程式設計的實現其實就是通過回撥函式來完成的。

首先看一個簡單程式碼片段:

[JavaScript] 純文字檢視 複製程式碼
fs.readFile(fileA, function (err, data) {
  fs.readFile(fileB, function (err, data) {
    fs.readFile(fileC, function (err, data) {
      fs.readFile(fileD, function (err, data) {
        // ...
      });
    });
  });
});

首先強調一下,回撥函式方式的非同步程式設計沒有任何問題,但是在程式碼的可讀性方面極差,大家可以想象,如果回撥函式的巢狀有100層,將會是一個什麼樣的情景。為了解決這種問題,Promise物件出現應運而生(ES2015之前雖然已經大量應用,但是並沒有標準化),關於它的用法可以參閱JavaScript Promise 物件一章節。

[JavaScript] 純文字檢視 複製程式碼
var readFile = require("fs-readfile-promise");
readFile(fileA)
.then(function(data){
  console.log(data.toString());
}).then(function(){
  return readFile(fileB);
}).then(function(data){
  console.log(data.toString());
}).catch(function(err) {
  console.log(err);
});

假定上面的程式碼中,readFile()都是非同步的檔案遠端請求操作;Generator函式之所以是良好的非同步容器,是因為yield語句會將執行權移出Generator函式,並且可以使用next()方法再次讓其獲得執行權。使用Generator函式可以使流程更加清晰,但是仍然不夠完美,因為無法自動將非同步操作全部執行完畢,當前可以使用co模組來解決此問題,簡單程式碼片段如下:

[JavaScript] 純文字檢視 複製程式碼
var gen = function* (){
  var f1 = yield readFile(fileA);
  var f2 = yield readFile(fileB);
};
var co = require("co");
co(gen);

上面的程式碼可以使用co模組自動化實現各個非同步操作。

特別說明:co模組約定yield後面只能是Promise物件或者是一個接收回撥函式的函式(或稱其為Thunk函式)。

四.async 函式:

它是JavaScript非同步操作比較完美的解決方案,兼具Promise物件和Generator函式能夠避免回撥函式層層巢狀的窘境,又具有邏輯清晰和自動化執行所有非同步操作的優點。

特別說明:async函式是ES2016新增。

語法結構:

[JavaScript] 純文字檢視 複製程式碼
async function [name]([param1[, param2[, ..., paramN]]]) {
   statements
}

引數解析:

(1).name:可選,用來規定函式的名稱,省略的話就是一個匿名函式。

(2).paramN:可選,傳遞給函式的引數。

(3).statements:可選,函式體內容(如果省略的話也就沒啥意義了)。

看一個簡單的程式碼例項:

[JavaScript] 純文字檢視 複製程式碼
async function func() {
  var x = await 10;
  var y = await 20;
  console.log(x);
  console.log(y);
}
func()

與普通的函式非常類似,下面簡單介紹一下它的特點:

(1).函式宣告的開頭需要使用async。

(2).函式內部具有await語句。

(3).呼叫方式與普通函式相同。

(4).能夠自動執行裡面的所有await語句,也就是所有非同步操作。

下面再來介紹一下await語句:

[JavaScript] 純文字檢視 複製程式碼
await expression;

expression期待是一個Promise物件,如果不是Promise物件,將會被轉換成 resolved 後的promise。

等同於如下程式碼:

[JavaScript] 純文字檢視 複製程式碼
Promise.resolve(expression)

await語句的返回值是它後面Promise物件resolved後的值,也就是傳遞給resolve()方法的引數值;如果promise被rejected,await 表示式會丟擲異常值。

特別說明:await語句不能用於普通函式中,否則會報錯。

程式碼例項:

[JavaScript] 純文字檢視 複製程式碼
async function func() {
  return await 10;
}
func().then(function(data){
  console.log(data)
})

async函式執行後,會立即返回一個Promise物件,並等待此物件狀態的變化,它的狀態有內部的await語句後面Promise物件的狀態決定;then的回撥函式的接收的值是return語句返回的await語句返回值。

[JavaScript] 純文字檢視 複製程式碼
async function func() {
  await 10;
  return await 20;
}
func().then(function(data){
  console.log(data)
})

async函式返回的Promise物件狀態的改變是等到所有await語句指定完畢之後。 

[JavaScript] 純文字檢視 複製程式碼
async function func() {
  await Promise.reject("螞蟻部落");
}
func().then(function(resove){
  console.log(resove)
},function(reject){
  console.log(reject)
})

如果await後面的Promise變為reject狀態,則reject的引數會被catch回撥函式接收。

特別說明:如果是變為reject狀態,前面不加return,reject的引數也會被catch回撥函式接收。

[JavaScript] 純文字檢視 複製程式碼
async function func() {
  await Promise.reject("螞蟻部落");
  await 10;
}
f().then(function(resove){
  console.log(resove)
},function(reject){
  console.log(reject)
})

丟擲錯誤後,會中斷整個async函式的執行。

[JavaScript] 純文字檢視 複製程式碼
async function func() {
  try {
    await Promise.reject("螞蟻部落一");
  } catch(e) {
  }
  return await Promise.resolve("螞蟻部落二");
}
func().then(function(resovle){
  console.log(resovle)
})

可以將可能丟擲錯誤的語句放入try catch語句中。

[JavaScript] 純文字檢視 複製程式碼
async function func() {
  await Promise.reject("螞蟻部落一").catch(function(reject){
    console.log(reject)
  });
  return await Promise.resolve("螞蟻部落二");
}
func().then(function(resovle){
  console.log(resovle)
})

也可以在可能丟擲錯誤的promise物件後面使用catch來捕獲丟擲的錯誤。

[JavaScript] 純文字檢視 複製程式碼
async function func() {
  try {
    var valA = await a();
    var valB = await b();
    var valC = await c();
  }
  catch (err) {
    console.error(err);
  }
}

如果有多個await語句,那麼可以將其統一放在try語句中。

特別說明:如果具有多個await語句,且它們之間不是繼發關係,建議讓它們同時觸發,以達到最大效能:

[JavaScript] 純文字檢視 複製程式碼
async function func(){
  let aV = await a();
  let bV = await b();
}

a和b是獨立的非同步操作,沒必要是繼發關係,也就是執行完a再去執行b,那麼可以採用以下方式:

[JavaScript] 純文字檢視 複製程式碼
async function func(){
  let [aV, bV] = await Promise.all([a(), b()]);
}

具體可以參閱Promise.all()方法一章節;也可以採用下面的方式:

[JavaScript] 純文字檢視 複製程式碼
async function func(){
  let aP=a();
  let bP=b();
  let aV = await aP;
  let bV = await bP;
}

建立兩個Promise物件,兩個幾乎同時開始非同步操作,不會等待a非同步操作完,然後在進行b的非同步操作。

相關文章