ReactJS - 每次调用“setState”时都会调用渲染吗?

IT技术 javascript reactjs
2021-01-18 12:43:48

React 是否每次setState()调用时都重新渲染所有组件和子组件

如果是这样,为什么?我认为这个想法是 React 只在需要的时候渲染——当状态改变时。

在下面的简单示例中,当单击文本时,两个类都会再次呈现,尽管状态在后续单击时不会更改,因为 onClick 处理程序始终将 设置state为相同的值:

this.setState({'test':'me'});

我原以为只有在state数据发生变化时才会进行渲染

这是示例的代码,作为 JS Fiddle和嵌入式代码段:

var TimeInChild = React.createClass({
    render: function() {
        var t = new Date().getTime();

        return (
            <p>Time in child:{t}</p>
        );
    }
});

var Main = React.createClass({
    onTest: function() {
        this.setState({'test':'me'});
    },

    render: function() {
        var currentTime = new Date().getTime();

        return (
            <div onClick={this.onTest}>
            <p>Time in main:{currentTime}</p>
            <p>Click me to update time</p>
            <TimeInChild/>
            </div>
        );
    }
});

ReactDOM.render(<Main/>, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.0/react-dom.min.js"></script>

6个回答

每次调用 setState 时,React 是否会重新渲染所有组件和子组件?

默认情况下 - 是的。

有一个方法boolean shouldComponentUpdate(object nextProps, object nextState),每个组件都有这个方法,它负责确定“组件是否应该更新(运行渲染函数)?” 每次更改状态从父组件传递新props时

您可以为组件编写自己的shouldComponentUpdate方法实现,但默认实现始终返回 true - 意味着始终重新运行渲染函数。

引用官方文档http://facebook.github.io/react/docs/component-specs.html#updating-shouldcomponentupdate

默认情况下,shouldComponentUpdate 总是返回 true 以防止在状态发生变化时出现细微的错误,但是如果您小心地始终将状态视为不可变的并且在 render() 中从 props 和 state 中只读,那么您可以使用一个将旧的 props 和 state 与其替换进行比较的实现。

你问题的下一部分:

如果是这样,为什么?我认为这个想法是 React 只在需要的时候渲染——当状态改变时。

我们可以称之为“渲染”有两个步骤:

  1. 虚拟 DOM 渲染:当调用render方法时,它会返回组件的新虚拟 dom结构。正如我之前提到的,当你调用setState()时总是会调用这个渲染方法,因为默认情况下shouldComponentUpdate总是返回 true。因此,默认情况下,React 中没有优化。

  2. 原生 DOM 渲染:React 仅在虚拟 DOM 中更改真实 DOM 节点时才会更改浏览器中的真实 DOM 节点,并且根据需要尽可能少地更改 - 这是 React 的强大功能,它优化了真实 DOM 突变并使 React 快速。

另外,研究一下使用 React.PureComponent ( reactjs.org/docs/react-api.html#reactpurecomponent )。如果组件的状态或道具实际发生了变化,它只会更新(重新渲染)。但是请注意,这种比较是肤浅的。
2021-03-19 12:43:48
很好的解释!在这种情况下,真正让 React 仍然大放异彩的一件事是,除非虚拟 DOM 已更改,否则它不会更改真实DOM。因此,如果我的渲染函数连续 2 次返回相同的内容,则真正的 DOM 根本不会改变...谢谢!
2021-03-25 12:43:48
如果您的状态包含一个相对较大的 javascript 对象(总共约 1k 个属性),它被渲染为一个大的组件树(总共约 100 个)......您应该让渲染函数构建虚拟 dom,还是应该在设置之前状态,手动将新状态与旧状态进行比较,只有setState在检测到差异时才调用如果是这样,如何最好地做到这一点 - 比较 json 字符串,构造和比较对象哈希,...?
2021-03-28 12:43:48
@Petr ,但是即使 react 重建了虚拟 dom,如果旧的虚拟 dom 与新的虚拟 dom 相同,浏览器 dom 也不会被触及,不是吗?
2021-04-07 12:43:48
我认为@tungd是对的 - 请参阅下面的回答。这也是组件之间的父子关系的问题。例如,如果TimeInChildMain的兄弟,那么它的render()将不会被不必要地调用。
2021-04-09 12:43:48

不,React 不会在状态改变时渲染所有内容。

  • 每当组件变脏(其状态发生变化)时,该组件及其子组件都会重新渲染。这在某种程度上是尽可能少地重新渲染。唯一不调用渲染的时间是将某个分支移动到另一个根时,理论上我们不需要重新渲染任何东西。在您的示例中,TimeInChild是 的子组件Main,因此当状态Main更改时它也会重新呈现

  • React 不比较状态数据。setState被调用时,它将组件标记为脏(这意味着它需要重新渲染)。需要注意的重要一点是,虽然render调用了组件的方法,但只有当输出与当前 DOM 树不同(也就是虚拟 DOM 树和文档的 DOM 树之间的差异)时,才会更新真正的 DOM。在您的示例中,即使state数据没有更改,上次更改的时间也发生了变化,这使得虚拟 DOM 与文档的 DOM 不同,因此更新 HTML 的原因。

@tungd 你能解释一下格林的问题吗 what does it mean some branch is moved to another root? What do you call branch? What do you call root?
2021-03-21 12:43:48
我真的更喜欢标记为正确答案的答案。我理解您对原生“真实”方面的“渲染”的解释...... 幸运的是,它足够聪明,可以确定真正改变了什么,并且只更新这些东西。这个答案可能会让新用户感到困惑,首先你说不,然后你解释说事情确实得到了渲染......
2021-03-25 12:43:48
是的,这是正确的答案。作为实验,将最后一行修改为React.renderComponent(<div><Main/><TimeInChild/></div>, document.body)并除去<TimeInChild />从所述主体呈现()的的主要组件。渲染()TimeInChild默认情况下不会被调用,因为它不是一个孩子了。
2021-03-30 12:43:48
谢谢,像这样的东西有点棘手,这就是为什么 React 作者建议该render()方法应该是“纯”的 - 独立于外部状态。
2021-04-02 12:43:48
@tungd,这是什么意思some branch is moved to another root你叫branch什么?你叫root什么?
2021-04-06 12:43:48

即使在这里的许多其他答案中都有说明,该组件也应该:

  • 实现shouldComponentUpdate仅在状态或属性更改时呈现

  • 切换到扩展PureComponent,它已经在shouldComponentUpdate内部实现了一个用于浅层比较方法。

这是一个使用 的示例shouldComponentUpdate,它仅适用于这个简单的用例和演示目的。使用此选项时,组件不再在每次单击时重新渲染自身,并在首次显示和单击一次后渲染。

var TimeInChild = React.createClass({
    render: function() {
        var t = new Date().getTime();

        return (
            <p>Time in child:{t}</p>
        );
    }
});

var Main = React.createClass({
    onTest: function() {
        this.setState({'test':'me'});
    },

    shouldComponentUpdate: function(nextProps, nextState) {
      if (this.state == null)
        return true;
  
      if (this.state.test == nextState.test)
        return false;
        
      return true;
  },

    render: function() {
        var currentTime = new Date().getTime();

        return (
            <div onClick={this.onTest}>
            <p>Time in main:{currentTime}</p>
            <p>Click me to update time</p>
            <TimeInChild/>
            </div>
        );
    }
});

ReactDOM.render(<Main/>, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.0/react-dom.min.js"></script>

是的。它只在render()我们每次调用时调用该方法,setState除非shouldComponentUpdate返回false

请再详细点
2021-04-06 12:43:48

使用 React 钩子时,似乎已接受的答案不再是这种情况(使用原始值,有关详细信息,请参阅对此答案的评论)你可以在这个代码沙箱中看到,当状态设置为相同的值时,类组件会被重新渲染,而在函数组件中,将状态设置为相同的值不会导致重新渲染。

https://codesandbox.io/s/still-wave-wouk2?file=/src/App.js

有道理,谢谢。我在答案中添加了一个小说明
2021-03-17 12:43:48
那是因为在您的示例中,您将原始值传递给 useState 钩子,并将复杂对象传递给 setState 方法。尝试将对象或数组传递给 useState 钩子以查看函数组件中的重新渲染,因为您正在更改对象引用。只是为了澄清你的例子,但你是对的,使用带有原始值的 useState 钩子可以防止重新渲染。
2021-03-20 12:43:48
当然,我刚刚解释了为什么 useState 在你的例子中没有重新渲染。有人可能会将一个对象传递给 useState(这是合法的),认为钩子会自动阻止重新渲染,因为你说“使用 React 钩子时,接受的答案不再是这种情况”。有人可能不会马上发现使用原语和对象引用的区别。
2021-03-24 12:43:48
@MichalSvorc 确实,如果我们在 useState 中使用对象,那么它也会重新渲染,但关键是不可能将简单的原语与类组件的setState功能一起使用。该示例仅显示使用最简单形式的类组件状态和最简单形式的 useState 之间的区别,以及 hooks 版本如何(稍微)更高效👍
2021-04-07 12:43:48