【问题标题】:React: Update Child Component Without Rerendering ParentReact:更新子组件而不重新渲染父组件
【发布时间】:2019-01-31 17:38:55
【问题描述】:

下面是一个简单的例子:

const { Component } = React
const { render } = ReactDOM

const Label = ({ text }) => (
  <p>{text}</p>
)

const Clock = ({ date }) => (
  <div>{date.toLocaleTimeString()}</div>
)

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      date: new Date()
    }
  }
  
  componentDidMount() {
    this.interval = setInterval(
      () => this.setState({ date: new Date() }),
      1000
    )
  }
  
  componentWillUnmount() {
    clearInterval(this.interval)
  }
  
  updateTime() {
    
  }
  
  render() {
    return (
      <div>
        <Label text="The current time is:" />
        <Clock date={this.state.date} />
      </div>
    )
  }
  
}

render(<App />, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

<div id="app"></div>

this.setState({ date: new Date() }) 每秒被调用一次,用当前时间更新时钟。据我所知,setState 在 App 上调用了 render 方法,这会导致整个组件(包括标签)被重新渲染。

有没有办法在不重新渲染整个 App 组件的情况下将日期传递给 Clock(导致它被重新渲染)?这对性能有多大影响?

【问题讨论】:

  • 为 Clock 组件提供自己的内部状态,而不是将其作为父级的 prop 传递。
  • 首先不建议在 setTimeout 或 setInterval 中执行 setState
  • 调用的渲染函数和实际更改的 DOM 之间存在差异。 react 的全部意义在于它渲染到它的虚拟 DOM 中,然后将其与之前的内容进行比较,并且只更新需要的内容。这称为和解。在您的示例中,标签不会被重新渲染,因为它没有改变,但时钟会。
  • 否,默认情况下,如果父级重新渲染所有子级重新渲染。 @ChrisCousins 可能你想告诉时钟组件中不需要 DOM 操作。
  • 有一个组件生命周期方法shouldcomponentupdate,以防您希望停止重新渲染特定组件

标签: javascript reactjs


【解决方案1】:

这不可能是你想要的。要将道具传递给子组件,父组件的状态或道具应该以某种方式改变。如您所知,这显然会触发重新渲染,因此所有孩子都会重新渲染。要更新,您的 Clock 组件应在这种情况下重新渲染和卸载/重新安装,以反映 DOM 更改。

如果您的应用程序不是那么大并且没有那么多孩子,请不要为这个问题而烦恼,因为渲染并不那么昂贵。代价高昂的是组件的 DOM 操作。在这里,React 区分真实和虚拟 DOM,即使重新渲染也不会卸载/重新安装 Label 组件。但是,如果您将Label 组件编写为PureComponent,它不会重新渲染。但是Clock组件要这样更新,就没办法了。

class Label extends React.PureComponent {
  render() {
    console.log("rendered");
    return (<p>{this.props.text}</p>)
  }
}

const Clock = ({ date }) => (
  <div>{date.toLocaleTimeString()}</div>
)

class App extends React.Component {

  constructor() {
    super()
    this.state = {
      date: new Date()
    }
  }


  componentWillMount() {
    this.interval = setInterval(
      () => this.setState({ date: new Date() }),
      1000
    )
  }

  componentWillUnmount() {
    clearInterval(this.interval)
  }

  updateTime() {

  }

  render() {
    return (
      <div>
        <Label text="The current time is:" />
        <Clock date={this.state.date} />
      </div>
    )
  }

}

【讨论】:

  • 有没有办法避免通过父母传播?我有高度嵌套的结构 (SVG),我知道只有某些孩子会因为状态的变化而受到影响。目前我可以看到由于向下钻取而导致的明显延迟。
  • @NikunjMadhogaria,如果我没听错,您正在寻找React.PureComponent
  • 刚刚尝试快速将 Component 替换为 PureComponent,功能停止工作。我只有一个被递归调用的组件。我的状态有关于孩子(一个他们的孩子)的所有细节,包括标签、属性等。我们想看看你在切换到 React 时是否会获得性能和代码可维护性。但是,由于这种递归,它似乎比普通 HTML(具有不同变量的所有 CSS 规则)慢。
  • 我们有像 body[x="1"] #id1 = {...style 1}, body[x="2"] #id1 = {...style 2} 这样的纯 CSS 规则。这目前比 React 中的递归方法更快。
  • 性能是我们应用程序的重要指标之一。我们有两个解决方案:第一个是纯 CSS,没有任何类型的 javascript,第二个是使用 React。如果纯 CSS 需要 200 毫秒进行更新,React 需要 2000 毫秒进行相同的更新(因为它在 javascript 中有额外的计算)。
【解决方案2】:

当你想更新一个子节点而不更新父节点时,状态必须在子节点中。您可以将状态 getter / setter 从子级传递给父级,以便能够读取和更新它:

function Child({onMount}) {
  const [value, setValue] = useState(0);

  useEffect(() => {
    onMount([value, setValue]);
  }, [onMount, value]);


  return (
    <div>
      {value}
    </div>    
  );
};


function Parent() {

  let value = null;
  let setValue = null;
  
  const onChildMount = (dataFromChild) => {
    value = dataFromChild[0];
    setValue = dataFromChild[1];
  };

  // Call setValue to update child without updating parent

  return (
    <div>
      <Child onMount={onChildMount}/>
    </div>    
  );
};

由于const [value, setValue] = useState(0);Child中,更新值时只有子组件会重新渲染。此外,由于ParentonChildMount 处接收valuesetValue,因此父级可以使用它们来更新子级,而无需重新渲染父级。

【讨论】:

  • 谢谢,这很好用。我所做的唯一更改是不必将value 传递给父级,因此onMount(setValue); 就足够了。
【解决方案3】:

使用子组件的引用可以实现:

export const parentA = () => {
  let ref = useRef<View>(null);
  const updateChild () {
    ref.current.props.setNativeProps({style:{backgroundColor: 'gray',}})
  }
  return (
    <childA>
      <Text>Test</Text>
    </childA>
  );
};

export const childA = () => {
  return <View style={{backgroundColor: 'green',}}></View>;
};

【讨论】: