- 原文地址:How to Cancel Your Promise
- 原文做者:Seva Zaikov
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:jonjia
- 校對者:kangkai124 hexianga
在 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 是一個 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
上面的技術並非 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 是 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 代碼中看到他們,因此你應該考慮採用和認知負載 - 你真的有不少的它的使用場景嗎?若是是,那麼這是一個很是好的解決方案,你未來可能會感謝你本身。
在 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 是徹底不一樣的概念,但實際上它的應用更普遍 不只在 JavaScript ,因此你能夠將其視爲獨立於平臺的模式。和 Promie/Generator 相比,Streams 可能更好也可能更糟糕。若是你已經接觸過它,而且使用它來處理過一些(或者全部的)異步邏輯,你會發現 Streams 更好,若是你沒接觸過,你會發現 Streams 更糟糕,由於它是徹底不一樣的方法。
我不是一個使用 Streams 的專家,只是使用過一些,我認爲你應該使用它們來處理全部的異步事件,或者徹底不使用它們。因此,若是你已經在使用它們,這個問題對你來講應該不是一件難事,由於這是 Streams 庫的一個長期以來衆所周知的特性。
正如我所提到的,我沒有足夠的使用 Streams 的經驗來提供使用它們的解決方案,因此我只是放幾個關於 Streams 實現取消的連接:
事情朝着好的方向發展 - fetch 將會新增 abort 方法,如何取消 Promise 在未來還會熱議很長一段時間。取消 Promise 可以實現嗎?可能會可能不會。並且,取消 Promise 對於許多應用程序來講不是相當重要的 - 是的,你能夠提出一些額外的請求,但有一個以上的請求結果是很是罕見的。另外,若是發生一次或兩次,則能夠從一開始就使用擴展現例來解決這些特定函數。可是,若是你的應用程序中有不少這樣的狀況,請考慮一下上面列出的內容。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。