[英]In ReactJS, why does `setState` behave differently when called synchronously?
我試圖理解一些有點“神奇”行為的根本原因,我看到我無法完全解釋,而且從閱讀ReactJS源代碼中看不出來。
當響應輸入上的onChange
事件同步調用setState
方法時,一切都按預期工作。 輸入的“新”值已經存在,因此DOM實際上沒有更新。 這是非常需要的,因為它意味着光標不會跳轉到輸入框的末尾。
但是,當運行具有完全相同結構但異步調用setState
的組件時,輸入的“new”值似乎不存在,導致ReactJS實際觸摸DOM,這導致光標跳轉到結尾輸入。
顯然,有些事情正在干預,以便在異步情況下將輸入“重置”回其先前value
,而在同步情況下則不會這樣做。 這是什么機制?
同步示例
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});
}
}));
請注意,代碼將登錄render
方法,打印出實際DOM節點的當前value
。
在兩個Ls“Hello”之間鍵入“X”時,我們看到以下控制台輸出,並且光標保持在預期的位置:
Rendering...
Setting value:HelXlo
Current value:HelXlo
異步示例
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});
}
}));
這與上面的完全相同,只是對setState
的調用是在setTimeout
回調中。
在這種情況下,在兩個Ls之間鍵入X會產生以下控制台輸出,並且光標會跳轉到輸入的末尾:
Rendering...
Setting value:HelXlo
Current value:Hello
為什么是這樣?
我理解React的受控組件概念,因此忽略用戶對value
更改是有道理的。 但看起來該value
實際上已更改,然后顯式重置。
顯然,同步調用setState
可確保它在重置之前生效,而在重置之后的任何其他時間調用setState
會強制重新呈現。
事實上這是怎么回事?
JS Bin示例
這是正在發生的事情。
setState({value: 'HelXlo'})
稍后的...
setState({value: 'HelXlo'})
是的,這里有一些魔力。 React調用在事件處理程序之后同步呈現。 這是避免閃爍的必要條件。
使用defaultValue而不是value解決了我的問題。 我不確定這是否是最佳解決方案,例如:
從:
return React.DOM.input({value: valueToSet,
onChange: this.changeHandler});
至:
return React.DOM.input({defaultValue: valueToSet,
onChange: this.changeHandler});
JS Bin示例
如上所述,這將是使用受控組件時的問題,因為React正在更新輸入的值,而不是相反(React攔截更改請求並更新其狀態以匹配)。
FakeRainBrigand的答案很棒,但我注意到,更新是同步還是異步並不是導致輸入以這種方式運行。 如果您正在執行某些操作,例如應用掩碼來修改返回的值,則還可能導致光標跳到行尾。 不幸的是(?)這就是React在受控輸入方面的工作原理。 但它可以手動解決。
關於反應github問題有一個很好的解釋和討論 ,其中包括Sophie Alpert的一個JSBin解決方案的鏈接[手動確保光標保持在它應該的位置]
這是使用這樣的<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;
},
});
這不是一個答案,而是減輕問題的一種可能方法。 它為React輸入定義了一個包裝器,它通過本地狀態Shim同步管理值更新; 並對傳出值進行版本化,以便僅應用從異步處理返回的最新值。
它基於Stephen Sugden( https://github.com/grncdr )的一些工作,我更新了現代React並通過版本化值進行了改進,從而消除了競爭條件。
它不漂亮:)
http://jsfiddle.net/yrmmbjm1/1/
var AsyncInput = asyncInput('input');
以下是組件需要如何使用它:
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);
試圖使控制組件代碼的影響不那么令人討厭的另一個版本在這里:
http://jsfiddle.net/yrmmbjm1/4/
最終看起來像:
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);
¯\\ _(ツ)_ /¯
使用Reflux時我遇到了同樣的問題。 狀態存儲在React組件之外,這導致與setTimeout
setState
包裝類似的效果。
@dule建議,我們應該同時使狀態更改同步和異步。 所以我已經准備好了一個HOC來確保值的變化是同步的 - 所以包裝遭受異步狀態變化的輸入很酷。
注意:此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;
然后它可以這樣使用:
const InputWithSynchronousValueChange = synchronousValueChangerHOC('input');
通過將其作為HOC,我們可以讓它為輸入,textarea和其他人工作。 也許這個名字不是最好的,所以如果你們有人建議如何改進,請告訴我:)
有一個破壞的黑客攻擊,因為有時,當打字很快完成時,錯誤再次出現。
我們有類似的問題,在我們的例子中,我們必須使用異步狀態更新。
所以我們使用默認值, 以及添加key
參數去與輸入被反映模型相關聯的輸入。 這確保了對於任何模型,輸入將保持與模型同步,但如果實際模型更改將強制生成新輸入。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.