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

175 阅读8分钟

这个谜题的答案

现在,我们回到开头的谜之代码

const Form = () => {
    const [isCompany, setIsCompany] = useState(false);
    
    return (
        <>
            {/* checkbo somewhere here */}
            {isCompany ? (
                <Input id="company-tax-id-number" placeholder="Enter you Company Tax Id" .../>
            ) : (
                <Input id="person-tax-id-number" placeholder="Enter you personal Tax Id" .../>
            )}
        </>
    )
}

如果isCompanytrue变为false,会比对的是哪些对象?

isCompany发变化前,为true

{
    type: Input,
    ... // the rest of the stuff, including props like id ="company-tax-id-number"
}

变化发生后,为false

{
    type: Input,
    ... // the rest of the stuff, including props like id ="person-tax-id-number"
}

站在React的视角,“type”的值并没有发生变化。前值和后值指向的是同一个函数的索引:Input组件。在React看来,真正发生变化的是id:从"company-tax-id-number"到"person-tax-id-number"。

所以,此时React会用新的数据更新Input组件,也就是重新渲染。而与Input相关的事物并没有被销毁。这在视觉上看就是:如果我们在输入框输入内容、点击勾选框,文本都依然存在。

虽然组件这样的行为并不算差。这个表现可以通过重新渲染来复用刚刚输入的值,而非重新挂载。但是在这个场景中,我想确保每次点击勾选框时,文本都会重置。

至少有两个方法可以达到这一效果:使用数组 和 使用 key关键字。

协调器与数组

现在,我们知道数组是虚拟DOM树里的成员。但是,我们不可能写一个每个组件都只返回一个元素的React应用。我们需要讨论元素数组,以及元素数组在重新渲染过程中的表现。我们的Form组件里也有数组:

const Form = () => {
    const [isCompany, setIsCompany] = useState(false);
    
    return (
        <>
            {/* checkbox somewhere here */}
            {isCompany ? (
                <Input id="company-tax-id-number" placeholder="Enter you Company Tax Id" .../>
            ) : (
                <Input id="person-tax-id-number" placeholder="Enter you personal Tax Id" .../>
            )}
        </>
    )
}

它返回了一个片段(就是: <>...</>)。这个片段的子组件是一个数组。此处的勾选框被隐藏了。完整的代码是这样的:

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" .../>
            ) : (
                <Input id="person-tax-id-number" placeholder="Enter you personal Tax Id" .../>
            )}
        </>
    )
}

在重新渲染过程中,当React遍历到了值为数组的子组件时,它会遍历这个数组,并依据类型关键字比较该数组的前值与后值。

如果我点击了勾选框,触发了Form的重新渲染,React会看到这样的数组:

[
 {
     type: Checkbox,
 },
 {
     type: Input, // our conditional input
 }
]

之后,React会逐个检查数组成员。第一个元素的type的前值是 Checkbox,后值也是Checkbox.React会复用这个组件并重新渲染它。第二个元素,亦复如是。

即便一些元素是这样条件式渲染的:

isCompany ? <Input /> : null;

React也会有稳定的数组成员。所以,我们可以把组件这样重写:

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}
        </>
    )
}

这个数组将永远只有三个成员,第一个成员是Checkbox,第二个成员是Inputnull,第三个成员是Inputnull

所以,在状态变化并重新渲染这整个表单时,到底发生了什么?

发生变化前,isCompanyfalse:

[{ type: Checkbox}, null, { type: Input }];

发生变化后,isCompanytrue:

[{ type: Checkbox}, { type: Input }, null]

而React一个一个比对前后值时,会是:

  • 第一个成员,前值后值都是Checkbox -> 重新渲染Checkbox
  • 第二个成员,前值是null,后值是Input -> 挂载Input
  • 第三个成员,前值是Input,后值是null -> 卸载Input

如此一来,我们就实现了isCompany变动时,文本的值也重置的效果了。

代码示例: advanced-react.com/examples/06…

协调器与key关键字

另一个解决这个bug的方法是: 使用key关键字。

对于使用React来写列表的人来说,key关键字已经是再熟悉不过了。React强制我们为列表添加key关键字:

const data =['1', '2'];

const Component = () => {
    // key is mandatory here!
    return data.map((value) => <Input key={value} />)
}

而其对应的虚拟DOM也是很清晰的:

[
    { type: Input }, // "1" data item
    { type: Input }, // "2" data item
]

但像这样的动态列表存在的问题在于,它们是动态的。我们可以对它们重新排序,在开头或末尾添加新的项目,而且通常可以随意对它们进行操作。

现在,React要面对一个有趣的任务:数组中的所有组件都是一样的类型。如何区分哪个是哪个?如果数组的顺序发生了变化:

[
    { type: Input }, // "2" data item now, but React desn't know that
    { type: Input }, // "1" data item now, but React desn't know that
]

