異步之三:Async 函數的使用及簡單實現

解決回調地獄的異步操做,Async 函數是終極辦法,但瞭解生成器和 Promise 有助於理解 Async 函數原理。因爲內容較多,分三部分進行,這是第三部分,介紹 Async 函數相關。第一部分介紹 Generator,第二部分介紹 Promise。ios

在這部分中,咱們會先介紹 Async 函數的基本使用,而後會結合前兩部分介紹的生成器和 Promise 實現一個 async 函數。git

1)Async 函數概覽

1.1 概念

經過在普通函數前加async操做符能夠定義 Async 函數:github

// 這是一個 async 函數
async function() {}
複製代碼

Async 函數體中的代碼是異步執行的,不會阻塞後面代碼執行,但它們的寫法和同步代碼類似。ajax

Async 函數會 返回一個已完成的 promise 對象,實際在使用的時候會和await操做符配合使用,在介紹await以前,咱們先看看 async 函數自己有哪些特色。express

1.2 Async 函數基本用法

1.2.1 函數體內沒有 await

若是 async 函數體內若是沒有await操做符,那麼它返回的 promise 對象狀態和他的函數體內代碼怎麼寫有關係,具體和 promise 的then()方法的處理方式相同:axios

1)沒有顯式 return 任何數據segmentfault

此時默認返回Promise.resolve():promise

var a = (async () => {})();
複製代碼

至關於網絡

var a = (async () => {
  return Promise.resolve();
})();
複製代碼

此時 a 的值:異步

a {
  [[PromiseStatus]]: 'resolved',
  [[PromiseValue]]: undefined
}
複製代碼

2)顯式 return 非 promise

至關於返回Promise.resolve(data)

var a = (async () => {
  return 111;
})();
複製代碼

至關於

var a = (async () => {
  return Promise.resolve(111);
})();
複製代碼

此時 a 的值:

a {
  [[PromiseStatus]]: 'resolved',
  [[PromiseValue]]: 111
}
複製代碼

3)顯式 return promise 對象

此時 async 函數返回的 promise 對象狀態由顯示返回的 promise 對象狀態決定,這裏以被拒絕的 promise 爲例:

var a = (async () => Promise.reject(111))();
複製代碼

此時 a 的值:

a {
  [[PromiseStatus]]: 'rejected',
  [[PromiseValue]]: 111
}
複製代碼

但實際使用中,咱們不會向上面那樣使用,而是配合await操做符一塊兒使用,否則像上面那樣,和 promise 相比,並無優點可言。特別的,沒有await操做符,咱們並不能用 async 函數解決相互依賴的異步數據的請求問題。

換句話說:咱們不關心 async 返回的 promise 狀態(一般狀況,async 函數不會返回任何內容,即默認返回Promise.resolve()),咱們關心的是 async 函數體內的代碼怎麼寫,由於裏面的代碼能夠異步執行且不阻塞 async 函數後面代碼的執行,這就爲寫異步代碼創造了條件,而且書寫形式上和同步代碼同樣。

1.2.2 await 介紹

await操做符使用方式以下:

[rv] = await expression;
複製代碼

expression:能夠是任何值,但一般是一個 promise;

rv: 可選。若是有且 expression 是非 promise 的值,則 rv 等於 expression 自己;否則,rv 等於 兌現 的 promise 的值,若是該 promise 被拒絕,則拋個異常(因此await通常被 try-catch 包裹,異常能夠被捕獲到)。

但注意await必須在 async 函數中使用,否則會報語法錯誤

1.2.3 await 使用

看下面代碼例子:

1)expression 後爲非 promise

(async () => {
  const b = await 111;
  console.log(b); // 111
})();
複製代碼

直接返回這個 expression 的值,即,打印 111

2)expression 爲兌現的 promise

(async () => {
  const b = await Promise.resolve(111);
  console.log(b); // 111
})();
複製代碼

返回兌現的 promise 的值,因此打印111

3)expression 爲拒絕的 promise

(async () => {
  try {
    const b = await Promise.reject(111);

    // 前面的 await 出錯後,當前代碼塊後面的代碼就不執行了
    console.log(b); // 不執行
  } catch (e) {
    console.log("出錯了:", e); // 出錯了:111
  }
})();
複製代碼

