[譯] 如何取消你的 Promise?

如何取消你的 Promise?

在 JavaScript 語言的國際標準 ECMAScript 的 ES6 版本中,引入了新的異步原生對象 Promise。這是一個很是強大的概念,它使咱們能夠避免臭名昭著的 回調陷阱。例如,幾個異步操做很容易寫成下面這樣的代碼:html

function updateUser(cb) {
  fetchData(function(error, data) => {
    if (error) {
      throw error;
    }
    updateUserData(data, function(error, data) => {
      if (error) {
        throw error;
      }
      updateUserAddress(data, function(error, data) => {
        if (error) {
          throw error;
        }
        updateMarketingData(data, function(error, data) => {
          if (error) {
            throw error;
          }

          // finally!
          cb();
        });
      });
    });
  });
}

複製代碼

正如你所看到的,咱們嵌套了幾個回調函數,若是想要改變一些回調函數的順序,或者想同時執行一些回調函數,咱們將很難管理這些代碼。可是,經過 Promise,咱們能夠將其重構爲可讀性更好的版本:前端

// 咱們再也不須要回調函數了 – 只須要使用 then 方法
// 處理函數的返回結果
function updateUser() {
  return fetchData()
    .then(updateUserData)
    .then(updateUserAddress)
    .then(updateMarketingData);
}

複製代碼

這樣的代碼不只更簡潔,可讀性更強,並且能夠輕鬆切換回調的順序,同時執行回調或刪除沒必要要的回調(或者在回調鏈中間新增一個回調)。node

使用 Promise 鏈式寫法的一個缺點是咱們沒法訪問每一個回調函數的做用域(或者其中未返回的的變量),你能夠閱讀 Alex Rauschmayer 博士這篇 a great article 來解決這個問題。python

可是,我發現了 這個問題,你不能取消 Promise,這是一個很關鍵的問題。有時你須要取消 Promise,你要構建變通的方法 — 工做量取決於你多長時間使用一次這個功能。react

使用 Bluebird

Bluebird 是一個 Promise 實現庫, 徹底兼容原生的 Promise 對象, 而且在原型對象 Promise.prototype 上添加了一些有用的方法(譯者注:擴展了原生 Promise 對象的方法)。在這裏咱們只介紹下 cancel 方法, 它部分實現了咱們的想要的 — 當咱們使用 promise.cancel 取消 Promise 時,它容許咱們有自定義的邏輯(爲何是部分實現? 由於代碼冗長還不通用).android

在咱們的例子中,咱們來看看如何使用 Bluebird 實現取消 Promise:ios

import Promise from 'Bluebird';

function updateUser() {
  return new Promise((resolve, reject, onCancel) => {
    let cancelled = false;

    // 你須要更改 Bluebird 的配置,才能使用 cancellation 特性
    // http://bluebirdjs.com/docs/api/promise.config.html
    onCancel(() => {
      cancelled = true;
      reject({ reason: 'cancelled' });
    });

    return fetchData()
      .then(wrapWithCancel(updateUserData))
      .then(wrapWithCancel(updateUserAddress))
      .then(wrapWithCancel(updateMarketingData))
      .then(resolve)
      .catch(reject);

    function wrapWithCancel(fn) {
      // promise resolved 的狀態只須要傳遞一個參數
      return (data) => {
        if (!cancelled) {
          return fn(data);
        }
      };
    }
  });
}

const promise = updateUser();
// 等一會...
promise.cancel(); // 用戶仍是會被更新
複製代碼

正如你所看到的,咱們在以前乾淨的例子中增長了不少代碼。不幸的是,沒有其餘辦法,由於咱們不能中止執行一個隨機的 Promise 鏈(若是咱們想,咱們須要把它包裝到另外一個函數中),因此咱們須要用處理取消狀態的函數包裝每一個回調函數。git

純 Promises

上面的技術並非 Bluebird 的特別之處,更多的是關於接口 - 你能夠實現你本身的取消版本,但須要額外的屬性/變量。一般這種方法被稱爲cancellationToken,在本質上,它幾乎和前一個同樣,但不是在Promise.prototype.cancel上有這個方法,咱們將它實例化在一個不一樣的對象 - 咱們能夠用cancel屬性返回一個對象,或者咱們能夠接受額外的參數,一個對象,咱們將在那裏添加一個屬性。github

