React源码揭秘(三):Diff算法详解

编者按:本文作者奇舞团前端开发工程师苏畅。

代码参照React 16.13.1

什么是Diff

在前两篇文章中我们分别介绍了 React 的首屏渲染流程1和组件更新流程2,其中

  • 首屏渲染会渲染一整棵 DOM 树

  • 组件更新会根据变化的状态局部更新 DOM 树

那么 React 如何知道哪些 DOM 节点需要被更新呢?

在上一篇文章这里3我们讲到,在render阶段的beginWork函数中,会将上次更新产生的 Fiber 节点与本次更新的 JSX 对象(对应ClassComponent的this.render方法返回值,或者FunctionComponent执行的返回值)进行比较。根据比较的结果生成workInProgress Fiber,即本次更新的 Fiber 节点。

用通俗的语言讲

React 将上次更新的结果与本次更新的值比较,只将变化的部分体现在 DOM 上

这个比较的过程,就是 Diff。本篇文章主要讲解Rect Diff 算法4的内部实现,对 Diff 的简单讲解请参考React 文档5

Diff的瓶颈以及React如何应对

由于 Diff 操作本身也会带来性能损耗,React文档中提到,即使在最前沿的算法中,将前后两棵树完全比对的算法的复杂程度为 O(n 3 ),其中 n 是树中元素的数量。

如果在 React 中使用了该算法,那么展示 1000个元素所需要执行的计算量将在十亿的量级范围。这个开销实在是太过高昂。

为了降低算法复杂度,React的diff会预设三个限制:

  1. 只对同级元素进行Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他。

  2. 两个不同类型的元素会产生出不同的树。如果元素由div变为p,React会销毁div及其子孙节点,并新建p及其子孙节点。

  3. 开发者可以通过 key属性 来暗示哪些子元素在不同的渲染下能保持稳定。考虑如下例子:

// 更新前
<div>
    <p key="ka">ka</p>
    <h3 key="song">song</h3>
</div>
// 更新后
<div>
    <h3 key="song">song</h3>
    <p key="ka">ka</p>
</div>

如果没有key,React会认为div的第一个子节点由p变为h3,第二个子节点由h3变为p。这符合限制2的设定,会销毁并新建。

但是当我们用key指明了节点前后对应关系后,React知道key === "ka"的p在更新后还存在,所以DOM节点可以复用,只是需要交换下顺序。

这就是React为了应对算法性能瓶颈做出的三条限制。

Diff是如何实现的

接下来我们看看Diff的具体实现。我们从Diff的入口函数reconcileChildFibers出发,接着再看看不同类型的Diff是如何实现的。

Diff函数入口函数简介

让我们稍稍看下Diff的入口函数,不要被代码长度吓到喽 ????,其实逻辑很简单——在函数内部,会根据newChild类型调用不同的处理函数。

// 根据newChild类型选择不同diff函数处理
function reconcileChildFibers(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChild: any,
): Fiber | null {
  const isObject = typeof newChild === 'object' && newChild !== null;
  if (isObject) {
    // object类型,可能是 REACT_ELEMENT_TYPE 或 REACT_PORTAL_TYPE
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        // 调用 reconcileSingleElement 处理
      // ...其他case
    }
  }
  if (typeof newChild === 'string' || typeof newChild === 'number') {
    // 调用 reconcileSingleTextNode 处理
  }
  if (isArray(newChild)) {
    // 调用 reconcileChildrenArray 处理
  }
  // 一些其他情况调用处理函数
  // 以上都没有命中,删除节点
  return deleteRemainingChildren(returnFiber, currentFirstChild);
}

这里的newChild参数就是本次更新的 JSX 对象(对应ClassComponent的this.render方法返回值,或者FunctionComponent执行的返回值)

不同类型的Diff是如何实现的

我们可以从同级的节点数量将Diff分为两类:

  1. 当newChild类型为object、number、string,代表同级只有一个节点

  2. 当newChild类型为Array,同级有多个节点

接下来,我们分别讨论。

情况一:同级只有一个节点的Diff

对于单个节点,我们以类型object为例,会进入reconcileSingleElement

const isObject = typeof newChild === 'object' && newChild !== null;
  if (isObject) {
    // 对象类型,可能是 REACT_ELEMENT_TYPE 或 REACT_PORTAL_TYPE
    switch (newChild.$$typeof) {
      case REACT_ELEME
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值