- 原文地址: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、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。