限時10分鐘,你會怎麼實現這段async/await程式碼?

林恒發表於2024-07-26

🧑‍💻 寫在開頭

點贊 + 收藏 === 學會🤣🤣🤣

本文用於記錄在React課程中學習時,課程中留下的一個關於async/await原理的思考題(預設讀者熟悉Promise

思考題

這個思考題就是:請將以下async/await程式碼,換一種方式實現,保證非同步等待功能和輸出順序:

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

const func = async() => {
    const data = await delay(2000, 'A');
    console.log(data);
    const res = await delay(2000, 'B');
    console.log(res);
};

func();

這裡可以先暫停去實現一下,以下內容從async/await基本知識開始。

async/await基本介紹

async/await是一種以更舒服的方式使用promise的特殊語法,讓非同步邏輯更加簡潔可讀,避免promise的鏈式寫法。

async

首先來介紹async,該關鍵字代表函式總是返回promise,返回的promiseresolved的情況和rejected的情況:

  • resolved情況如下:
    • 若函式返回了值,則該值會被Promise.resolve包裝,被解決的值就是該函式返回的值
    • 若函式沒有返回值,則promise中被解決的值為undefined
// 返回值
const func = async () => {
  return 1;
}
// 控制檯列印:Promise {<fulfilled>: 1} 'func'
console.log(func(), 'func');

// ------------------------------------------------------

// 沒有返回
const func1 = async () => {
}
// 控制檯列印:Promise {<fulfilled>: undefined} 'func1'
console.log(func1(), 'func1');
  • rejected情況:
    • 返回錯誤或是丟擲錯誤,會導致這個promiserejected
// ---------------------------返回錯誤---------------------------
const func = async () => {
  return new Error('error');
}
// 控制檯列印:Promise {<fulfilled>: Error: error
console.log(func(), 'func')


// ---------------------------丟擲錯誤---------------------------
const func1 = async () => {
  throw new Error('error');
}
// 控制檯列印:Promise {<fulfilled>: Error: error
console.log(func1(), 'func1')

await

async配對使用的就是await,並且await只能在async函式內工作,作用是等待promise完成並返回結果,這裡也分resolved的情況和rejected的情況:

  • resolved情況:
    • promiseresolved情況,被解決的值作為await表示式的值
const func = async () => {
    // await表示式的值就是被解決值'done',然後被賦值給data
    const data = await Promise.resolve('done');
}
  • rejected情況:
    • promiserejected情況,如果不使用try/catch捕獲,則語句(1)等同於語句(2)的效果,都會丟擲錯誤
const func = async () => {
    // 控制檯:Uncaught (in promise) error
    const data = await Promise.reject('error'); (1)
    throw 'error';                              (2)
}

關鍵點

熟悉async/await之後,就是要準備實現它了;在實現它之前,不妨將目前的特點總結一下:

  • 處理的是Promise
  • 能夠暫停函式執行
  • 能夠等待Promise解決之後,取出解決值,恢復函式執行

縱觀以上的特點,關鍵點就在於函式的暫停和恢復執行,只要解決它,就能夠實現async/await一樣的效果;

查閱資料能發現,在JavaScript中有一個能夠實現函式的暫停與執行的,那就是Generator(生成器),所以接下來先了解一下Generator的基本語法。

Generator簡介

Generator:譯為生成器,是ECMAScript 6新增的一個極為靈活的結構,擁有在一個函式塊內暫停恢復程式碼執行的能力;基礎程式碼示例如下:

const func = function* (){
  yield 1;
  yield 2;
  yield 3;
}


const iterator = func();

iterator.next(); // {value: 1, done: false}
iterator.next(); // {value: 2, done: false}
iterator.next(); // {value: 3, done: false}
iterator.next(); // {value: undefined, done: true}

Generator有以下特點:

  • 宣告生成器函式需要使用function* 函式名()語法,其實function *函式名()也可以,因為是函式的特殊語法,所以建議使用前者*靠近function的寫法
  • 生成器函式被呼叫的時候,函式並不會執行,而是返回一個生成器例項
  • GeneratorIterator的子類,所以生成器例項具有迭代器的特性
  • 生成器例項具有next、return、throw方法,其主要方法就是next;當next被呼叫時,會恢復函式執行,執行到最近的yield,然後暫停,並將yield後的結果返回到外部,也就是next呼叫後的value
  • yield既可以產出值,也可以輸入值;給next方法傳入的值,作為上一個yield表示式的值

接下來看一個使用next傳入值,yield接收值的例子,也請思考一下列印結果:

const func = function* () {
  console.log(1);
  const data = yield 2;
  console.log(data);
  yield 4;
}

const it = func();

console.log(it.next());
console.log(it.next(3));
console.log(it.next());

列印結果如下所示:

可能這裡的列印順序以及邏輯處理,對之前沒有接觸過生成器知識的朋友有點不知所以,接下來,我來對程式碼的執行做一個解釋(這裡用「」代表行數,例如:「8」表示第8行):

  • 執行「8」:執行生成器函式,生成生成器例項,此時函式內部並未執行
  • 執行「10」
    • 先呼叫next方法,函式開始執行
    • 執行「2」,列印1
    • 執行「3」,遇到yield 2,暫停執行,返回內容
    • 執行「10」,列印{value: 2, done: false}
  • 執行「11」
    • 先呼叫next方法,函式從上一次暫停處「3」恢復執行
    • 執行「3」,next中的引數作為yield 2表示式的值;data被賦值為3
    • 執行「4」,列印3
    • 執行「5」,遇到yield 4,暫停執行,返回內容
    • 執行「11」,列印{value: 4, done: false}
  • 執行「12」:
    • 先呼叫next方法,函式從上一次暫停處恢復執行
    • 無執行內容,迭代結束
    • 執行「12」,列印{value: undefined, done: true}

實現

思路

經過以上的步驟,對Generator的暫停和執行的特點有了認識,現在來講解一下實現思考題的思路。

觀察之前的這段程式碼:

const func = function* () {
  console.log(1);
  const data = yield 2;
  console.log(data);
  yield 4;
}

const it = func();

console.log(it.next());
console.log(it.next(3));
console.log(it.next());

可以發現「3」就比較類似業務程式碼中的const {data} = await API.xxx()形式,兩者都有等待後表示式的值賦值給左側的特點;

  • 等待後賦值

關鍵點就在這個“等待後賦值”上,將上面程式碼改造為的讓data等待一會兒再被賦值,如下:

const func = function* () {
  console.log(1);
  const data = yield 2;
  console.log(data);
  yield 4;
}

const it = func();

console.log(it.next());
// 等待3s再執行
setTimeout(() => {
    console.log(it.next(3));
    console.log(it.next());
}, 3000);

以上程式碼讓3s之後再執行next(3)data賦值,也就是再被賦值之前,操作空間很大,完全可以等待一些事件完成之後再呼叫next(3)將值傳入函式內部,且讓函式內部繼續執行。


如果讀者的思路一直跟到這裡,那麼我相信讀者對如何用GeneratorPromise實現async/await已經有了一些思路了,不妨先去動手試試,再來看下面的具體程式碼。

具體程式碼

那麼用GeneratorPromise實現文章開頭的思考題,如下所示:

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

const func = function* () {
  const data = yield delay(2000, 'A');
  console.log(data);
  const res = yield delay(2000, 'B');
  console.log(res);
}

let p1, p2, it = func();
// 接收第一個Promise
p1 = it.next().value;
p1.then((res) => {
  // 給data賦值,接收第二個Promise
  p2 = it.next(res).value;
  p2.then((res) => {
    // 執行到最後
    it.next(res);
  });
});

async/await與Generator/Promise的關係

到這裡,思考題的意圖就顯現出來了;目的就是點出async/await的原理其實就是Generator+Promise;再換一句話描述就是:async/awaitGenerator+Promise語法糖

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文件,大家一起討論學習,一起進步。

限時10分鐘,你會怎麼實現這段async/await程式碼?

相關文章