探索 Node.js 中的 Promise

Promise 是 JavaScript 中的一個特殊物件,表示一個非同步操作的最終完成(或失敗)及其結果值。本質上,Promise 是一個尚未可用但將來會有的值的佔位符。

把 Promise 想象成點披薩:你不會立刻拿到它,但外賣員承諾稍後會送到。你不知道*確切*的時間,但你知道結果要麼是“披薩已送達”,要麼是“出了點問題”。

Promise 的狀態

一個 Promise 可以處於以下三種狀態之一:

  • 待定(Pending):初始狀態,非同步操作仍在進行中。
  • 已兌現(Fulfilled):操作成功完成,Promise 現在已用一個值解決(resolved)。
  • 已拒絕(Rejected):操作失敗,Promise 因一個原因(通常是錯誤)而敲定(settled)。

當你點披薩時,你處於待定狀態,飢餓而充滿希望。如果披薩熱騰騰、香噴噴地送達,你就進入了已兌現狀態。但如果餐廳打電話說他們把你的披薩掉地上了,你就處於已拒絕狀態。

無論你的晚餐是喜是悲,一旦有了最終結果,Promise 就被認為是已敲定(settled)

Promise 的基本語法

建立 Promise 最常見的方法之一是使用 new Promise() 建構函式。該建構函式接受一個帶有兩個引數的函式:resolvereject。這兩個函式用於將 Promise 從待定狀態轉換到已兌現已拒絕狀態。

如果在執行器函式內部丟擲錯誤,Promise 將因該錯誤而被拒絕。執行器函式的返回值會被忽略:只應使用 resolvereject 來敲定 Promise。

const  = new ((, ) => {
  const  = true;

  if () {
    ('Operation was successful!');
  } else {
    ('Something went wrong.');
  }
});

在上面的例子中:

  • 如果 success 條件為 true,Promise 將被兌現,並將值 'Operation was successful!' 傳遞給 resolve 函式。
  • 如果 success 條件為 false,Promise 將被拒絕,並將錯誤 'Something went wrong.' 傳遞給 reject 函式。

使用 .then().catch().finally() 處理 Promise

一旦建立了 Promise,你就可以使用 .then().catch().finally() 方法來處理其結果。

  • .then() 用於處理已兌現的 Promise 並訪問其結果。
  • .catch() 用於處理已拒絕的 Promise 並捕獲可能發生的任何錯誤。
  • .finally() 用於處理已敲定的 Promise,無論 Promise 是解決還是拒絕。
const  = new ((, ) => {
  const  = true;

  if () {
    ('Operation was successful!');
  } else {
    ('Something went wrong.');
  }
});


  .( => {
    .(); // This will run if the Promise is fulfilled
  })
  .( => {
    .(); // This will run if the Promise is rejected
  })
  .(() => {
    .('The promise has completed'); // This will run when the Promise is settled
  });

鏈式呼叫 Promise

Promise 的一個強大特性是它們允許你將多個非同步操作連結在一起。當你鏈式呼叫 Promise 時,每個 .then() 塊都會等待前一個塊完成後再執行。

const { :  } = ('node:timers/promises');

const  = (1000).(() => 'First task completed');


  .( => {
    .(); // 'First task completed'
    return (1000).(() => 'Second task completed'); // Return a second Promise
  })
  .( => {
    .(); // 'Second task completed'
  })
  .( => {
    .(); // If any Promise is rejected, catch the error
  });

將 Async/Await 與 Promise 結合使用

在現代 JavaScript 中,處理 Promise 的最佳方式之一是使用 async/await。這讓你能夠編寫看起來像同步程式碼的非同步程式碼,使其更易於閱讀和維護。

  • async 用於定義一個返回 Promise 的函式。
  • await 用於在 async 函式內部暫停執行,直到一個 Promise 敲定。
async function () {
  try {
    const  = await promise1;
    .(); // 'First task completed'

    const  = await promise2;
    .(); // 'Second task completed'
  } catch () {
    .(); // Catches any rejection or error
  }
}

();

performTasks 函式中,await 關鍵字確保每個 Promise 在繼續執行下一條語句之前都已敲定。這使得非同步程式碼的流程更加線性和易讀。

