更改 table 结构会导致 React 失去对 DOM 的跟踪

Changing table structure causes React to lose track of the DOM

我正在尝试在 React 中创建一个 table 组件,其中的列和行都是动态的并且会随时间变化。但是,当数据发生变化时,React 会抛出意外的 DOM 突变不变性违规。

这里是 fiddle 演示问题 http://jsfiddle.net/69z2wepo/1797/。如您所见,初始渲染后数据发生变化,React 无法再跟踪 DOM.

的状态

代码在这里:

var Table = React.createClass({
    getDefaultProps: function() {
      return {
        data: [
          {
            colA: { value: 'foo' },
          },
        ],
      };
    },
    render: function() {
        return (
          <table>
            <thead>
              <tr> {
                Object.keys(this.props.data[0]).map(function(key) {
                  return (<th>{ key }</th>);
                }.bind(this))
              } </tr>
            </thead>
            <tbody> {
              this.props.data.map(function(item) {
                return (<tr> { Object.keys(item).map(function(key) {
                  return (
                    <td>{ item[key] }</td>
                  );
                }.bind(this)) } </tr>);
              }.bind(this))    
            } </tbody>
          </table>
        );
    }
 });

var Main = React.createClass({
  componentWillMount: function() {
    setTimeout(function() {
      var data = [
        {
          colA: {
            value: 'foobar',
          },
        },
      ];
      this.setState({
        data: data,
      });
    }.bind(this), 3000);
  },
  getInitialState: function() {
    var data = [
      {
        colA: {
          value: 'foo',
        },
        colB: {
          value: 'bar',
        }
      },
      {
        colA: {
          value: 'foo',
        },
        colB: {
          value: 'bar',
        }
      }
    ];

    return {
      data: data,
    };
  },
  render: function() {
    return (<Table data={ this.state.data } />);
  },
});

React.render(<Main />, document.body);
console.log(React.renderToString(<Table/>));

我已经尝试了所有添加关键属性以跟踪各种元素的方法,但似乎无法解决这个问题。

使用 renderToString 渲染 table 组件显示 React 正在 table 的不同级别插入一堆元素。这是可能的原因吗?请参阅此处呈现的 DOM:

<table data-reactid=".1" data-react-checksum="1098817301">
  <thead data-reactid=".1.0">
    <tr data-reactid=".1.0.0">
      <span data-reactid=".1.0.0.0"> </span>
      <th data-reactid=".1.0.0.1:0">colA</th>
      <span data-reactid=".1.0.0.2"> </span>
    </tr>
  </thead>
  <tbody data-reactid=".1.1">
    <span data-reactid=".1.1.0"> </span>
    <tr data-reactid=".1.1.1:0">
      <span data-reactid=".1.1.1:0.0"> </span>
      <td data-reactid=".1.1.1:0.1:0"><span data-reactid=".1.1.1:0.1:0.$value:0">foo</span></td>
      <span data-reactid=".1.1.1:0.2"> </span>
    </tr>
    <span data-reactid=".1.1.2"> </span>
  </tbody>
</table>

原来问题出在你的缩进上。如果你在一个新行开始花括号 {}(花括号在 JSX 中写 javascript),你的代码就可以工作。不知道为什么会这样。

jsfiddle:http://jsfiddle.net/cwn2nebs/

var Table = React.createClass({
    getDefaultProps: function() {
      return {
        data: [
          {
            colA: { value: 'foo' },
          },
        ],
      };
    },
    render: function() {
        return (
          <table>
            <thead>
              <tr> 
              {
                Object.keys(this.props.data[0]).map(function(key, idx) {
                  return (<th key={ idx } >{ key }</th>);
                }.bind(this))
              } 
              </tr>
            </thead>
            <tbody> 
            {
              this.props.data.map(function(item, idx) {
                return (
                <tr key={ idx }> 
                { 
                  Object.keys(item).map(function(key, i) {
                      return (
                        <td key={ i }>{ item[key] }</td>
                      );
                  }.bind(this)) } 
                </tr>);
              }.bind(this))

            } 
            </tbody>
          </table>
        );
    }
});

var Main = React.createClass({
  componentWillMount: function() {
    setTimeout(function() {
      var data = [
        {
          colA: {
            value: 'foobar',
          }
        },
      ];
      this.setState({
        data: data,
      });
    }.bind(this), 3000);
  },
  getInitialState: function() {
    var data = [
      {
        colA: {
          value: 'foo',
        },
        colB: {
          value: 'bar',
        }
      },
      {
        colA: {
          value: 'foo',
        },
        colB: {
          value: 'bar',
        }
      }
    ];

    return {
      data: data,
    };
  },
  render: function() {
    return (<Table data={ this.state.data } />);
  },
});

React.render(<Main />, document.body);

更新

使用 JSX compiler 我尝试将您的部分代码转换为纯 JS:

 render: function() {
        return (
          React.createElement("table", null, 
            React.createElement("thead", null, 
              React.createElement("tr", null, " ", 
                Object.keys(this.props.data[0]).map(function(key, idx) {
                  return (React.createElement("th", {key: idx }, key ));
                }.bind(this)), 
              " ")
            ), 
            React.createElement("tbody", null, " ", 
              this.props.data.map(function(item, idx) {
                return (React.createElement("tr", {key: idx }, " ",  Object.keys(item).map(function(key, i) {
                  return (
                    React.createElement("td", {key: i },  item[key] )
                  );
                }.bind(this)), " "));
              }.bind(this)), 

            " ")
          )
        );
    }

这就是 React.createElement 的工作原理:

React.createElement(type, props, children);

注意 tr 元素的空 children:

React.createElement("tr", null, " ", 
    Object.keys(this.props.data[0]).map(function(key, idx) {
        return (React.createElement("th", {key: idx }, key ));
    }.bind(this)), 
 " ")

但是换行加上花括号,编译后的代码是这样的(没有空(" ")children):

React.createElement("tr", null,     
    Object.keys(this.props.data[0]).map(function(key, idx) {
        return (React.createElement("th", {key: idx }, key ));
    }.bind(this))
)

我相信 React 将 " " children 转换为 span 元素,正如您所发现的那样,这是问题所在。