TC39JavaScript新語法Promise非同步

TC39 新語法深度解析(四):Promise.withResolvers — 更優雅地建立可控 Promise

整理 ES2024 Promise.withResolvers(Stage 4)的使用方式,與傳統 Deferred Promise 模式的完整比較,以及 AbortController 結合的實戰範例。

· 3 分鐘閱讀

這是 TC39 新語法深度解析系列的第四篇。當你需要在一個地方建立 Promise,在另一個地方 resolve/reject,過去的 workaround 總是冗長而且容易寫錯。Promise.withResolvers() 把這個模式變成一行。


Deferred Promise 問題:需要把 resolve 和 reject 拿出來

有時候你需要在一個地方建立 Promise,但希望 resolvereject 可以在另一個地方呼叫——例如,當你需要把 Promise 傳給外部系統,但由你的程式邏輯決定什麼時候完成。

// 過去的做法(尷尬)
let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});

// 問題:resolve 和 reject 是 let 變數,可能被意外覆寫
// 而且 Promise executor 立即執行——有時浪費

// 使用時:
fetchSomething()
  .then(data => resolve(data))
  .catch(err => reject(err));

// 等 promise 在某處被使用

Promise.withResolvers():一行解決

// 一行:返回 { promise, resolve, reject }
const { promise, resolve, reject } = Promise.withResolvers();

// 使用時:
fetchSomething()
  .then(data => resolve(data))
  .catch(err => reject(err));

// 使用 promise:
return promise;  // 只返回 promise,不暴露 resolve/reject

這個模式在封裝(encapsulation)上很有價值——你返回一個 Promise 給外部,但把控制權留在一個可控的地方。


實際應用場景

與 AbortController 整合:可取消的 fetch

function fetchWithTimeout(url, options = {}) {
  const { promise, resolve, reject } = Promise.withResolvers();
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 5000);

  fetch(url, { signal: controller.signal })
    .then(res => {
      clearTimeout(timeoutId);
      resolve(res);
    })
    .catch(err => {
      clearTimeout(timeoutId);
      reject(err);
    });

  return {
    promise,
    abort: () => controller.abort()
  };
}

// 使用
const { promise, abort } = fetchWithTimeout('/api/data');
promise.then(data => console.log(data));
// 取消
setTimeout(abort, 100);  // 5秒內取消

事件驅動的 bridge

// 將 DOM 事件橋接到 Promise
function once(element, eventName) {
  const { promise, resolve } = Promise.withResolvers();

  if (eventName === 'error') {
    element.addEventListener('error', resolve, { once: true });
  } else {
    element.addEventListener(eventName, resolve, { once: true });
  }

  return promise;
}

// 使用:等待圖片載入
const img = new Image();
img.src = '/photo.jpg';
document.body.appendChild(img);

const loaded = once(img, 'load');
const error = once(img, 'error');

Promise.race([loaded, error])
  .then(() => console.log('圖片載入成功'))
  .catch(() => console.log('圖片載入失敗'));

測試工具:等待某個函式被呼叫

function mockOnce(fn) {
  const { promise, resolve } = Promise.withResolvers();
  const original = fn;
  fn = (...args) => {
    fn = original;  // 恢復原函式
    resolve(args);    // 呼叫時 resolve
  };
  return { promise, mock: fn };
}

// 在測試中:
const { promise, mock } = mockOnce(console.log);
user.updateName('Alice');
promise.then(([name]) => {
  console.log(name);  // 'Alice'
});

與傳統模式的比較

做法語法問題
let resolve, reject; new Promise(...)多行、let 變數可被意外覆寫
Promise.withResolvers()一行沒有(推薦)

瀏覽器支援

ES2024——所有主流瀏覽器與 Node.js 22+ 都已原生支援:

// Node.js 確認
node -e "const {promise} = Promise.withResolvers(); console.log(promise instanceof Promise)"
// true

總結

Promise.withResolvers() 是個小語法,但它是那種「讓你回頭看舊代碼,納悶以前是怎麼忍受」的功能之一。適合用於:

  • 需要把 Promise 控制權(resolve/reject)封裝在內部
  • 需要與 AbortController 或其他取消機制整合
  • 測試時需要等待特定函式被呼叫

這是 TC39 新語法深度解析系列的第四篇。下一篇:Iterator Helpers — 迭代器終於有了 mapfiltertake


延伸閱讀


本文基於 ES2024 Promise.withResolvers(Stage 4)整理。