React 通过存储
useState
调用的顺序来维护其状态,并在下次重新渲染时使用该顺序填充状态。
但是当有条件重新渲染时,例如使用布尔标志来控制是否显示子级,则此顺序被破坏。
import { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Click to increment: {count}
</button>
);
};
const CounterGroup = ({ visible }) => {
// visible controls whether the first counter is displayed
return (
<div>
{ visible && <Counter/> }
<Counter/>
</div>
);
};
const App = () => {
const [visible, setVisible] = useState(true);
return (
<div>
<CounterGroup visible={ visible }/>
<p>
<button onClick={ () => setVisible(!visible) }>
Toggle Counter
</button>
</p>
</div>
);
};
export default App;
在此示例中,渲染
App
时,使用了 3 种状态:一个 visible
和两个 count
。但是当 visible
设置为 false
时,只有一个 visible
和一个 count
状态需要填充。React 如何知道要保留哪个 count
(显然是第二个)?
React 在每次渲染组件期间使用与组件绑定的钩子链接列表以特定顺序跟踪钩子。
组件的条件渲染不会干扰状态,因为每个组件的状态都是通过其节点独立跟踪的。
卸载组件时,React 会从内存中完全删除该组件(及其关联的状态),然后重新挂载会创建一个具有新状态的新组件实例。
React 在每个组件渲染期间维护
useState
调用的顺序,而不是存储全局顺序。
问题的例子中,渲染过程是这样的:
// Step 1
<App/>
// Step 2
// found one useState call
// storing the state value: true
<div>
<CounterGroup visible={true}/> // visible is true
<p> ... </p>
</div>
// Step 3
<div>
<div>
<Counter/>
<Counter/>
</div>
<p> ... </p>
</div>
// Step 4
// found one `useState` use in each <Counter/>
<div>
<div>
<button> ... </button>
<button> ... </button>
</div>
<p> ... </p>
</div>
现在您可以单击按钮几次,然后单击切换按钮。 当
visible
设置为false
时,关键区别在于步骤3。你可能会认为重新渲染的结果是:
// Step 3
<div>
<div>
<Counter/>
</div>
<p> ... </p>
</div>
如果是这样,React 将不知道剩下哪个
<Counter/>
。然而,JSX 语法{ ... }
实际上总是提供一些东西。当 visible
为 false
时,JSX 表达式的结果是 false
,而不是空。所以生成的树实际上是:
// Step 3
<div>
<div>
{ false }
<button> ... </button>
</div>
<p> ... </p>
</div>
因此,孩子的数量没有变化。
由于React存储了每个组件的状态,因此需要比较两次渲染得到的DOM树,确定组件之间的对应关系。
上面的例子中,第3步有两棵不同的DOM树,为了简单起见,只写不同的部分:
// Before
<Counter/>
<Counter/>
// After
{ false }
<Counter/>
默认情况下,React 会让相同位置的组件对应起来。即第一个计数器对应
{ false }
,第二个对应<Counter/>
:
<Counter/> => { false }
<Counter/> => <Counter/>
由于
{ false }
与 <Counter/>
具有不同的类型,因此这种对应关系没有意义,因此被忽略。第二个对应关系确实有意义,因此 React 在重新渲染过程中用第二个计数器的状态填充 <Counter/>
的状态。
React key 是你可以告诉 React 如何对应子元素的东西。通过设置组件的 key,React 会将具有相同 key 的组件对应起来,而不是比较位置索引。
如果你不使用 JSX 语法,而是使用 if-else 块,你会得到不同的结果:
const CounterGroup = ({ visible }) => {
// visible controls whether the first counter is displayed
if (visible) {
return (
<div>
<Counter/>
<Counter/>
</div>
);
} else {
return (
<div>
<Counter/>
</div>
);
}
};
这次,没有渲染任何
{ false }
组件。所以 DOM 差异将是:
<Counter/> => <Counter/>
<Counter/> => // Nothing here
因此第一个计数器的状态将在重新渲染期间应用。
您想要实现的目标可以通过将子状态移动到父状态来完成,因此您的视图部分仅依赖于您的可见状态。
import { useState } from 'react';
function App() {
const [visible, setVisible] = useState(false);
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setVisible(!visible)}>Toggle</button>
{visible && <Child count={count} setCount={setCount}/>}
</div>
)
}
function Child({count,setCount}) {
return (
<div>
<button onClick={() => setCount(count + 1)}>Click</button>
<p>{count}</p>
</div>
)
}
export default App;