function updateUser() {
  let resolve, reject, cancelled;
  const promise = new Promise((resolveFromPromise, rejectFromPromise) => {
    resolve = resolveFromPromise;
    reject = rejectFromPromise;
  });

  fetchData()
    .then(wrapWithCancel(updateUserData))
    .then(wrapWithCancel(updateUserAddress))
    .then(wrapWithCancel(updateMarketingData))
    .then(resolve)
    .then(reject);

  return {
    promise,
    cancel: () => {
      cancelled = true;
      reject({ reason: 'cancelled' });
    }
  };

  function wrapWithCancel(fn) {
    return (data) => {
      if (!cancelled) {
        return fn(data);
      }
    };
  }
}

const { promise, cancel } = updateUser();
// 等一會...
cancel(); // 用戶仍是會被更新
複製代碼

這比之前的解決方案稍微冗長一點,可是它解決了一樣的問題,若是你沒有使用 Bluebird(或者不想在 Promise 中使用非標準的方法),這是一個可行的解決方案。正如你所看到的,咱們改變了簽名 - 如今咱們返回對象而不是一個 Promise,但實際上咱們能夠傳遞一個對象參數給函數,並附上cancel方法(或者 Promise 的 monkey-patch 實例,但它也會在之後給你形成問題)。若是你只在幾個地方有這個要求,這是一個很好的解決方案。後端

切換到 generators

Generators 是 ES6 另外一個新特性,但因爲某些緣由,它們並無被普遍使用。使用前請想清楚 - 你團隊中的新手會看不懂呢,仍是所有成員都遊刃有餘呢?並且,它還存在於其餘一些語言中,如 Python,因此做爲團隊使用這個解決方案應該會很容易。

Generators 有它本身的文檔, 因此我不會介紹基礎知識,只是實現一個 Generator 執行器,這將容許咱們以通用方式取消咱們的 Promise,而不會影響咱們的代碼。

