ReactJS的重新渲染规则是什么?只有组件的状态发生变化时,整个子树才会重新渲染吗?

4
首先,“重新渲染”在这里指的是:
  1. 调用任何类组件的render()方法,或
  2. 调用函数组件的函数。
我们把实际DOM中发生变化的元素称为“刷新”,以将其与“重新渲染”区分开来。
“重新渲染”的规则是否就如此简单呢?

当组件的任何状态发生改变时,该组件及从该组件向下的所有子树都将被重新渲染。

像这样就行了吗?例如:

function A() {
  console.log("Component A re-render");

  return <div>Component A says Hello World</div>;
}

function App() {
  const [counter, setCounter] = React.useState(0);

  console.log("Component App re-render");

  function increaseCount() {
    setCounter(c => c + 1);
  }

  return (
    <div>
      {counter}
      <button onClick={increaseCount} >Increment</button>
      <A />
    </div>
  )
}

ReactDOM.render(<App />, document.querySelector("#root"));
<script src="https://unpkg.com/react@16.12.0/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16.12.0/umd/react-dom.development.js" crossorigin></script>


<div id="root"></div>

组件A非常简单:它甚至不需要任何props,仅输出静态文本,但是每次仍然被调用(通过查看console.log()输出可以看到)。

但是即使它被“重新渲染”,实际的DOM元素也没有被“刷新”,如Google Chrome的Inspect Element所示,对于组件A,DOM元素没有闪烁,但只有计数器数字在闪烁。

那么它是如何工作的呢?

  1. 无论何时更改组件的任何状态,该组件及其整个子树都将被“重新渲染”。
  2. 但是ReactJS将使用那些JSX构建的“虚拟DOM”的内容与实际DOM的内容进行“协调”,如果内容不同,则“刷新实际DOM”。

但是话虽如此,似乎ReactJS并没有真正与实际DOM协调,而是与可能的“先前虚拟DOM”协调。为什么? 因为如果我在3秒后使用setTimeout()将组件A的实际DOM更改为其他内容,并单击按钮,ReactJS不会将组件A的内容更改回“Hello World”。例如:

function A() {
  console.log("Component A re-render");

  return <div id="foo">Component A says Hello World</div>;
}

function App() {
  const [counter, setCounter] = React.useState(0);

  console.log("Component App re-render");

  function increaseCount() {
    setCounter(c => c + 1);
  }

  return (
    <div>
      {counter}
      <button onClick={increaseCount} >Increment</button>
      <A />
    </div>
  )
}

ReactDOM.render(<App />, document.querySelector("#root"));

setTimeout(function() {
  document.querySelector("#foo").innerText = "hi"
}, 3000);
<script src="https://unpkg.com/react@16.12.0/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16.12.0/umd/react-dom.development.js" crossorigin></script>


<div id="root"></div>


我真的很好奇,为什么会因为一个有效的编程问题被负评而感到困惑。 - nonopolarity
React协调并非Virtual DOM与真实DOM之间的差异,而是先比较前一个Virtual DOM树和当前的树之间的差异,然后根据结果更新实际的DOM。在你的实验中,该算法决定无需从A组件更新DOM(我不知道他们如何作出这个决定,你可以查看协调文档了解更多详情)。 - Son Dang
另外一个有趣的实验基于你的实验,即使一些代码提供了完全相同的DOM输出,但不同的编码风格会给出不同的协调结果。请参见CodeSandbox - Son Dang
是的...我也发现了...我无法通过 document.getElementById("B").firstChild = "abc" 更改文本节点,如果我在 #B 下创建两个子节点,它仍然不会更新,所以看起来 React 记住了第二个文本节点并更新它,而它已经不在屏幕上了。 - nonopolarity
但是如果我们想要,我们可以使用 document.getElementById("B").firstChild.nodeValue = "Hi B"; document.getElementById("B").firstChild.nextSibling.nodeValue = "Hi B";,而React可以更新第二个节点...所以看起来React也使用 node.nodeValue 来更改文本节点的内容。 - nonopolarity
显示剩余2条评论
3个回答

1
在渲染后,React会得到一个关于视图应该如何呈现的JSON,计算出更改并更改实际的DOM。因此,即使重新渲染A,输出的JSON与虚拟DOM的相同,因此React不会触及DOM。

1
这是回答你问题中“为什么”的部分 -
如此链接所解释的那样 - https://gist.github.com/paulirish/5d52fb081b3570c81e3a,许多事情都可能触发DOM的重排。即使是访问DOM元素属性也可能导致DOM的重排,而这是一个非常昂贵的过程。
因此,如果React开始与实际的DOM进行比较,那么它将降低渲染性能而不是提高它。这就是为什么React将更改与先前的副本进行比较,并在必要时将更改更新到实际的DOM中。

0

阅读了React协调文档后,似乎一个简化的思考方式是:

  1. 每当提供给组件COMPO1的props或该组件的状态发生更改时,所有类组件和函数组件的render()都会被调用,以形成虚拟DOM树,并将整个子树与先前的子树进行比较--而不是与实际DOM进行比较,而是与先前的虚拟DOM子树进行比较。
  2. 它将被比较以查看节点是否不同,并递归地对所有子节点执行此操作。
  3. 只有从组件及其下方开始的最小子树与先前的子树不同时,才会导致内容刷新到实际DOM--这意味着如果节点A有子节点B和C,并且B保持不变,而节点C变为不同,则仅C将导致实际DOM的该部分刷新。(当然,如果节点C具有节点D和E,并且D保持不变,而E变为不同,则仅E将刷新到实际DOM)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接