若是await後面的 promise 被拒絕或自己代碼執行出錯都會拋出一個異常,而後被 catch 到,而且,和當前await同屬一個代碼塊的後面的代碼再也不執行。

2)Async 函數處理異步請求

2.1 相互依賴的異步數據

在 promise 中咱們處理相互依賴的異步數據使用鏈式調用的方式,雖然相比回調函數已經優化不少,但書寫及理解上仍是沒有同步代碼直觀。咱們看下 async 函數如何解決這個問題。

先回顧下需求及 promise 的解決方案:

需求:請求 URL1 獲得 data1;請求 URL2 獲得 data2,但 URL2 = data1[0].url2;請求 URL3 獲得 data3,但 URL3 = data2[0].url3

使用 promise 鏈式調用能夠這樣寫代碼:

promiseAjax 在 第二部分介紹 promise 時在 3.1 中定義的,經過 promise 封裝的 ajax GET 請求。

promiseAjax('URL1')
  .then(data1 => promiseAjax(data1[0].url2))
  .then(data2 => promiseAjax(data2[0].url3);)
  .then(console.log(data3))
  .catch(e => console.log(e));
複製代碼

若是使用 Async 函數則能夠像同步代碼的同樣寫:

async function() {
  try {
    const data1 = await promiseAjax('URL1');
    const data2 = await promiseAjax(data1[0].url);
    const data3 = await promiseAjax(data2[0].url);
  } catch (e) {
    console.log(e);
  }
}
複製代碼

之因此能夠這樣用,是由於只有當前await等待的 promise 兌現後,它後面的代碼纔會執行(或者拋出錯誤,後面代碼都不執行,直接去到 catch 分支)。

這裏有兩點值得關注:

1)await幫咱們處理了 promise,要麼返回兌現的值,要麼拋出異常; 2)await在等待 promise 兌現的同時,整個 async 函數會掛起,promise 兌現後再從新執行接下來的代碼。

對於第 2 點,是否是想到了生成器?在 1.4 節中咱們會經過生成器 + promise 本身寫一個 async 函數。

2.2 無依賴關係的異步數據

Async 函數沒有Promise.all()之類的方法,咱們須要寫多幾個 async 函數。

能夠藉助Promise.all()在同一個 async 函數中並行處理多個無依賴關係的異步數據,以下:

async function fn1() {
  try {
    const arr = await Promise.all([
      promiseAjax("URL1"),
      promiseAjax("URL2"),
    ]);

    // ... do something
  } catch (e) {
    console.log(e);
  }
}
複製代碼

感謝 @賈順名評論

但實際開發中若是異步請求的數據是業務不相關的,不推薦這樣寫,緣由以下:

把全部的異步請求放在一個 async 函數中至關於手動增強了業務代碼的耦合,會致使下面兩個問題:

1)寫代碼及獲取數據都不直觀,尤爲請求多起來的時候; 2)Promise.all裏面寫多個無依賴的異步請求,若是 其中一個被拒絕或發生異常,全部請求的結果咱們都獲取不到

若是業務場景是不關心上面兩點,能夠考慮使用上面的寫法,否則,每一個異步請求都放在不一樣的 async 函數中發出。

下面是分開寫的例子:

async function fn1() {
  try {
    const data1 = await promiseAjax("URL1");

    // ... do something
  } catch (e) {
    console.log(e);
  }
}

async function fn2() {
  try {
    const data2 = await promiseAjax("URL2");

    // ... do something
  } catch (e) {
    console.log(e);
  }
}
複製代碼

3)Async 模擬實現

3.1 async 函數處理異步數據的原理

咱們先看下 async 處理異步的原理:

  • async 函數遇到await操做符會掛起;
  • await後面的表達式求值(一般是個耗時的異步操做)前 async 函數一直處於掛起狀態,避免阻塞 async 函數後面的代碼;
  • await後面的表達式求值求值後(異步操做完成),await能夠對該值作處理:若是是非 promise,直接返回該值;若是是 promsie,則提取 promise 的值並返回。同時告訴 async 函數接着執行下面的代碼;
  • 哪裏出現異常,結束 async 函數。

await後面的那個異步操做,每每是返回 promise 對象(好比 axios),而後交給 await 處理,畢竟,async-await 的設計初衷就是爲了解決異步請求數據時的回調地獄問題,而使用 promise 是關鍵一步。

async 函數自己的行爲,和生成器相似;而await等待的一般是 promise 對象,也正因如此,常說 async 函數是 生成器 + promise 結合後的語法糖。

既然咱們知道了 async 函數處理異步數據的原理,接下來咱們就簡單模擬下 async 函數的實現過程。

3.2 async 函數簡單實現

這裏只模擬 async 函數配合await處理網絡請求的場景,而且請求最終返回 promise 對象,async 函數自己返回值(已完成的 promise 對象)及更多使用場景這裏沒作考慮。

因此接下來的 myAsync 函數只是爲了說明 async-await 原理,不要將其用在生產環境中。

3.2.1 代碼實現

/** * 模擬 async 函數的實現,該段代碼取自 Secrets of the JavaScript Ninja (Second Edition),p159 */
// 接收生成器做爲參數,建議先移到後面,看下生成器中的代碼
var myAsync = generator => {
  // 注意 iterator.next() 返回對象的 value 是 promiseAjax(),一個 promise
  const iterator = generator();

  // handle 函數控制 async 函數的 掛起-執行
  const handle = iteratorResult => {
    if (iteratorResult.done) return;

    const iteratorValue = iteratorResult.value;

    // 只考慮異步請求返回值是 promise 的狀況
    if (iteratorValue instanceof Promise) {
      // 遞歸調用 handle,promise 兌現後再調用 iterator.next() 使生成器繼續執行
      // ps.原書then最後少了半個括號 ')'
      iteratorValue
        .then(result => handle(iterator.next(result)))
        .catch(e => iterator.throw(e));
    }
  };

  try {
    handle(iterator.next());
  } catch (e) {
    console.log(e);
  }
};
複製代碼

3.2.2 使用

myAsync接收的一個生成器做爲入參,生成器函數內部的代碼,和寫原生 async 函數相似,只是用yield代替了await

myAsync(function*() {
  try {
    const a = yield Promise.resolve(1);
    const b = yield Promise.resolve(a + 10);
    const c = yield Promise.resolve(b + 100);
    console.log(a, b, c); // 輸出 1,11,111
  } catch (e) {
    console.log("出錯了:", e);
  }
});
複製代碼

上面會打印1 11 111

若是第二個yield語句後的 promise 被拒絕Promise.reject(a + 10),則打印出錯了:11

3.2.3 說明:

  • myAsync 函數接受一個生成器做爲參數,控制生成器的 掛起 可達到使整個 myAsync 函數在異步代碼請求過程 掛起 的效果;
  • myAsync 函數內部經過定義handle函數,控制生成器的 掛起-執行

具體過程以下:

1)首先調用generator()生成它的控制器,即迭代器iterator,此時,生成器處於掛起狀態; 2)第一次調用handle函數,並傳入iterator.next(),這樣就完成生成器的第一次調用的; 3)執行生成器,遇到yield生成器再次掛起,同時把yield後表達式的結果(未完成的 promise)傳給 handle; 4)生成器掛起的同時,異步請求還在進行,異步請求完成(promise 兌現)後,會調用handle函數中的iteratorValue.then(); 5)iteratorValue.then()執行時內部遞歸調用handle,同時把異步請求回的數據傳給生成器(iterator.next(result)),生成器更新數據再次執行。若是出錯直接結束; 6)三、四、5 步重複執行,直到生成器結束,即iteratorResult.done === true,myAsync 結束調用。

若是看不明白,可參考下 第一部分 生成器相關和 第二部分 Promise 相關。

參考

【1】[美]JOHN RESIG,BEAR BIBEAULT and JOSIP MARAS 著(2016),Secrets of the JavaScript Ninja (Second Edition),p159,Manning Publications Co.
【2】async function-MDN
【3】await-MDN
【4】理解 JavaScript 的 async/await

相關文章
相關標籤/搜索