Javascript 在 ReactJS 中,为什么“setState”在同步调用时表现不同?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/28922275/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
In ReactJS, why does `setState` behave differently when called synchronously?
提问by levand
I am trying to understand the underlying cause for some somewhat "magical" behavior I am seeing that I cannot fully explain, and which is not apparent from reading the ReactJS source code.
我试图了解一些有点“神奇”的行为的根本原因,我发现我无法完全解释,这在阅读 ReactJS 源代码时并不明显。
When calling the setStatemethod synchronously in response to an onChangeevent on an input, everything works as expected. The "new" value of the input is already present, and so the DOM is not actually updated. This is highly desirable because it means the cursor will not jump to the end of the input box.
当setState同步调用该方法以响应onChange输入上的事件时,一切都按预期工作。输入的“新”值已经存在,因此实际上并未更新 DOM。这是非常可取的,因为这意味着光标不会跳到输入框的末尾。
However, when running a component with exactly the same structure but that calls setStateasynchronously, the "new" value of the input does not appear to be present, causing ReactJS to actually touch the DOM, which causes the cursor to jump to the end of the input.
但是,当运行一个结构完全相同但setState异步调用的组件时,输入的“新”值似乎不存在,导致 ReactJS 实际接触到 DOM,从而导致光标跳转到输入。
Apparently, something is intervening to "reset" the input back to its prior valuein the asynchronous case, which it is not doing in the synchronous case. What is this mechanic?
显然,value在异步情况下,某些东西正在干预以将输入“重置”回其先前的状态,而在同步情况下则没有这样做。这是什么机制?
Synchronous Example
同步示例
var synchronouslyUpdatingComponent =
React.createFactory(React.createClass({
getInitialState: function () {
return {value: "Hello"};
},
changeHandler: function (e) {
this.setState({value: e.target.value});
},
render: function () {
var valueToSet = this.state.value;
console.log("Rendering...");
console.log("Setting value:" + valueToSet);
if(this.isMounted()) {
console.log("Current value:" + this.getDOMNode().value);
}
return React.DOM.input({value: valueToSet,
onChange: this.changeHandler});
}
}));
Note that the code will log in the rendermethod, printing out the current valueof the actual DOM node.
请注意,该代码将登录该render方法,打印出value实际 DOM 节点的当前值。
When typing an "X" between the two Ls of "Hello", we see the following console output, and the cursor stays where expected:
在“Hello”的两个 L 之间键入“X”时,我们看到以下控制台输出,并且光标停留在预期位置:
Rendering...
Setting value:HelXlo
Current value:HelXlo
Asynchronous Example
异步示例
var asynchronouslyUpdatingComponent =
React.createFactory(React.createClass({
getInitialState: function () {
return {value: "Hello"};
},
changeHandler: function (e) {
var component = this;
var value = e.target.value;
window.setTimeout(function() {
component.setState({value: value});
});
},
render: function () {
var valueToSet = this.state.value;
console.log("Rendering...");
console.log("Setting value:" + valueToSet);
if(this.isMounted()) {
console.log("Current value:" + this.getDOMNode().value);
}
return React.DOM.input({value: valueToSet,
onChange: this.changeHandler});
}
}));
This is precisely the same as the above, except that the call to setStateis in a setTimeoutcallback.
这与上面的完全相同,除了调用setState是在setTimeout回调中。
In this case, typing an X between the two Ls yields the following console output, and the cursor jumps to the end of the input:
在这种情况下,在两个 L 之间键入 X 会产生以下控制台输出,并且光标会跳转到输入的末尾:
Rendering...
Setting value:HelXlo
Current value:Hello
Why is this?
为什么是这样?
I understand React's concept of a Controlled Component, and so it makes sense that user changes to the valueare ignored. But it looks like the valueis in fact changed, and then explicitly reset.
我理解 React 的Controlled Component概念,因此value忽略用户对 的更改是有道理的。但看起来value实际上已更改,然后显式重置。
Apparently, calling setStatesynchronously ensures that it takes effect beforethe reset, while calling setStateat any other time happens afterthe reset, forcing a re-render.
显然,setState同步调用确保它在重置之前生效,而setState在重置之后的任何其他时间调用,强制重新渲染。
Is this in fact what's happening?
这真的是正在发生的事情吗?
JS Bin Example
JS Bin 示例
回答by Brigand
Here's what's happening.
这就是正在发生的事情。
Synchronous
同步
- you press X
- input.value is 'HelXlo'
- you call
setState({value: 'HelXlo'}) - the virtual dom says the input value should be 'HelXlo'
- input.value is 'HelXlo'
- no action taken
- 你按 X
- input.value 是 'HelXlo'
- 你打电话
setState({value: 'HelXlo'}) - 虚拟 dom 说输入值应该是“HelXlo”
- input.value 是 'HelXlo'
- 不采取行动
Asynchronous
异步
- you press X
- input.value is 'HelXlo'
- you do nothing
- the virtual DOM says the input value should be 'Hello'
- react makes input.value 'Hello'.
- 你按 X
- input.value 是 'HelXlo'
- 你什么都不做
- 虚拟 DOM 说输入值应该是“你好”
- 反应使 input.value '你好'。
Later on...
稍后的...
- you
setState({value: 'HelXlo'}) - the virtual DOM says the input value should be 'HelXlo'
- react makes input.value 'HelXlo'
- the browser jumps the cursor to the end (it's a side effect of setting .value)
- 你
setState({value: 'HelXlo'}) - 虚拟 DOM 表示输入值应该是“HelXlo”
- 反应使 input.value 'HelXlo'
- 浏览器将光标跳转到末尾(这是设置 .value 的副作用)
Magic?
魔法?
Yes, there's a bit of magic here. React calls render synchronously after your event handler. This is necessary to avoid flickers.
是的,这里有一点魔法。React 在您的事件处理程序之后同步调用渲染。这是避免闪烁所必需的。
回答by Daniel Billingham
Using defaultValue rather than value resolved the issue for me. I'm unsure if this is the best solution though, for example:
使用 defaultValue 而不是 value 为我解决了这个问题。我不确定这是否是最好的解决方案,例如:
From:
从:
return React.DOM.input({value: valueToSet,
onChange: this.changeHandler});
To:
到:
return React.DOM.input({defaultValue: valueToSet,
onChange: this.changeHandler});
JS Bin Example
JS Bin 示例
回答by Damon
As mentioned, this will be an issue when using controlled components because React is updating the value of the input, rather than vice versa (React intercepts the change request and updates its state to match).
如前所述,当使用受控组件时,这将是一个问题,因为 React 正在更新输入的值,而不是反之(React 拦截更改请求并更新其状态以匹配)。
FakeRainBrigand's answer is great, but I have noticed that It's not entirely whether an update is synchronous or asynchronous that causes the input to behave this way. If you are doing something synchronously like applying a mask to modify the returned value it can also result in the cursor jumping to the end of the line. Unfortunately(?) this is just how React works with respect to controlled inputs. But it can be manually worked around.
FakeRainBrigand 的回答很好,但我注意到更新是同步的还是异步的并不完全是导致输入的行为方式。如果您正在同步执行某些操作,例如应用掩码来修改返回值,也可能导致光标跳到行尾。不幸的是(?)这就是 React 在受控输入方面的工作方式。但它可以手动解决。
There is a great explanation and discussion of thison the react github issues, which includes a link to a JSBin solutionby Sophie Alpert[that manually ensures the cursor remains where it ought to be]
有一个的这个很好的解释和讨论在反应github上的问题,其中包括一个链接到一个JSBin溶液通过柔阿尔珀特[即手动确保光标保持在那里它应该是]
This is achieved using an <Input>component like this:
这是使用这样的<Input>组件实现的:
var Input = React.createClass({
render: function() {
return <input ref="root" {...this.props} value={undefined} />;
},
componentDidUpdate: function(prevProps) {
var node = React.findDOMNode(this);
var oldLength = node.value.length;
var oldIdx = node.selectionStart;
node.value = this.props.value;
var newIdx = Math.max(0, node.value.length - oldLength + oldIdx);
node.selectionStart = node.selectionEnd = newIdx;
},
});
回答by Eric O'Connell
This is not exactly an answer, but one possible approach to mitigating the issue. It defines a wrapper for React inputs that manages value updates synchronously via a local state shim; and versions the outgoing values so that only the latest returned from asynchronous processing is ever applied.
这不完全是一个答案,而是一种缓解问题的可能方法。它为 React 输入定义了一个包装器,通过本地状态垫片同步管理值更新;并版本化传出值,以便只应用从异步处理返回的最新值。
It's based on some work by Stephen Sugden (https://github.com/grncdr) which I updated for modern React and improved by versioning the values, which eliminates the race condition.
它基于 Stephen Sugden ( https://github.com/grncdr) 的一些工作,我针对现代 React 进行了更新,并通过对值进行版本控制进行了改进,从而消除了竞争条件。
It's not beautiful :)
不漂亮:)
http://jsfiddle.net/yrmmbjm1/1/
http://jsfiddle.net/yrmmbjm1/1/
var AsyncInput = asyncInput('input');
Here is how components need to use it:
以下是组件需要如何使用它:
var AI = asyncInput('input');
var Test = React.createClass({
// the controlling component must track
// the version
change: function(e, i) {
var v = e.target.value;
setTimeout(function() {
this.setState({v: v, i: i});
}.bind(this), Math.floor(Math.random() * 100 + 50));
},
getInitialState: function() { return {v: ''}; },
render: function() {
{/* and pass it down to the controlled input, yuck */}
return <AI value={this.state.v} i={this.state.i} onChange={this.change} />
}
});
React.render(<Test />, document.body);
Another version that attempts to make the impact on the controlling component's code less obnoxious is here:
另一个试图减少对控制组件代码的影响的版本在这里:
http://jsfiddle.net/yrmmbjm1/4/
http://jsfiddle.net/yrmmbjm1/4/
That ends up looking like:
最终看起来像:
var AI = asyncInput('input');
var Test = React.createClass({
// the controlling component must send versionedValues
// back down to the input
change: function(e) {
var v = e.target.value;
var f = e.valueFactory;
setTimeout(function() {
this.setState({v: f(v)});
}.bind(this), Math.floor(Math.random() * 100 + 50));
},
getInitialState: function() { return {v: ''}; },
render: function() {
{/* and pass it down to the controlled input, yuck */}
return <AI value={this.state.v} onChange={this.change} />
}
});
React.render(<Test />, document.body);
ˉ\_(ツ)_/ˉ
ˉ\_(ツ)_/ˉ
回答by chmurson
I've had same issue when using Reflux. State was stored outside of a React Component which caused similar effect as wrapping setStateinside of a setTimeout.
我在使用 Reflux 时遇到了同样的问题。状态存储在 React 组件之外,这与包装setState在setTimeout.
@dule suggested, we should make our state change synchronous and asynchronous at the same time. So I've prepared an HOC that ensures that value change is synchronous - so it's cool to wrap input that suffers from asynchronous state change.
@dule 建议,我们应该同时使我们的状态更改同步和异步。所以我准备了一个 HOC 来确保值变化是同步的——所以包装受异步状态变化影响的输入是很酷的。
A note: this HOC will work for only for components that have similar to <input/>API, but I guess it's straightforward to make it more generic if there would be such need.
注意:这个 HOC 仅适用于具有类似于<input/>API 的组件,但我想如果有这样的需要,让它更通用是很简单的。
import React from 'react';
import debounce from 'debounce';
/**
* The HOC solves a problem with cursor being moved to the end of input while typing.
* This happens in case of controlled component, when setState part is executed asynchronously.
* @param {string|React.Component} Component
* @returns {SynchronousValueChanger}
*/
const synchronousValueChangerHOC = function(Component) {
class SynchronousValueChanger extends React.Component {
static propTypes = {
onChange: React.PropTypes.func,
value: React.PropTypes.string
};
constructor(props) {
super(props);
this.state = {
value: props.value
};
}
propagateOnChange = debounce(e => {
this.props.onChange(e);
}, onChangePropagationDelay);
onChange = (e) => {
this.setState({value: e.target.value});
e.persist();
this.propagateOnChange(e);
};
componentWillReceiveProps(nextProps) {
if (nextProps.value !== this.state.value) {
this.setState({value: nextProps.value});
}
}
render() {
return <Component {...this.props} value={this.state.value} onChange={this.onChange}/>;
}
}
return SynchronousValueChanger;
};
export default synchronousValueChangerHOC;
const onChangePropagationDelay = 250;
And then it can be used in such a way:
然后它可以以这样的方式使用:
const InputWithSynchronousValueChange = synchronousValueChangerHOC('input');
By making it HOC we can have it working for inputs, textarea and probably for others as well. Maybe the name is not the best one, so if anyone of you have a suggestion how to improve, let me know :)
通过使其成为 HOC,我们可以让它为输入、文本区域以及其他人工作。也许这个名字不是最好的,所以如果你们中的任何人有如何改进的建议,请告诉我:)
There is a hack with debounce, because sometimes, when typing was done really quickly the bug reappeared.
有一个去抖动的黑客,因为有时,当打字非常快时,错误又出现了。
回答by Mitch VanDuyn
We have a similar issue and in our case we have to use asyncronous state updates.
我们有一个类似的问题,在我们的例子中,我们必须使用异步状态更新。
So we use defaultValue, andadd a keyparam to the input associated with the model that the input is reflecting. This insures that for any model the input will remain syncronized to the model, but if the actual model changes will force a new input to be generated.
因此,我们使用 defaultValue,并向key与输入所反映的模型关联的输入添加一个参数。这确保对于任何模型,输入将保持与模型同步,但如果实际模型更改将强制生成新输入。