本質上,上述程式碼的執行效果與使用者編寫以下程式碼相同:

promise1
  .then(function () {
    .();
    return promise2;
  })
  .then(function () {
    .();
  })
  .catch(function () {
    .();
  });

頂層 Await

使用 ECMAScript 模組時,模組本身被視為一個原生支援非同步操作的頂層作用域。這意味著你可以在頂層使用 await,而無需 async 函式。

import {  as  } from 'node:timers/promises';

await (1000);

Async/await 的用法可能比所提供的簡單示例複雜得多。Node.js 技術指導委員會成員 James Snell 有一個深入的演講,探討了 Promise 和 async/await 的複雜性。

基於 Promise 的 Node.js API

Node.js 為其許多核心 API 提供了基於 Promise 的版本,特別是在傳統上使用回撥處理非同步操作的情況下。這使得使用 Node.js API 和 Promise 更加容易,並降低了“回撥地獄”的風險。

例如,fs(檔案系統)模組在 fs.promises 下有一個基於 Promise 的 API:

const  = ('node:fs').;
// Or, you can import the promisified version directly:
// const fs = require('node:fs/promises');

async function () {
  try {
    const  = await .('example.txt', 'utf8');
    .();
  } catch () {
    .('Error reading file:', );
  }
}

();

在這個例子中,fs.readFile() 返回一個 Promise,我們使用 async/await 語法來非同步讀取檔案內容。

高階 Promise 方法

JavaScript 的 Promise 全域性物件提供了幾個強大的方法,可以幫助更有效地管理多個非同步任務:

Promise.all()

此方法接受一個 Promise 陣列,並返回一個新的 Promise。這個新的 Promise 會在所有 Promise 都兌現後解決。如果任何一個 Promise 被拒絕,Promise.all() 會立即拒絕。然而,即使發生拒絕,其他 Promise 仍會繼續執行。在處理大量 Promise 時,尤其是在批處理中,使用此函式可能會對系統記憶體造成壓力。

const { :  } = ('node:timers/promises');

const  = (1000).(() => 'Data from API 1');
const  = (2000).(() => 'Data from API 2');

.([, ])
  .( => {
    .(); // ['Data from API 1', 'Data from API 2']
  })
  .( => {
    .('Error:', );
  });

Promise.allSettled()

此方法等待所有 promise 都解決或拒絕,並返回一個物件陣列,描述每個 Promise 的結果。

const  = .('Success');
const  = .('Failed');

.([, ]).( => {
  .();
  // [ { status: 'fulfilled', value: 'Success' }, { status: 'rejected', reason: 'Failed' } ]
});

Promise.all() 不同,Promise.allSettled() 不會在失敗時短路。它會等待所有 promise 都敲定,即使有些被拒絕。這為批處理操作提供了更好的錯誤處理,因為你可能想知道所有任務的狀態,無論成功與否。

Promise.race()

此方法在第一個 Promise 敲定(無論是解決還是拒絕)時立即解決或拒絕。無論哪個 promise 先敲定,所有 promise 都會被完全執行。

const { :  } = ('node:timers/promises');

const  = (2000).(() => 'Task 1 done');
const  = (1000).(() => 'Task 2 done');

.([, ]).( => {
  .(); // 'Task 2 done' (since task2 finishes first)
});

Promise.any()

此方法在任意一個 Promise 解決後立即解決。如果所有 promise 都被拒絕,它將以一個 AggregateError 拒絕。

const { :  } = ('node:timers/promises');

const  = (2000).(() => 'API 1 success');
const  = (1000).(() => 'API 2 success');
const  = (1500).(() => 'API 3 success');

.([, , ])
  .( => {
    .(); // 'API 2 success' (since it resolves first)
  })
  .( => {
    .('All promises rejected:', );
  });

Promise.reject()Promise.resolve()

這些方法直接建立一個已拒絕或已解決的 Promise。

.('Resolved immediately').( => {
  .(); // 'Resolved immediately'
});

Promise.try()

