前言
React 使用树状结构来对你的代码进行管理,将 JSX 生成 UI 树,并且根据 UI 树去更新浏览器的 DOM 元素。
根据组件在 UI 树中的位置,React 可以跟踪哪些 state 属于哪个组件。你可以控制在重新渲染过程中何时对 state 进行保留和重置。
或许说到这里,你会觉得,在你为一个组件添加 state 的时候,他就是属于这个组件的,但是事实真是如此吗,并不是,实际上 satae 是被保存在 React 内部,根据当前这个组件在 UI 树的位置,React 会将 它 与 对应的 state 关联起来。
你会说这和存在于组件内部有什么区别,请往下看。
不同组件的state
import { useState } from 'react';
export default function App() {
const [showB, setShowB] = useState(true);
return (
<div>
<Counter />
{showB && <Counter />}
<label>
<input
type="checkbox"
checked={showB}
onChange={e => {
setShowB(e.target.checked)
}}
/>
渲染第二个计数器
</label>
</div>
);
}
function Counter() {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
加一
</button>
</div>
);
}
很明显这会产生两个不同的盒子,并且他们有着属于自己的状态,互不干扰。
在你关闭掉第二个盒子的渲染并且重新渲染的时候,state 会被重置,这也没有问题
相同位置的相同组件的 state
接下去的例子就能够体现出来,state 保存在组件内部以及保存在 React 内部,会造成什么样的不同
import { useState } from 'react';
export default function App() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{isFancy ? (
<Counter isFancy={true} />
) : (
<Counter isFancy={false} />
)}
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
渲染有阴影的组件
</label>
</div>
);
}
function Counter({ isFancy }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
if (isFancy) {
className += ' fancy';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
加一
</button>
</div>
);
}
你可以会以为在勾选复选框的时候 Counter 组件的state会被重置,但是很明显,虽然在代码中
{isFancy ? (
<Counter isFancy={true} />
) : (
<Counter isFancy={false} />
)}
我们写了两个子组件,但是对于react来说,他们两个是完全相同的,保留了state的状态
后面我又写了一个简单的vue项目验证了一下,这个问题在vue中也是一样的,这可能就是在编译器编译完我们写的代码以后,丢给渲染器的前后组件时完全一样的(渲染器不去关心你是哪个子组件,他只关心拿到的组件结构跟之前是否有出入,是否需要重新渲染)。
后面我在子组件中定义了 mounted 钩子也证实了这个事,子组件并没有被销毁重建,因为这个钩子就只有在最开始的时候执行了一次!
- 想要避免这种情况,我们可以用不同的节点包裹上子组件,
{isFancy ? (
<div>
<Counter isFancy={true} />
</div>
) : (
<section>
<Counter isFancy={false} />
</section>
)}
- 或者给两个子组件设置不同的 key。这也是为什么我们在使用 react 或者 vue 的循环生成的时候,要为每一个子组件设置一个不同的 key。
{isFancy ? (
<Counter key="1"isFancy={true} />
) : (
<Counter key="2"isFancy={false} />
)}
- 在 react 中,还可以在不同的地方渲染这两个组件,但是这在vue中好像是行不通的,因为上面的vue例子中使用的就是这样的写法。
{isFancy && (
<Counter isFancy={true} />
)}
{!isFancy && (
<Counter isFancy={false} />
)}
如何为移除的组件保留状态
在实际的使用的时候,你可能希望两个切换的tab虽然是属于同一个组件,但是他们要有着自己的状态,这时候我们可以不选择移除这个组件,而是选择隐藏它,类似于 vue 中的 v-show 来代替 v-if。
但是要注意的是,这种用法要是过多,可能会导致性能上的问题,所以在实际的使用场景当中,应该要根据自己的需求来选择要如何使用,以及是否保留组件状态。
插曲,为什么不要用index作为列表的key
当你使用index作为列表的key的时候,说明你的列表都是一样的组件,万一你的组件内部存在状态,并且你的这个列表是可以切换顺序的,那么就会导致状态混乱,因为对于react来说,组件都是一样的,index为key代表组件的位置从来没变化过,一旦变化了,那么状态也只会对号入座的赋值。