当元素在 DOM 中的位置发生变化时,是否可以让 React 移动元素而不是重新创建它?

IT技术 javascript reactjs
2021-04-29 11:27:43

当元素在 DOM 中的位置发生变化时,是否可以让 React 移动元素而不是重新创建它?

假设我正在制作一个 2 窗格组件,并且我希望能够隐藏/取消隐藏一个窗格。让我们也想象一下窗格本身很重。就我而言,每个窗格都有 2000 多个元素。

在我的实际代码中,当有 2 个窗格时,我使用了拆分器。为了只显示一个窗格,我需要删除拆分器并将其替换为 div。

下面的代码模拟了这一点。如果有一个窗格,它使用 adiv来包含该窗格。如果有 2 个窗格,它用于pre包含它们。在我的情况下,它会有div1 次疼痛和splitter2 次疼痛

因此,通过检测,document.createElement我发现不仅创建了容器,而且还重新创建了内部元素。换句话说,在我的代码中,当从 splitter->div 转到 2000+ 元素窗格时,将完全重新创建,这很慢。

有没有办法有效地告诉 React。“喂,不要重新创建这个组件,只是移动它?”

class TwoPanes extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    const panes = this.renderPanes();
    if (panes.length === 2) {
      return React.createElement('pre', {className: "panes"}, panes);
    } else {
      return React.createElement('div', {className: "panes"}, panes);
    }
  }
  renderPanes() {
    return this.props.panes.map(pane => {
      return React.createElement('div', {className: "pane"}, pane);
    });
  }
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      panes: [
        "pane one",
        "pane two",
      ],
    };
  }
  render() {
    const panes = React.createElement(TwoPanes, {panes: this.state.panes}, null);
    const button = React.createElement('button', {
      onClick: () => {
        const panes = this.state.panes.slice();
        if (panes.length === 1) {
          panes.splice(0, 0, "pane one");  // insert pane 1
        } else {
          panes.splice(0, 1);  // remove pane 1
        }
        this.setState({panes: panes});
      },
    }, "toggle pane one");
    return React.createElement('div', {className: "outer"}, [panes, button]);
  }
}

// wrap document.createElement so we can see if new elements are created
// vs reused
document.createElement = (function(oldFn) {
  let count = 0;
  let inside = false;
  return function(type, ...args) {
    if (!inside) {  // needed because SO's console wrapper calls createElement
      inside = true;  
      console.log(++count, "created:", type);
      inside = false;
    }
    return oldFn.call(this, type, ...args);
  }
}(document.createElement));
  
  
ReactDOM.render(
  React.createElement(App, {}, null),
  document.getElementById('root')        
);
html { box-sizing: border-box; }
*, *:before, *:after { box-sizing: inherit; }
body { margin: 0; }
#root { width: 100vw; height: 100vh; }
.outer { 
  width: 100%; 
  height: 100%; 
}
.panes { 
  width: 100%; 
  height: 100%; 
  display: flex;
  flow-direction: row;
  justify-content: space-between;
}
.pane {
  flex: 1 1 auto;
  border: 1px solid black;
}
button {
  position: absolute;
  left: 10px;
  top: 30px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

1个回答

我不认为有一种方法可以绕过DOM树木,即使有 - 它会非常昂贵,因为

  • React 的 diff 算法仍然需要比较不同的树,如果它偶然发现具有不同结构的子树/节点,它会立即丢弃旧的。这是 diff 算法为了运行而做出的假设之一O(n)

两个不同类型的元素会产生不同的树

  • 为了移动缓存的DOM,您首先需要将其从树中分离出来,这意味着稍后仍需要重新应用它,这是一个瓶颈。插入HTMLDOM非常昂贵,即使缓存/分段。

我的建议是使用CSS,因为display: none/display: block比重新应用缓存快得多DOM

class TwoPanes extends React.Component {

  render() {
    return (
      <div> 
       <Pane1 />
       <Pane2 style={this.state.panes.length === 2 ? {} : {display: 'none'} } />
     </div>
    );
  }
}