第六章 深入比对算法与协调器--下

200 阅读6分钟

重置状态的技巧

为什么所有这些关于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"... />
)}

现在,重新渲染时,在比较的前后数组就不一样了。

在状态发生变化前,isCompanyfalse:

[
    { type: Checkbox },
    {
        type: Input,
        key: 'person-tax-id-number',
    }
]

在状态发生变化后,isCompanytrue:

[
    { 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会是这样:

在状态发生变化前,isCompanyfalse:

[
    { type: Checkbox }.
    null,
    {
      type: Input,
      key: 'tax-input',
    }
]

在状态发生变化后,isCompanytrue:

[
    { 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组件将永远处于第二位。将不会对它进行重新挂载。因此没有性能问题。而这个顾虑也是多余的。