Published on

深入React - part 2

Authors
  • avatar
    Name
    Deng Hua
    Twitter

目录

RENDER PHASE阶段中,我们提到了一个关键部分,DIFFING过程中的不同算法,这篇文章将深入DIFFING是如何起作用的。

Diffing

首先,diff是基于两个基本假设

  • 两种不同类型的元素会产生不同的树

  • 具有稳定的key的元素,随着时间推移,key在渲染器中保持不变。

基于这两点,可以使算法的速度快几个数量级。

什么是diff,diff是在两个树之间根据它们的位置,一步一步的比较元素。

基本上有两种不同的情况需要考虑:

  1. 在树的两个渲染器之间的相同位置有两个不同的元素

  2. 在树的的相同位置有相同的元素

这是diff仅考虑的两种情况

相同位置,不同的元素

diff

以上图为例,我们发现一个元素在树的某个位置发生了变化,具体的,这是元素的类型发生变化(<div> -> <header>)。

在这种情况下,react将假设元素本身与它的子元素不再有效,所有的这些元素都将被销毁并从DOM中移除,这也包括它们的状态。

以这个例子,<div>和<SearchBar>将被移除,然后被重建为<header>组件,然后重建一个全新的<SearchBar>组件。 是的,哪怕这个新建的<SearchBar>和之前被移除的<SearchBar>看起来“相同”,这当然包括<SearchBar>之前的状态也会一并移除。

如果上例的父元素<div>不变,子元素变呢?

diff

也是类似的,整个<SearchBar>会被彻底的移除,重建为<ProfileMenu>组件,但是此时父元素就不会销毁了。

这是第一种情况。


相同位置,相同的元素

第二种情况,在两个渲染树之间有相同的位置,不同的元素。

在渲染阶段后,树中某个位置的元素与以前相同,元素将简单的保留在DOM中,如下例:

diff

保留的元素包括其所有子元素,最重要的是组件状态也一并保留下来。

在例子中,更改的不是元素类型,而是元素的className属性,对于<SearchBar>,更改的是wait属性,React 要做的仅仅是简单的更改DOM元素属性和新的props。 这期间,DOM不会被移除,状态也不会被销毁。

但是有时候我们并不想这样,我们希望用新的状态去创建一个全新的组件,所以这就是key发挥作用的地方。

例子

回到最开始的demo中,我们点击Show Details按钮时会显示段落,再点击会隐藏段落。点击红心可以增加点赞数量。但是我们切换Tab的时候发现,段落保持了上一个Tab的状态,点赞数也依旧是之前的Tab的。

这意味着,这个Tab组件的状态被保留了。

而我们希望的是,每当我们首次新的Tab时,状态应该是初始化的,这与我们事与愿违。

那么这其中发生了什么?

当我们每次单击这些Tab之一时,<Tab>组件会经历重新渲染的过程。不过我们也发现每次重新渲染,组件始终位于树中完全相同的位置,这就是上一节中提到的相同位置,相同的元素的情况。

diff

当我们切换Tab的时候,<TabContent>组件并没有被销毁,所以它留在DOM中,唯一改变的是它收到的props,所以props会改变,

diff

可是,当点击第四个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

diff

我们有一个包含两个问题题目的列表组件,没有使用key属性,当我们将一个新项目添加到列表的顶部时,之前存在的两个<Question>组件显然还是一样的,但它们在react element tree中的位置发生了变化, 原来的第一,第二的位置变为了第二和第三。

这两个组件自始至终除了在树内的位置不同外,没有任何变化,根据之前的规则,这两个DOM元素将从DOM中移除,然后立即在他们的新位置重新创建,这显然是不利于性能的,因为它在移除和重建相同的元素。

问题是,React不知道这是在浪费性能。开发人员可以直观的知道这两个元素实际上和之前一样, react不可能知道。

这就是Key发挥作用的地方。

唯一的Key可以帮助React标识一个元素,我们可以给react它自己没有的信息。

diff

当在顶部添加新组件时,两个原始组件在树内的位置仍然会改变,但它们都有独一无二的key(q1q2),且key在渲染过程中保持不变。这两个元素将保留在DOM中,即使它们在树上的位置不同,所以它们不会被销毁。

这两种设计在数据量小的列表下,差异不会很明显,但当数据量非常庞大的时候,它倆产生的性能差异会非常大。

  • key prop to reset state

Key用于重置组件中的状态

diff

在这个例子中,<Question>组件包含一个输入框以及问题标题和详情(代码略过),此时输入框也有对应的输入,作为问题的答案。

此时,将quesitonprops内的的问题更改了会发现,输入框中的文字依旧没有变化,因为<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 PHASECOMMIT PHASE阶段。 然后会转到下一行,再对其他状态更新函数做类似的流程。

直觉上,如果在这个事件处理程序中有三个状态变量在更新,那么React会重新渲染三次。

state batching


然而,并不是这样。实际上,这些状态更新将被收集到一个批处理中,更新整个事件处理程序。

更新多个状态不会立即导致每次更新的重新呈现,相反,事件处理程序中的所有状态片段都只有一次更新,它们都是成批的。

这可以避免一些重复和多余的状态更新,也能节省性能。这也算是react给开发者的一种开箱即用的性能优化。

state batching


我们看向这行代码:

const reset = function () {
  setAnswer('');
  console.log(answer);
  setBest(true);
  setSolved(false);
}

我们在更新answer状态后立即引用它,那么会打印出什么值呢?

记住,组件状态在RENDER PHASE都存储在Fiber Tree中,在执行这一行代码时,RENDER PHASE还未发生,所以React仍然在逐行读取代码,弄清楚哪些状态需要更新。 但它实际上还没有更新状态,它也没有被渲染。所以answer仍将保持当前状态,尽管我们将再次更新其为空字符串。

在React中,此时的状态也称为Stable State,这个状态是**“过时”**的,最新的状态只有在重新渲染之后才会反映在状态变量中。

所以也可以说,React的更新状态是异步的。

state batching


flushSync

But,如果你想使React同步执行状态更新,对于刚才的例子,每个状态更新函数都将导致重新渲染,可以使用React.flushSync

React flushSync