react---diff算法

66 阅读6分钟

摘要

一个DOM节点早某一时刻最多会有4个节点和他相关

  • current Fiber,如果该DOM节点已在页面中。current Fiber代表该DOM节点对应的Fiber节点
  • workInProgress Fiber,如果该DOM节点将在本次更新中渲染到页面中,workInProgress Fiber代表该DOM节点对应的Fiber节点
  • DOM节点本身
  • JSX对象,即ClassComponent的render方法的返回结果,或FunctionComponent的调用结果,JSX对象中包含描述DOM节点的信息

【DIff算法的本质就是对比1和4,生成2】

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

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

  • 只对同级元素进行Diff,如果一个DOM节点在前后两次更新中跨域了层级,那么React不会尝试复用他
  • 两个不同类型的元素会产生不同的树,如果元素由div变为p,React会销毁div及其子孙节点,并新建p及其子孙节点
  • 开发者可以通过key prop来暗示哪些子元素在不同的渲染下能保持稳定

执行reconcileChildFibers会判断节点的类型,主要分为三种

  • 单一节点:执行reconcileSingleTextNode
  • 数组:执行reconcileChildrenArray
  • 可迭代的对象:执行reconcileChildrenIterator

Diff算法--单一节点

DOM节点是否可以复用的依据

  • 更新前后type是否相同
  • 更新前后key是否相同(节点没有设置key时,key为null)

1.在reconcileChildFibers中进入reconcileSingleElement函数,判断currenFiber是否存在,如果不存在则执行对应的创建函数然后返回新创建的fiber节点。如果此时的currentFiber存在,则判断currentFiber和JSX对象的type和key是否相同,相同则调用useFiber函数,在这个函数中会调用createWorkInprogress函数返回当前fiber节点的副本,否则重新创建一个新的fiber节点

2.如果当前currentFiber存在,但是更新前后的key不一样,则会执行deleteChild函数,将当前fiber节点删除,然后将这个删除的fiber节点的兄弟节点赋值给当前fiber节点,然后继续判断,这样是为了解决以下类似情况

更新前:ul>li*3

更新后:ul>p

如果所有的节点都不满足更新后的节点会执行对应的创建函数

3.如果当前节点存在,但是更新前后的type不一样,则会执行deleteRemainingChildren函数,将currentFiber节点以及它所有的兄弟节点标记为删除,(删除兄弟节点是因为,这个时候的兄弟节点的key一定和JSX返回对象的key值不同,因为对比type的前提时两个节点的key值相同,key值是唯一的)

function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    const key = element.key;//JSX对象的key值
    let child = currentFirstChild;//将当前Fiber节点赋值给child
    while (child !== null) {//判断child是否不存在,当处于mount阶段时,child就不存在
      if (child.key === key) {//判断更新前后key是否相同
        const elementType = element.type;
        if (elementType === REACT_FRAGMENT_TYPE) {//判断更新前后type是否相同
          if (child.tag === Fragment) {
            deleteRemainingChildren(returnFiber, child.sibling);
            const existing = useFiber(child, element.props.children);
            existing.return = returnFiber;
            if (__DEV__) {
              existing._debugSource = element._source;
              existing._debugOwner = element._owner;
            }
            return existing;
          }
        } else {
          if (
            child.elementType === elementType ||//判断更新前后type值是否相同,前提是key值相同
            (__DEV__
              ? isCompatibleFamilyForHotReloading(child, element)
              : false) ||
            (enableLazyElements &&
              typeof elementType === 'object' &&
              elementType !== null &&
              elementType.$$typeof === REACT_LAZY_TYPE &&
              resolveLazy(elementType) === child.type)
          ) {
            deleteRemainingChildren(returnFiber, child.sibling);
            const existing = useFiber(child, element.props);//如果节点可以复用,则调用这个函数,在这个函数中调用createWorkInProgress()函数创建当前fiber节点的副本并返回
            existing.ref = coerceRef(returnFiber, child, element);
            existing.return = returnFiber;
            if (__DEV__) {
              existing._debugSource = element._source;
              existing._debugOwner = element._owner;
            }
            return existing;//返回创建的副本节点
          }
        }
        deleteRemainingChildren(returnFiber, child);//如果type值不同则调用这个函数,将当前的fiber节点以及该节点的兄弟节点一起删除
        break;//跳出循环
      } else {//如果key值不同
        deleteChild(returnFiber, child);//删除当前节点
      }
      child = child.sibling;//将curretFiber节点的兄弟节点赋值给child,如果child存在则会继续循环
    }
        
        //需要创建新的fiber节点,根据不同的类型调用不同的函数来创建新的fiber节点
    if (element.type === REACT_FRAGMENT_TYPE) {
      const created = createFiberFromFragment(
        element.props.children,
        returnFiber.mode,
        lanes,
        element.key,
      );
      created.return = returnFiber;
      return created;
    } else {
      const created = createFiberFromElement(element, returnFiber.mode, lanes);
      created.ref = coerceRef(returnFiber, currentFirstChild, element);
      created.return = returnFiber;
      return created;
    }
  }

Diff算法--同级多节点

分三种情况

1.节点更新

//情况一---节点属性的变化
//之前
<ul>
    <li key='0' className='before'>0</li>
    <li key='1'>1</li>
</ul>
//之后
<ul>
    <li key='0' className='after'>0</li>
    <li key='1'>1</li>
</ul>
//情况二---节点类型更新
//之前
<ul>
    <li key='0'>0</li>
    <li key='1'>1</li>
</ul>
//之后
<ul>
    <div key='0'>0</div>
    <li key='1'>1</li>
</ul>

2.节点新增或减少

//情况一---新增节点
//之前
<ul>
    <li key='0'>0</li>
    <li key='1'>1</li>
</ul>
//之后
<ul>
    <li key='0'>0</li>
    <li key='1'>1</li>
    <li key='2'>2</li>
</ul>
//情况二---删除节点
//之前
<ul>
    <li key='0'>0</li>
    <li key='1'>1</li>
</ul>
//之后
<ul>
    <li key='0'>0</li>
</ul>

3.节点位置变化

//之前
<ul>
    <li key='0'>0</li>
    <li key='1'>1</li>
</ul>
//之后
<ul>
    <li key='1'>1</li>
    <li key='0'>0</li>
</ul>

在日常开发中,相较于新增删除更新组件发生的频率更高,所以Diff算法会优先判断当前节点是否属于更新

【注意】:

在我们做数组相关的算法题时,经常使用双指针从数组头部和尾部同时遍历以提高效率,但是这里不可以,虽然本次更新的JSX对象为数组形式,但是newChild中每个组件进行比较的是current filber节点,同级的fiber节点是由sibling指针链接形成的单链表,即不支持双指针遍历,所以Diff算法的整体逻辑会经历两轮遍历

  • 第一轮遍历:处理更新节点
  • 第二轮遍历:处理剩下的不属于更新的节点

第一轮遍历

  • let i=0;,遍历newChildren,将newChildren[i]与oldFiber比较,判断DOM节点是否复用
  • 如果可复用,i++,继续比较newChildren与oldFiber.sibling,可以复用则继续遍历
  • 如果不可复用,分两种情况:
    • key不同导致不可复用,立即跳出整个遍历,第一轮遍历结束
    • key相同,type不同导致不可复用,会将oldFiber标记为DELETION,并继续遍历
  • 如果newChildren遍历完或者oldFiber遍历完,跳出遍历,第一轮遍历结束

第二轮遍历

  • 第一轮遍历以步骤四跳出的第一轮遍历,存在三种情况

    //之前
    <li key='0' className='a'>0</li>
    <li key='1' className='b'>1</li>
    //之后 情况一---newChildren与oldFiber都遍历完成
    //删除所有没有遍历的oldFiber
    <li key='0' className='aa'>0</li>
    <li key='1' className='bb'>1</li>
    //之后 情况二---newChildren没遍历完,oldFiber遍历完
    //将剩下的newChildren遍历完,创建对应的新的fiber节点
    <li key='0' className='a'>0</li>
    <li key='1' className='b'>1</li>
    <li key='2' className='c'>2</li>
    //之后 情况三---newChildren遍历完,oldFiber没遍历完
    //删除所有没有遍历的oldFiber
    //这种情况只剩下一个节点,会进入单一节点的diff算法
    <li key='0' className='a'>0</li>
    
  • 第一轮遍历以步骤三跳出

    //之前
    <ul>
        <li key='0'>0</li>
        <li key='1'>1</li>
        <li key='2'>2</li>
    </ul>
    //之后
    <ul>
        <li key='0'>0</li>
        <li key='2'>2</li>
        <li key='1'>1</li>
    </ul>
    
    

    为了可以在O(1)的复杂度中到key对应的fiber节点,我们会将oldFiber存储到一个Map中,以key为key值,fiber节点为value,然后遍历newChildren,然后在Map中找是都有对应的oldFiber,如果没有的话则重新创建新的fiber节点,否则直接复用,并且将这个oldFiber在Map中删除。在遍历完newFiber后,如果Map不为空,则遍历Map将这些oldFiber标记为删除。