如何确保正确的已有元素被重复使用呢?因为如果仅仅依靠该数组中元素的顺序,它会将第二个元素的数据用于第一个元素的实例,反之亦然。如果这些项目(元素)有状态,这将会导致奇怪的行为:状态会保留在第一个项目中。如果你在第一个输入字段中输入了某些内容,然后重新排列数组,输入的文本仍会保留在第一个输入框中。

image.png

这正是为什么我们需要key关键字:它是React在进行前后比对时区分每一个数组成员的标识符。如果一个元素的"key"和"type"都没有发生变化,React就可以更好的复用这个元素。

在这个数组中,有了key的数组应该是这样的。调整顺序前:

[
    { type: Input, key: '1' }, // "1" data item
    { type: Input, key: '2' }, // "2" data item
]

调整顺序后:

[
    { type: Input, key: '2' }, // "2" data item, React knows that beacause of "key"
    { type: Input, key: '1' }, // "1" data item, React knows that beacause of "key"
]

现在,有了key关键字,React知道了在重新渲染时,它需要复用已经创造的元素,把它移到第一个位置。它只要互换这两个inputDOM节点即可。我们在第一个input输入的文本,会被挪到第二个文本框的位置。

image.png

代码示例: advanced-react.com/examples/06…

key关键字与被缓存的列表

有一个普遍的误解是:key关键字不是是用来提升应用性能的。为动态列表添加key关键字可以防止数组重新渲染。但是从上面的例子,大家可以知道,key关键字不是这样发生作用的。key关键字的作用在于,帮助React知道哪些元素在重新渲染时可以复用。重新渲染依然会发生,就像渲染在其他组件的子组件一样。

如果我们想避免列表的重新渲染,我们需要使用React.memo。对于静态数组,这是非常简单的:只要把需要缓存的元素项包裹在React.memo里,然后为数组赋予id属性或者其在数组的下标即可。因为这些值在静态列表里都是稳定的。

const data = [
 { id: 'business', placeholder: 'Business Tax' },
 { id: 'person', placeholder: 'Person Tax' },
];

const InputMemo = React.memo(Input);
const Component = () => {
    // array's index is fine here, the array is static
    return data.mao((value, index) => {
        <InputMemo
            key={index}
            placeholder={value.placeholder}
        />
    })
}

如果Parent组件的重新渲染被触发了,没有一个InputMemo会重新渲染:它们被包裹在React.memo里面,针对每一个成员的key关键字并没有变化。

如果缓存的是动态数组,情况就更有趣了,而这这是key关键字发挥作用的时候。当我们通过为数组重新排序来触发重新渲染时,会发生什么?

// array befor re-render
[
 { id: 'business', placeholder: 'Business Tax' },
 { id: 'person', placeholder: 'Person Tax' },
];

// array after re-render
[
 { id: 'person', placeholder: 'Person Tax' },
  { id: 'business', placeholder: 'Business Tax' },
];

如果我们还是使用index作为key的值,那么在React视角,在重新渲染前和重新渲染后,key= "0"的元素始终是列表的第一个元素。但是,属性placeholder将会从Business Tax 变为Person Tax。那么,即使这个元素被缓存了,但是属性发生了变化,所以React会把它视作没有被缓存一样重新渲染。

image.png

修复这个bug其实很简单:我们只要确保key能更好的匹配每一个成员。在这个例子中,我们可以使用id作为key的值:

const Parent = () => {
    // if array can be sorted, or number of its items can change,
    then "index" as "key" is not a good idea
    // we need to use something that identifies an array item instead
    return sortedData.map((value, index) => (
        <InputMemo
            key={value.id}
            placeholder={value.placeholder}
        /> 
    ));
};

如果列表数据没有唯一性标识符,我们需要在这个重新渲染的组件外重新遍历这个数组,并手动添加id。

在我们输入的情况中,如果我们使用 id 作为键(key),那个 key="business" 的项仍会带有 placeholder="Business Tax" 这个属性,只是它在数组中的位置会有所不同。所以 React 只会交换相关联的 DOM 节点位置,但实际的组件并不会重新渲染。

image.png

如果我们在数组开头添加另一个输入项,情况也完全一样。如果我们使用数组的索引作为键(key),那么从 React 的角度来看,原本 placeholder 属性为 Business Tax 的那个项,其属性会从 Business Tax 变为 New tax;而原本 placeholder 属性为 Person Tax 的项会变成 Business Tax。所以它们两者都会重新渲染。并且那个带有 placeholder 属性值为 Person Tax 的新项将会从头开始挂载。

image.png

并且如果我们改为使用 id 作为键(key),那么 “Business Tax” 和 “Person Tax” 这两项都会保留它们各自的键,由于它们已经被 memoized(记忆化)了,所以它们不会重新渲染。而那个键为 “New tax” 的新项将会从头开始挂载。

image.png

代码示例: advanced-react.com/examples/06…