簡體   English   中英

在ReactJS中,為什么`setState`在同步調用時表現不同?

[英]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示例

http://jsbin.com/sogunutoyi/1/

這是正在發生的事情。

同步

  • 你按X.
  • input.value是'HelXlo'
  • 你調用setState({value: 'HelXlo'})
  • 虛擬dom說輸入值應為'HelXlo'
  • input.value是'HelXlo'
    • 不采取行動

異步

  • 你按X.
  • input.value是'HelXlo'
  • 你什么都不做
  • 虛擬DOM表示輸入值應為'Hello'
    • react使input.value'Hello'。

稍后的...

  • setState({value: 'HelXlo'})
  • 虛擬DOM表示輸入值應為'HelXlo'
    • 反應使輸入值。'HelXlo'
    • 瀏覽器將光標跳到最后(這是設置.value的副作用)

魔法?

是的,這里有一些魔力。 React調用在事件處理程序之后同步呈現。 這是避免閃爍的必要條件。

使用defaultValue而不是value解決了我的問題。 我不確定這是否是最佳解決方案,例如:

從:

return React.DOM.input({value: valueToSet,
    onChange: this.changeHandler});

至:

return React.DOM.input({defaultValue: valueToSet,
    onChange: this.changeHandler});

JS Bin示例

http://jsbin.com/xusefuyucu/edit?js,output

如上所述,這將是使用受控組件時的問題,因為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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM