isMounted
在 ES6 类上已经不可用,我们已经有一个警告说我们“可能”删除它们,但我们实际上没有 github 问题来弃用它们。 根据我们今天的讨论,我们基本上同意我们将开始远离isMounted
并弃用它。 我们仍然需要围绕 Promise(和相关用例)找出一些好故事。
这个问题是为了跟踪实现这个目标的进展。
有关背景,请阅读:
我不同意这一点。 特别是 ES6 承诺不能在componentWillUnmount
上可靠地取消,因此删除检查组件是否在 setState 或其他操作之前安装的唯一方法为许多难以跟踪的异步错误开辟了道路。
我确实阅读了评论。 我只是觉得这些问题很棘手。
为什么承诺不能可靠取消? 任何来源/证据/例子?
2015 年 11 月 16 日,星期一,Evan Jacobs [email protected]写道:
我不同意这一点。 特别是 ES6 承诺不能可靠
在 componentWillUnmount 上取消,因此删除检查是否的唯一方法
组件在 setState 或其他操作打开之前安装
许多难以追踪的异步错误的方法。
@nvartolomei查看 ES6 承诺规范。
这是一个长期目标,而不是立即发生的事情。 但我们希望在一个地方跟踪计划和讨论,而不是在出现这种情况时跨每个问题的评论。 我们知道 Promises 目前不可取消的问题,这是我们还没有这样做的一个主要原因。
@yaycmyk为了过度简化一个非常复杂的问题......评论是说......使用isMounted
来避免未安装组件的setState
实际上并没有解决setState
警告试图表明 - 事实上,它只是隐藏了问题。 此外,作为承诺的结果调用setState
无论如何都是一种反模式,因为它可能导致竞争条件,而这些条件不一定会出现在测试中。 因此,我们想摆脱它,并找出在 React 中使用 promise 的“最佳实践”建议。
我同意这些问题有点难以理解,但这主要是因为这是一个复杂的问题,我们仍在弄清楚并且还没有明确的回应。
无论如何,作为 promise 的结果调用 setState 是一种反模式,因为它可能导致竞争条件,而这些条件不一定会出现在测试中
我们可以同意不同意这一点。 有时,内容是异步获取的,一旦解决,您就不想通过全面的重新渲染来弹出该内容。 我专门在不需要完整虚拟重新渲染的无限表视图实现中使用它。
您可能无法取消承诺,但您可以在卸载时取消对组件的引用,如下所示:
const SomeComponent = React.createClass({
componentDidMount() {
this.protect = protectFromUnmount();
ajax(/* */).then(
this.protect( // <-- barrier between the promise and the component
response => {this.setState({thing: response.thing});}
)
);
},
componentWillUnmount() {
this.protect.unmount();
},
});
重要的区别是在this.protect.unmount()
中调用componentWillUnmount
,所有回调都被取消引用,这意味着组件被取消引用,然后当承诺完成时,它只调用一个无操作。 这应该可以防止与承诺引用未安装组件相关的任何内存泄漏。 来源protectFromUnmount
这个简单的方法可用于将取消添加到任何承诺
const makeCancelable = (promise) => {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then((val) =>
hasCanceled_ ? reject({isCanceled: true}) : resolve(val)
);
promise.catch((error) =>
hasCanceled_ ? reject({isCanceled: true}) : reject(error)
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};
编辑:更新正确性/完整性。
如何使用
const somePromise = new Promise(r => setTimeout(r, 1000));
const cancelable = makeCancelable(somePromise);
cancelable
.promise
.then(() => console.log('resolved'))
.catch(({isCanceled, ...error}) => console.log('isCanceled', isCanceled));
// Cancel promise
cancelable.cancel();
列出支持 ES6 承诺以使其可取消的方法是无关紧要的。 目的应该是提供一个适用于规范的解决方案,而不是试图围绕规范工作。
我同意。 当我们收到 promise 结果时,不是简单地检查组件是否仍然被挂载,我们必须诉诸各种魔法,这样我们就可以将我们的 promise 与它应该设置结果的组件“解除绑定”,显然与 promise 的方式作斗争被设计。
对我来说,感觉就像过度设计一个解决方案,其中一个简单的测试是解决这个问题的最简单方法。
我们可以通过以下方式保持简单的检查:
React.createClass(function() {
componentDidMount: function() {
this._isMounted = true;
ajax(/* */).then(this.handleResponse);
}
handleResponse: function(response) {
if (!this._isMounted) return; // Protection
/* */
}
componentWillUnmount: function() {
this._isMounted = false;
}
});
这当然是我的观点,但在我看来,在 react 组件中使用 promise 加载异步数据是一种常见的场景,它应该被 react 覆盖,而不必编写我们自己的样板代码。
问题是,为了让真正的挂载状态休止,我们必须在 react 将在每个组件中完成 DOM 挂载过程时添加侦听器(相同,附加 componentDidMount,如果已定义),但它会影响性能,因为我们不需要到处休耕。 由于 componentDidMount 未定义,因此组件默认不侦听 DOM 挂载就绪。
如果setState
可以传递一个链式承诺来解决所需的状态变化怎么办? 如果组件卸载,那么如果有任何挂起的承诺,它们的最终结果将被忽略。
@istarkov漂亮的图案,喜欢! 这是它的稍微改变的 API:
// create a new promise
const [response, cancel] = await cancelable(fetch('/api/data'));
// cancel it
cancel();
由于我是 React 和阅读文档的新手,只是想把它扔出去:通过 Ajax提示.isMounted()
,因此该网站不同意该网站。 很高兴看到有关如何取消componentWillUnmount
初始加载的完整提示,也许使用上面的@istarkov模式。
@dtertman在https://github.com/facebook/react/pull/5870 中修复,当文档被挑选出来时将在线。
@jimfb谢谢,不知道我是怎么在搜索中错过的。
@istarkov不确定这是否是故意的,但如果原始承诺失败,您的makeCancelable
无法处理。 当原始承诺被拒绝时,不会调用处理程序。
这似乎并不理想,因为您可能仍然希望处理原始承诺的错误。
这是我对makeCancelable
建议,用于处理原始承诺中的拒绝:
const makeCancelable = (promise) => {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then((val) =>
hasCanceled_ ? reject({isCanceled: true}) : resolve(val)
);
promise.catch((error) =>
hasCanceled_ ? reject({isCanceled: true}) : reject(error)
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};
如果做出可取消的承诺是个好主意,我不确定我的立场,但如果我们要取消承诺,我们应该保留潜在的行为:)。
@vpontis :+1:
@istarkov您的原始帖子在此处引用: https : //facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html
想要更新您的帖子还是应该给帖子的作者发消息?
@vpontis谢谢,我会解决的! (https://github.com/facebook/react/pull/6152)
嘿@jimfb ,在互联网上遇到你很有趣!
makeCancelable
函数中的另一个错误修复:它可能会在最近的节点版本中导致UnhandledPromiseRejectionWarning
(特别是在使用新版本的节点运行测试时)。 节点 6.6.0的变化之一是所有未处理的承诺拒绝都会导致警告。 来自@vpontis的现有代码在同一个基本承诺上有单独的then
和catch
调用。 实际上,这会创建_两个_promise,一个只处理成功,一个只处理错误。 这意味着如果出现错误,第一个承诺将被节点视为未处理的承诺拒绝。
修复非常简单:只需将两个调用链接起来,这样它就会对成功和错误处理程序做出一个承诺。 这是固定代码:
const makeCancelable = (promise) => {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise
.then((val) =>
hasCanceled_ ? reject({isCanceled: true}) : resolve(val)
)
.catch((error) =>
hasCanceled_ ? reject({isCanceled: true}) : reject(error)
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};
@alangpierce这非常接近正确,但不完全正确; 如果resolve()
或reject()
出于任何原因同步抛出已解决的 promise,则将调用两个处理程序。
解决方案是使用.then(onFulfilled, onRejected)
模式:
const makeCancelable = (promise) => {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then(
(val) => hasCanceled_ ? reject({isCanceled: true}) : resolve(val),
(error) => hasCanceled_ ? reject({isCanceled: true}) : reject(error)
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};
当查看第 3 点关于为什么不推荐使用 isMounted() 时,这个 makeCancelable 解决方案是否实际上与 isMounted() 调用相同:
当组件完全卸载时调用 setState。
这是一个强有力的迹象,表明异步回调没有被正确清理。 不幸的是,主流的 JS API 很容易避免清理挂起的异步回调。
一次回调没什么大不了的。 但是,该回调挂在对象和中间回调、承诺和订阅上。 如果你的很多组件都这样做,你很快就会遇到内存问题
makeCancellable 只是创建了另一个 promise,它最终持有对持有组件引用的函数的引用。 makeCancellable 解决方案只是将布尔属性 isMounted 移动到承诺中。
为了解决 GC 问题,您需要在调用 cancel() 时将某些内容清零。 否则你仍然有一个从异步进程到组件的引用链。
class CancellableDeferred {
constructor(request) {
this.deferred = $.Deferred();
request.then((data) => {
if (this.deferred != null) {
this.deferred.resolve(data);
}
});
request.fail((data) => {
if (this.deferred != null) {
this.deferred.reject(data);
}
});
}
cancel() {
this.deferred = null;
}
promise() {
return this.deferred.promise();
}
}
-> 是我将如何使用 jQuery 延迟对象来做到这一点。 我对 Promise API 不太熟悉,所以我不知道它会是什么样子。 此外,当调用 cancel() 并且延迟尚未解决时,这不会拒绝延迟。 可能人们对这应该如何工作有不同的看法。
所以链看起来像这样:
AJAX 请求 -> Closure -> CancelableDeferredInstance -> JQuery Deferred -> 组件
然后取消后它看起来像这样:
AJAX 请求 -> 关闭 -> CancelableDeferredInstance /object 引用现在为 null/ JQuery Deferred -> 组件
所以 AJAX 请求不再阻止组件被 GCd [假设我没有因为意外持有对延迟的引用而在某处搞砸了实现。 耶关闭....]
嗨@benmmurphy ,我对 JS 垃圾编译不是很熟悉,它可能与 React 的工作方式不同,但我有不同的理解。
makeCancellable
允许 React 组件在卸载时被垃圾回收。 我来解释一下。
makeCancellable 只是创建了另一个 promise,它最终持有对持有组件引用的函数的引用。 makeCancellable 解决方案只是将布尔属性 isMounted 移动到承诺中。
没有makeCancellable
:
handleError() {
if (this.isMounted()) {
console.log('ERROR')
}
}
使用makeCancellable
:
promise.then(...).fail((reason) => {
if (reason.isCancelled) return;
console.log('ERROR');
})
如果没有makeCancellable
您仍然拥有对this
的引用,因此组件在卸载时无法被垃圾回收。 但在另一种情况下,可取消承诺的失败处理程序会在组件卸载后立即调用,因此您不再有任何引用。
@vpontis
我有一些说明问题的 nodejs 代码。 一旦异步回调resolve
被设置为 null,Foo 组件才会被 GC 处理。 例如,假设您触发了一个需要 30 秒才能解决的 ajax 请求,然后组件被卸载。 那么该组件在 30 秒内不会被 GCd。 这是他们试图通过弃用 isMount() 来解决的问题之一。
npm install promise
npm install weak
node --expose-gc gc.js
first gc Foo {}
after first gc Foo {}
after resolve = null Foo {}
foo gc'd
after second gc {}
https://gist.github.com/benmmurphy/aaf35a44a6e8a1fbae1764ebed9917b6
编辑:
很抱歉和你说话,但我第一次读到这篇文章时,我不明白你想表达的意思,但现在我想我明白了。 我想你想说的是,因为错误回调不包含对组件的引用(或不遵循对组件的引用),那么该组件不被认为是由承诺引用的。 这实际上是真的。 嗯,第一部分是真的。 但是,这种推理存在问题:
1) 即使您示例中的错误处理程序没有对组件的引用, then()
回调通常也会。 例如then
句柄通常会执行this.setState(...)
。
2) 即使您的示例中的错误处理程序没有对大多数错误处理程序的组件的引用。 例如,他们会做这样的事情:
promise.then(...).fail((reason) => {
if (reason.isCancelled) return;
console.log('ERROR');
this.setState({error: true});
})
3) 即使我们知道代码不会跟随then()
回调并且我们知道它会在检查isCancelled
变量后退出函数,但 GC 不知道这一点。
并且在任何人使用我的示例或基于它的示例之前,请确保您测试它确实正确地进行了 GC。 我还没有测试过我的,如果它不起作用我也不会感到惊讶,因为我犯了一些愚蠢的错误:/
就承诺 API 而言,这对我在 nodejs 中的 GC 有效。 不过,我不希望在闭包附近有_resolve
、 _reject
参数,因为我不确定这是否可以保证根据 JS 规范工作,或者它是否恰好工作因为节点正在做一些优化。 实现能否捕获所有可见变量或仅捕获闭包中引用的变量? 我不知道也许真正了解 JS 的人可以插话解释:)
var makeCancelable = (promise) => {
let resolve;
let reject;
const wrappedPromise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
promise.then((val) => {
if (resolve != null) resolve(val)
});
promise.catch((error) => {
if (reject != null) reject(error)
});
});
return {
promise: wrappedPromise,
cancel() {
resolve = null;
reject = null;
},
};
};
isMounted 函数会在 16.0 中删除吗?
对@istarkov代码的小改进建议:
const makeCancelable = (promise) => {
let hasCanceled_ = false
promise.then((val) =>
hasCanceled_ ? reject({isCanceled: true}) : resolve(val)
)
.catch((error) =>
hasCanceled_ ? reject({isCanceled: true}) : reject(error)
)
return {
promise,
cancel() {
hasCanceled_ = true
}
}
}
只是新的承诺是多余的。
只是新的承诺是多余的。
@BnayaZil您正在调用resolve
和reject
函数,但不清楚它们来自哪里。 你的意思是Promise.resolve
和Promise.reject
吗? 在这种情况下,您仍然会返回一个新的 Promise。
几天前,DOM 规范中添加了一个新 API,允许您中止 fetch() 请求。 这个 API 还没有在任何浏览器中实现,但我已经为它创建了一个可在 NPM 上使用的 polyfill 是“abortcontroller-polyfill”。 polyfill 的作用与
详情在这里:
https://mo.github.io/2017/07/24/abort-fetch-abortcontroller-polyfill.html
由于React.createClass
在 React 16 中不再存在,并且新的create-react-class
包包含明确的isMounted
弃用消息,我将关闭它。
我同意@benmmurphy 的观点, @istarkov的解决方案实际上与使用isMounted()
因为它没有解决垃圾收集问题。
@benmmurphy的解决方案更接近,但将错误的变量Promise处理程序的引用。
关键是通过取消引用处理程序的闭包向上传递一个函数:
const makeCancelable = promise => {
let cancel = () => {};
const wrappedPromise = new Promise((resolve, reject) => {
cancel = () => {
resolve = null;
reject = null;
};
promise.then(
val => {
if (resolve) resolve(val);
},
error => {
if (reject) reject(error);
}
);
});
wrappedPromise.cancel = cancel;
return wrappedPromise;
};
关于为什么这个解决方案允许垃圾收集而不是以前的解决方案的进一步解释可以在这里找到。
我继续把它变成了一个 npm 包,可垃圾。 由于用例是 react,我制作了一个 HOC 组件,用于跟踪 promise 并在组件卸载时取消它们, trashable-react 。
编辑:我的错,我只是看了@hjlewis thrashable ,它也取消了承诺。 下面的模式仍然是 IMO 的一个小改进。
这些解决方案都没有取消承诺,可以通过吸收永久挂起的承诺而无需任何扩展就取消承诺。
function makeCancelable(promise) {
let active = true;
return {
cancel() {active = false},
promise: promise.then(
value => active ? value : new Promise(()=>{}),
reason => active ? reason : new Promise(()=>{})
)
}
}
// used as above:
const {promise, cancel} = makeCancelable(Promise.resolve("Hey!"))
promise.then((v) => console.log(v)) // never logs
cancel()
关于 GC 可能有一些微妙之处需要解决,而且咖啡还没有开始,但是这种模式确保返回的承诺确实被取消并且可以使其不泄漏(我过去已经实现过)。
@pygy谢谢回复!
不幸的是,您的解决方案仍然不允许垃圾收集。 您基本上只是重写了使用条件的@istarkov的解决方案。
您可以通过将此实现放入可回收垃圾并运行测试(垃圾收集测试失败)来轻松测试。
您的实现也无法正确处理错误。
现在是 2018 年,有没有比上面提到的更好的方法?
是的,您可以使用一些导航框架,这些框架的文档大小是 React Native 的两倍,但非常专业
这些“取消”承诺的片段并不是那么好恕我直言。 在原始承诺解决之前,取消的承诺仍然不会解决。 因此,如果您只是使用 isMounted 技巧,则内存清理不会发生。
一个合适的可取消承诺包装器必须使用第二个承诺和 Promise.race。 即Promise.race([originalPromise, cancelationPromise])
@benmmurphy的解决方案更接近,但将错误的变量Promise处理程序的引用。
我认为我的解决方案有效,但我对 javascript 运行时给出的承诺知之甚少。 如果您在测试工具中的节点下运行解决方案,它会正确地 GC 值。 我的解决方案将解析/拒绝函数分配给更高的范围,然后在调用取消时将这些值清零。 但是,这些函数在较低的范围内仍然可用,但没有被引用。 我认为现代 javascript 引擎不会在闭包中捕获变量,除非它们被引用。 我认为这曾经是一个大问题,人们会不小心造成 DOM 泄漏,因为他们做了以下事情: var element = findDOM(); element.addEventListener('click', function() {}); 即使没有在闭包中使用,元素也会在闭包中被引用。
@hjlewis @benmmurphy为什么我们需要取消引用处理程序?? 处理程序执行后,垃圾收集会以任何方式发生,对吗??
这些“取消”承诺的片段并不是那么好恕我直言。 在原始承诺解决之前,取消的承诺仍然不会解决。 因此,如果您只是使用 isMounted 技巧,则内存清理不会发生。
一个合适的可取消承诺包装器必须使用第二个承诺和 Promise.race。 即
Promise.race([originalPromise, cancelationPromise])
@hjlewis和我的你真的工作了吗,你可以用节点弱来验证它。 但是再看看它们,我同意它们都不是特殊的书面承诺代码。 作为承诺用户,您可能会期望在拒绝状态下解决“取消”承诺,但他们都不会这样做。 不过,可能在组件的情况下,这是一个更易于使用的解决方案,因为您不必编写额外的代码来忽略拒绝处理程序。
我认为一个特殊的可拒绝承诺会使用 Promise.race([]) 来构建一个可取消的承诺。 它起作用是因为当一个承诺被解决时,挂起的回调被删除,所以在这一点上将没有从浏览器网络到您的组件的引用链,因为在比赛承诺和组件之间将不再有引用。
我很好奇是否有可能将Promise.all()
与那些可取消的承诺一起使用并避免浏览器控制台中未捕获的错误......因为我只能捕获第一个取消错误,其他人仍未捕获。
现在是 2018 年,有没有比上面提到的更好的方法?
取消 Promise 执行的任何更好方法,即 setTimeout、API 调用等。现在是 2019 年 😭 😞
TC39 上有 Promise 取消线程,(我认为)它在这里很重要(也许……不确定)
https://github.com/tc39/proposal-cancellation/issues/24
取消 Promise 执行的任何更好方法,即 setTimeout、API 调用等。现在是 2019 年 😭 😞
我们是否正在寻找类似的东西
const promise = new Promise(r => setTimeout(r, 1000))
.then(() => console.log('resolved'))
.catch(()=> console.log('error'))
.canceled(() => console.log('canceled'));
// Cancel promise
promise.cancel();
最有用的评论
这个简单的方法可用于将取消添加到任何承诺
编辑:更新正确性/完整性。
如何使用