// 這是運行咱們異步代碼的核心方法
// 而且提供 cancellation 方法
function runWithCancel(fn, ...args) {
  const gen = fn(...args);
  let cancelled, cancel;
  const promise = new Promise((resolve, promiseReject) => {
    // 定義 cancel 方法,並返回它
    cancel = () => {
      cancelled = true;
      reject({ reason: 'cancelled' });
    };

    let value;

    onFulfilled();

    function onFulfilled(res) {
      if (!cancelled) {
        let result;
        try {
          result = gen.next(res);
        } catch (e) {
          return reject(e);
        }
        next(result);
        return null;
      }
    }

    function onRejected(err) {
      var result;
      try {
        result = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(result);
    }

    function next({ done, value }) {
      if (done) {
        return resolve(value);
      }
      // 假設咱們老是接收 Promise,因此不須要檢查類型
      return value.then(onFulfilled, onRejected);
    }
  });

  return { promise, cancel };
}
複製代碼

這是一個至關長的函數,但基本上它(除了檢查,固然這是一個很是初級的實現) - 代碼自己將保持徹底相同,咱們將從字面上獲取cancel方法!讓咱們看看如何在咱們的例子中使用它:

// * 表示這是一個 Generator 函數
// 你能夠把 * 放到幾乎任何地方 :)
// 這種寫法語法上和 async/await 很類似
function* updateUser() {
  // 假設咱們全部的函數都返回 Promise
  // 不然須要調整咱們的執行器函數
  // 去接受 Generator
  const data = yield fetchData();
  const userData = yield updateUserData(data);
  const userAddress = yield updateUserAddress(userData);
  const marketingData = yield updateMarketingData(userAddress);
  return marketingData;
}

const { promise, cancel } = runWithCancel(updateUser);

// 見證奇蹟的時刻
cancel();
複製代碼

正如你所看到的,接口保持不變,可是如今咱們能夠選擇在執行過程當中取消任何基於 Generator 的函數,只需將其包裝到合適的運行器中便可。缺點是一致性 - 若是它只是在你的代碼中的幾個地方,那麼別人看你代碼時會很困惑,由於你在代碼中使用了全部可能的異步方法,這又是一個折中方案。

我想,Generator 是最具擴展性的選擇,由於你能夠從字面上完成全部你想要的事情 - 若是出現某種狀況,你能夠暫停,等待,重試,或者運行另外一個 Generator。可是,我並無常常在 JavaScript 代碼中看到他們,因此你應該考慮採用和認知負載 - 你真的有不少的它的使用場景嗎?若是是,那麼這是一個很是好的解決方案,你未來可能會感謝你本身。

注意 async/await

ES2017 版本提供了 async/await,你能夠在 Node.js(版本7.6以後)中沒有任何標誌的狀況下使用它們。不幸的是,沒有任何東西能夠支持取消 Promise,並且因爲 async 函數隱含地返回 Promise,因此咱們不能真正感受到它(附加一個屬性或返回其餘東西),只有 resolved/rejected 狀態的值。這意味着爲了使咱們的函數能夠被取消,咱們須要傳遞一個對象,並將每一個調用包裝在咱們著名的包裝器方法中:

async function updateUser(token) {
  let cancelled = false;

  // 咱們不調用 reject,由於咱們沒法訪問
  // 返回的 Promise
  // 咱們不調用其它函數
  // 在結束時調用 reject
  token.cancel = () => {
    cancelled = true;
  };

  const data = await wrapWithCancel(fetchData)();
  const userData = await wrapWithCancel(updateUserData)(data);
  const userAddress = await wrapWithCancel(updateUserAddress)(userData);
  const marketingData = await wrapWithCancel(updateMarketingData)(userAddress);

  // 由於咱們已經包裝了全部的函數,以防取消
  // 不須要調用任何實際函數來達到這一點
  // 咱們也不能調用 reject 方法
  // 由於咱們沒法控制返回的 Promise
  if (cancelled) {
    throw { reason: 'cancelled' };
  }

  return marketingData;

  function wrapWithCancel(fn) {
    return data => {
      if (!cancelled) {
        return fn(data);
      }
    }
  }
}

const token = {};
const promise = updateUser(token);
// 等一會...
token.cancel(); // 用戶仍是會被更新
複製代碼

這是很是類似的解決方案,可是由於咱們沒有直接在cancel方法中調用 reject,因此可能會使讀者感到困惑。另外一方面,它是如今語言的一個標準功能,具備很是方便的語法,容許你在後面使用前面調用的結果(因此在這裏解決了 Promise 鏈式調用的問題),而且具備很是簡明和直觀的經過try / catch的錯誤處理。因此,若是取消再也不困擾你(或者你能夠用這種方式來取消某些東西),那麼這個特性絕對是在現代 JavaScript 中編寫異步代碼的最好方式。

使用 streams (就像 RxJS)

Streams 是徹底不一樣的概念,但實際上它的應用更普遍 不只在 JavaScript ,因此你能夠將其視爲獨立於平臺的模式。和 Promie/Generator 相比,Streams 可能更好也可能更糟糕。若是你已經接觸過它,而且使用它來處理過一些(或者全部的)異步邏輯,你會發現 Streams 更好,若是你沒接觸過,你會發現 Streams 更糟糕,由於它是徹底不一樣的方法。

我不是一個使用 Streams 的專家,只是使用過一些,我認爲你應該使用它們來處理全部的異步事件,或者徹底不使用它們。因此,若是你已經在使用它們,這個問題對你來講應該不是一件難事,由於這是 Streams 庫的一個長期以來衆所周知的特性。

正如我所提到的,我沒有足夠的使用 Streams 的經驗來提供使用它們的解決方案,因此我只是放幾個關於 Streams 實現取消的連接:

接受

事情朝着好的方向發展 - fetch 將會新增 abort 方法,如何取消 Promise 在未來還會熱議很長一段時間。取消 Promise 可以實現嗎?可能會可能不會。並且,取消 Promise 對於許多應用程序來講不是相當重要的 - 是的,你能夠提出一些額外的請求,但有一個以上的請求結果是很是罕見的。另外,若是發生一次或兩次,則能夠從一開始就使用擴展現例來解決這些特定函數。可是,若是你的應用程序中有不少這樣的狀況,請考慮一下上面列出的內容。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索