Promise.try() 是一個執行給定函式的方法,無論該函式是同步還是非同步,並將其結果包裝在一個 promise 中。如果函式丟擲錯誤或返回一個被拒絕的 promise,Promise.try() 將返回一個被拒絕的 promise。如果函式成功完成,返回的 promise 將以其值兌現。

這對於以一致的方式啟動 promise 鏈特別有用,尤其是在處理可能同步丟擲錯誤的程式碼時。

function () {
  if (.() > 0.5) {
    throw new ('Oops, something went wrong!');
  }
  return 'Success!';
}

.()
  .( => {
    .('Result:', );
  })
  .( => {
    .('Caught error:', .message);
  });

在這個例子中,Promise.try() 確保如果 mightThrow() 丟擲錯誤,它將在 .catch() 塊中被捕獲,從而更容易地在一個地方處理同步和非同步錯誤。

Promise.withResolvers()

此方法建立一個新的 promise 及其關聯的 resolve 和 reject 函式,並將它們返回在一個方便的物件中。例如,當你需要建立一個 promise,但稍後從執行器函式外部解決或拒絕它時,可以使用此方法。

const { , ,  } = .();

(() => {
  ('Resolved successfully!');
}, 1000);

.( => {
  .('Success:', );
});

在此示例中,Promise.withResolvers() 讓你完全控制 promise 何時以及如何被解決或拒絕,而無需內聯定義執行器函式。這種模式常用於事件驅動程式設計、超時或與非基於 promise 的 API 整合時。

使用 Promise 進行錯誤處理

處理 Promise 中的錯誤可確保你的應用程式在出現意外情況時能正確執行。

  • 你可以使用 .catch() 來處理在 Promise 執行期間發生的任何錯誤或拒絕。
myPromise
  .then( => .())
  .catch( => .()) // Handles the rejection
  .finally( => .('Promise completed')); // Runs regardless of promise resolution
  • 或者,在使用 async/await 時,你可以使用 try/catch 塊來捕獲和處理錯誤。
async function () {
  try {
    const  = await myPromise;
    .();
  } catch () {
    .(); // Handles any errors
  } finally {
    // This code is executed regardless of failure
    .('performTask() completed');
  }
}

();

在事件迴圈中排程任務

除了 Promise,Node.js 還提供了幾種其他機制來在事件迴圈中排程任務。

queueMicrotask()

queueMicrotask() 用於排程一個微任務,這是一個輕量級任務,在當前執行的指令碼之後、任何其他 I/O 事件或計時器之前執行。微任務包括 Promise 解決和其他優先於常規任務的非同步操作。

(() => {
  .('Microtask is executed');
});

.('Synchronous task is executed');

在上面的例子中,“Microtask is executed” 將在 “Synchronous task is executed” 之後、任何 I/O 操作(如計時器)之前被列印。

process.nextTick()

process.nextTick() 用於排程一個回撥,在當前操作完成後立即執行。這對於希望確保回撥儘快執行,但仍需在當前執行上下文之後的情況非常有用。

.(() => {
  .('Next tick callback');
});

.('Synchronous task executed');

setImmediate()

setImmediate() 排程一個回撥,在 Node.js 事件迴圈的檢查階段執行,該階段在輪詢階段之後執行,大多數 I/O 回撥都在輪詢階段處理。

(() => {
  .('Immediate callback');
});

.('Synchronous task executed');

何時使用它們

  • 對於需要在當前指令碼之後、任何 I/O 或計時器回撥之前立即執行的任務(通常用於 Promise 解決),請使用 queueMicrotask()
  • 對於應在任何 I/O 事件之前執行的任務(通常用於延遲操作或同步處理錯誤),請使用 process.nextTick()
  • 對於應在輪詢階段之後、大多數 I/O 回撥處理完畢後執行的任務,請使用 setImmediate()

因為這些任務在當前的同步流程之外執行,所以這些回撥中的未捕獲異常不會被周圍的 try/catch 塊捕獲,並且如果管理不當(例如,透過給 Promise 附加 .catch() 或使用全域性錯誤處理器如 process.on('uncaughtException')),可能會導致應用程式崩潰。

有關事件迴圈以及各階段執行順序的更多資訊,請參閱相關文章 Node.js 事件迴圈