- Published on
深入React - part 2
- Authors
- Name
- Deng Hua
目录
在RENDER PHASE
阶段中,我们提到了一个关键部分,DIFFING
过程中的不同算法,这篇文章将深入DIFFING
是如何起作用的。
Diffing
首先,diff是基于两个基本假设
两种不同类型的元素会产生不同的树
具有稳定的
key
的元素,随着时间推移,key
在渲染器中保持不变。
基于这两点,可以使算法的速度快几个数量级。
什么是diff,diff是在两个树之间根据它们的位置,一步一步的比较元素。
基本上有两种不同的情况需要考虑:
在树的两个渲染器之间的相同位置有两个不同的元素
在树的的相同位置有相同的元素。
这是diff仅考虑的两种情况
相同位置,不同的元素
以上图为例,我们发现一个元素在树的某个位置发生了变化,具体的,这是元素的类型发生变化(<div>
-> <header>
)。
在这种情况下,react将假设元素本身与它的子元素不再有效,所有的这些元素都将被销毁并从DOM中移除,这也包括它们的状态。
以这个例子,<div>和<SearchBar>
将被移除,然后被重建为<header>
组件,然后重建一个全新的<SearchBar>
组件。 是的,哪怕这个新建的<SearchBar>
和之前被移除的<SearchBar>
看起来“相同”,这当然包括<SearchBar>
之前的状态也会一并移除。
如果上例的父元素<div>
不变,子元素变呢?
也是类似的,整个<SearchBar>
会被彻底的移除,重建为<ProfileMenu>
组件,但是此时父元素就不会销毁了。
这是第一种情况。
相同位置,相同的元素
第二种情况,在两个渲染树之间有相同的位置,不同的元素。
在渲染阶段后,树中某个位置的元素与以前相同,元素将简单的保留在DOM中,如下例:
保留的元素包括其所有子元素,最重要的是组件状态也一并保留下来。
在例子中,更改的不是元素类型,而是元素的className
属性,对于<SearchBar>
,更改的是wait
属性,React 要做的仅仅是简单的更改DOM元素属性和新的props。 这期间,DOM不会被移除,状态也不会被销毁。
但是有时候我们并不想这样,我们希望用新的状态去创建一个全新的组件,所以这就是key
发挥作用的地方。
例子
回到最开始的demo中,我们点击Show Details
按钮时会显示段落,再点击会隐藏段落。点击红心可以增加点赞数量。但是我们切换Tab的时候发现,段落保持了上一个Tab的状态,点赞数也依旧是之前的Tab的。
这意味着,这个Tab组件的状态被保留了。
而我们希望的是,每当我们首次新的Tab时,状态应该是初始化的,这与我们事与愿违。
那么这其中发生了什么?
当我们每次单击这些Tab之一时,<Tab>
组件会经历重新渲染的过程。不过我们也发现每次重新渲染,组件始终位于树中完全相同的位置,这就是上一节中提到的相同位置,相同的元素的情况。
当我们切换Tab的时候,<TabContent>
组件并没有被销毁,所以它留在DOM中,唯一改变的是它收到的props
,所以props
会改变,
可是,当点击第四个Tab的时候,就不是这样了。
{
activeTab <= 2 ? (
<TabContent item={content[activeTab]} />
) : (
<DifferentContent />
)
}
此时,<TabContent>
组件会被移除,重建为<DifferentContent>
组件。
所以会发现,点击第四个Tab后,在重新点击前三个Tab任意一个,恢复<TabContent>
组件显示,<TabContent>
会被初始化,之前的点赞数等状态都会消失,因为<TabContent>
此前已经被彻底的删除,包括状态。
The Key Prop
Key属性是一个特殊的属性,我们可以用它来告诉React,一种元素是“独一无二”的。这对DOM元素和react元素都有效。
在实践中,我们可以给每个组件一个唯一的标识,这将允许react区分同一组件类型下的多个组件。
key in lists
我们有一个包含两个问题题目的列表组件,没有使用key属性,当我们将一个新项目添加到列表的顶部时,之前存在的两个<Question>
组件显然还是一样的,但它们在react element tree
中的位置发生了变化, 原来的第一,第二的位置变为了第二和第三。
这两个组件自始至终除了在树内的位置不同外,没有任何变化,根据之前的规则,这两个DOM元素将从DOM中移除,然后立即在他们的新位置重新创建,这显然是不利于性能的,因为它在移除和重建相同的元素。
问题是,React不知道这是在浪费性能。开发人员可以直观的知道这两个元素实际上和之前一样, react不可能知道。
这就是Key发挥作用的地方。
唯一的Key可以帮助React标识一个元素,我们可以给react它自己没有的信息。
当在顶部添加新组件时,两个原始组件在树内的位置仍然会改变,但它们都有独一无二的key(q1
和q2
),且key在渲染过程中保持不变。这两个元素将保留在DOM中,即使它们在树上的位置不同,所以它们不会被销毁。
这两种设计在数据量小的列表下,差异不会很明显,但当数据量非常庞大的时候,它倆产生的性能差异会非常大。
key prop to reset state
Key用于重置组件中的状态
在这个例子中,<Question>
组件包含一个输入框以及问题标题和详情(代码略过),此时输入框也有对应的输入,作为问题的答案。
此时,将quesiton
props内的的问题更改了会发现,输入框中的文字依旧没有变化,因为<Question>
组件的状态被保留了下来。
如何重置输入框呢?
可以添加一个key
<QuestionBox>
<Question
question={{
title: 'React VS JS',
body: 'Why should we use React?'
}}
key='q23'
/>
</QuestionBox>
然后在更改propsquesiton
时,更改这个key
<QuestionBox>
<Question
question={{
title: 'React VS JS',
body: 'Why should we use React?'
}}
key='q89'
/>
</QuestionBox>
通过这样做,react知道这会是一个不同的组件,它会创造一个全新的DOM元素,其中的状态也会被重置。
State Update Batching(状态批处理)
const [answer, setAnswer] = useState('');
const [best, setBest] = useState(true);
const [solved, setSolved] = useState(false);
const reset = function () {
setAnswer('');
console.log(answer);
setBest(true);
setSolved(false);
}
return (
<div>
<button onClick={reset}>Reset</button>
{/* ... */}
</div>
)
在这段示例代码中,我们使用useState
定义了三个状态,在UI中也有一个按钮,点击按钮时会调用reset
事件处理函数,将三个状态恢复初始状态。
那么,这三个状态在背后是如何更新的?
我们可能认为,React看到第一个函数,setAnswer
函数执行时,它会根据函数将answer
状态更新为空字符串,然后触发RENDER PHASE
和COMMIT PHASE
阶段。 然后会转到下一行,再对其他状态更新函数做类似的流程。
直觉上,如果在这个事件处理程序中有三个状态变量在更新,那么React会重新渲染三次。
然而,并不是这样。实际上,这些状态更新将被收集到一个批处理中,更新整个事件处理程序。
更新多个状态不会立即导致每次更新的重新呈现,相反,事件处理程序中的所有状态片段都只有一次更新,它们都是成批的。
这可以避免一些重复和多余的状态更新,也能节省性能。这也算是react给开发者的一种开箱即用的性能优化。
我们看向这行代码:
const reset = function () {
setAnswer('');
console.log(answer);
setBest(true);
setSolved(false);
}
我们在更新answer
状态后立即引用它,那么会打印出什么值呢?
记住,组件状态在RENDER PHASE
都存储在Fiber Tree
中,在执行这一行代码时,RENDER PHASE
还未发生,所以React仍然在逐行读取代码,弄清楚哪些状态需要更新。 但它实际上还没有更新状态,它也没有被渲染。所以answer
仍将保持当前状态,尽管我们将再次更新其为空字符串。
在React中,此时的状态也称为Stable State
,这个状态是**“过时”**的,最新的状态只有在重新渲染之后才会反映在状态变量中。
所以也可以说,React的更新状态是异步的。
flushSync
But,如果你想使React同步执行状态更新,对于刚才的例子,每个状态更新函数都将导致重新渲染,可以使用React.flushSync