重置状态的技巧
为什么所有这些关于key关键字的逻辑对我们的表单Form组件以及本章开头提到的它的那个 bug 很重要呢?有趣的是:“key关键字” 仅仅是元素的一个属性,它并不局限于动态数组。在任何子元素数组中,它的作用方式都是完全一样的。正如我们已经发现的那样,从一开始就带有那个 bug 的表单Form组件的对象定义如下:
const Form = () => {
const [isCompany, setIsCompany] = useState(false);
return (
<>
<Checkbox onChange={() => setIsCompany(!isCompany)} />
{isCompany ? (
<Input id="company-tax-id-number" ... /> )
:(
<Input id="person-tax-id-number" ... />
)}
</>
)
}
有这样一个子数组:
[
{ type: Checkbox },
{ type: Input }, // react thinks it's the same input between re-renders
]
为了修复一开始这个bug,我们需要做的是,让React知道在重新渲染时的Input组件添加是不同的,这个Input组件不应该被复用。如果我们为这两个Input组件添加key关键字,就可以解决这个问题:
{isCompany ? (
<Input id="company-tax-id-number" key="company-tax-id-number"... /> )
:(
<Input id="person-tax-id-number" key="person-tax-id-number"... />
)}
现在,重新渲染时,在比较的前后数组就不一样了。
在状态发生变化前,isCompany为false:
[
{ type: Checkbox },
{
type: Input,
key: 'person-tax-id-number',
}
]
在状态发生变化后,isCompany为true:
[
{ type: Checkbox },
{
type: Input,
key: 'company-tax-id-number',
}
]
如此一来,两个Input组件的key不一样了。React会卸载第一个Input组件,并从头开始建立第二个Input组件。在此基础上,来回切换Input组件时,状态就得以重置了。
代码示例: advanced-react.com/examples/06…
这个技巧被称呼为“状态重置”。它本身与状态并无关联,但有时候当需要将非受控组件(比如输入框)的状态重置为默认值时会用到它。做这件事甚至都不需要像我上面提到的那样有两个组件,一个组件就够了。key 中任何会根据具体条件变化的唯一值都能起到这个作用。例如,如果你想在 URL 变化时强制重置状态,操作可以像下面这样简单:
// grab the current url from our router solution
const { url } = useRouter();
// I want to reset that input fiel when the page URL changes
return <Input id="some-id" kye={url} />
不过在这里要小心谨慎。正如你所见,这可不只是 “状态重置” 这么简单。它会迫使 React 完全卸载一个组件,然后从头开始挂载一个新的组件。对于大型组件来说,这可能会引发性能方面的问题。状态被重置这一情况只是这种彻底销毁行为的一个附带结果罢了。
通过使用key关键字来复用已经存在的元素
有趣的是:key关键字开可以帮我们复用已经存在的元素。还记得这段代码吗,我们通过渲染Input元素在不同位置来来解决状态相同的问题:
const Form = () => {
const [isCompany, setIsCompany] = useState(false);
return (
<>
<Checkbox onChange={() => setIsCompany(!isCompany)} />
{/* checkbox somewhere here */}
{isCompany ? (
<Input id="company-tax-id-number" placeholder="Enter you Company Tax Id" .../>
) : null}
{!isCompany ? (
<Input id="person-tax-id-number" placeholder="Enter you personal Tax Id" .../>
) : null}
</>
)
}
当isCompany发生变化时,React会卸载相应位置的Input组件,再挂载相应位置的Input组件。但是,如果我为这两个Input组件添加相同的key值,神奇的事情就发生了:
<>
<Checkbox onChange={() => setIsCompany(!isCompany)} />
{/* checkbox somewhere here */}
{isCompany ? (
<Input id="company-tax-id-number" key="tax-input" placeholder="Enter you Company Tax Id" .../>
) : null}
{!isCompany ? (
<Input id="person-tax-id-number" key="tax-input" placeholder="Enter you personal Tax Id" .../>
) : null}
</>
从数据和重新渲染的视角,其虚拟DOM会是这样:
在状态发生变化前,isCompany为false:
[
{ type: Checkbox }.
null,
{
type: Input,
key: 'tax-input',
}
]
在状态发生变化后,isCompany为true:
[
{ type: Checkbox }.
{
type: Input,
key: 'tax-input',
},
null
]
React在比对这段虚拟DOM的前后值时,发现有两个Input组件共享着相同的"key"。所以,React会认为,这个Input组件只是变化了位置,所以React会复用这个元素的实例。如果我们输入了某些内容,即便从技术层面来讲这些输入框(Input)是不同的,但状态仍会被保留下来。
代码示例: advanced-react.com/examples/06…
就这个特定的例子而言,当然这只是一种有意思的行为,在实际应用中并不是特别有用。不过我可以想象它能被用于对诸如手风琴组件、标签页内容或者某些图库等组件的性能进行微调。
为什么我们不需要在数组外添加key关键字
让我们进一步了解协调器。现在我们已经解决了开头的bug,也多少了解了协调器的算法。但还是有些谜题没有解决。比如,当你在列举一个列表时,为什么React没有强制你为数组的每一个成员添加key关键字?
代码是这样的:
const data = ['1', '2'];
const Component = () => {
// "key" is mandatory here!
return (
<>
{data.map((value) => (
<Input key={value} /> ))}
</>
);
};
和这样的:
const Component = () => {
// "key" is mandatory here!
return (
<>
<Input />
<Input />
</>
);
};
这两段代码的虚拟DOM是一样的:
[{ type: Input }, { type: Input }]
所以,为什么我们需要在第一段代码中添加key关键字,而在第二段不需要呢?
区别在于第一个示例是一个动态数组。React 并不知道在下一次重新渲染期间你会对这个数组做些什么:是移除、添加、重新排列元素,又或者是保持原样。所以它强制要求你添加 “键(key)” 作为一种预防性措施,以防你动态地对该数组进行更改。
你可能会问,这里面的趣味点在哪里呢?就在这里:尝试有条件地用相同的 “键(key)” 去渲染那些不在数组中的输入框(input):
代码示例: advanced-react.com/examples/06…
当动态数组与普通元素在一起
如果你仔细阅读了本章节,你也许会有一个顾虑:
- 如果动态项被转换为一个子元素数组,而这个数组和普通组合在一起的元素没有区别
- 并且如果我把普通元素放在一个动态数组之后
- 然后在这个数组中添加或移除一个元素
这是否意味着这个数组之后的元素总是会重新挂载自己?基本上,这段代码是不是会造成性能灾难呢?
const data = ['1', '2'];
const Component = () => {
return(
<>
{data.map((i) => (
<Input key={i} id={i} /> ))}
{/* will this input re-mount if I add a new item in the array above? */}
<Input id="3" />
</>
);
};
当这段代码被转化成虚拟DOM,它是一个有三个成员的数组 - 前两个成员是动态的,第三个是静态的:
[
{ type: Input, key: 1 }, // input from the array
{ type: Input, key: 2 }, // input from the array
{ type: Input }, // input after the array
];
而且如果我往这个数组中再添加一个元素,那么在第三个位置将会出现一个来自该数组的输入框(Input)元素,而那个 “手动” 输入框就会移到第四个位置,从 React 的角度来看,这意味着它是一个需要重新挂载的新元素。
幸运的是,情况并非如此。呼……React 可比这聪明多了。 当我们混合使用动态元素和静态元素时,就像上面代码那样,React 会简单地创建一个由那些动态元素组成的数组,并将整个这个数组作为子元素数组中的第一个子元素。以下将是那段代码的定义对象:
[
// the entire dynamic array is the first position in the
children's array
[
{ type: Input, key: 1 },
{ type: Input, key: 2 },
],
{
type: Input, // this is our manual Input after the array
},
];
这个Input组件将永远处于第二位。将不会对它进行重新挂载。因此没有性能问题。而这个顾虑也是多余的。