Wsh's blog Wsh's blog
首页
  • 基础知识
  • ArkUI
  • UIAbility
  • 组件通信方式
  • 前端缓存
  • React
  • typescript
  • javascript
  • flutter
  • node
  • webpack
web3D😉
宝库📰
  • 分类
  • 标签
  • 归档
龙哥的大🐂之路 (opens new window)
GitHub (opens new window)

wsh

热爱前端的程序媛
首页
  • 基础知识
  • ArkUI
  • UIAbility
  • 组件通信方式
  • 前端缓存
  • React
  • typescript
  • javascript
  • flutter
  • node
  • webpack
web3D😉
宝库📰
  • 分类
  • 标签
  • 归档
龙哥的大🐂之路 (opens new window)
GitHub (opens new window)
  • react基础

  • react更新特性

  • react进阶

    • react 架构演变
    • react render阶段
    • react commit阶段
    • react diff
      • react 状态更新
      • react hook
      • Concurrent Mode
    • react
    • react进阶
    2022-04-22
    目录

    react diff

    # 01. diff 算法时机

    是在render阶段 beginWork时,针对update组件,将当前组件与 当前组件上次更新时的对应的Fiber节点进行对比(也就是我们所说的diff算法),将对比结果生成新的Fiber节点。

    提示

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

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

    Diff 算法的本质是对比1,4,生成2

    # 02. Diff的缺陷以及React的改进

    由于前后将两棵树完全对比的算法的复杂程度为o(n3),n是树中元素的数量。

    假设有1000个元素进行对比,那就需要10亿次的计算量。

    为了降低这个复杂度,React的diff 设置了三个限制:

    1. 只对同级元素进行diff,如果一个DOM节点在前后两次更新中跨越了层级,那么react不会复用
    2. 两个不同类型的元素产生出不同的树,如果元素由div变为p,react会销毁div以及子孙节点,将新建p以及子孙节点
    3. 可以通过key prop来表示哪些元素可以在不同的渲染下保持稳定。

    # 03. Diff 是如何实现的?

    入口函数:reconcileChildFibers 该函数会根据newChild(即JSX对象)类型调用不同的处理函数

    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:
               // 调用reconclieSingleElement 处理
           }
        }
    
        if (typeof newChild === 'string' || typeof newChild === 'number'){
          // 调用reconclieSingNode处理
        }
    
        if (isArray(newChild)) {
          // 调用reconclieChildFibers处理
        }
        // ...
    
       // 都没有命中,就会删除节点
       return deleteRemainingChildren(returnFiber, currentFistChild)
    }
    

    从同级的节点数量将Diff分为两类:

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

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

    # 04. 单节点diff

    单节点入口:reconcileSingleElement,以object为例:

    if (typeof newChild === 'object' && newChild !== null) {
        switch(newChild.$$typeof) {
            case REACT_ELEMENT_TYPE:
              return palceSingChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes))
            // ...
        }
    }
    

    reconcileSingleElement的作用是:会判断上次更新的Fiber节点是否存在对应的DOM节点 => DOM节点是否可以复用,最后会返回workInProgress fiber节点(可能是新生成的也可能是将上次更新的Fiber节点的副本作为本次新生成的的fiber节点)

    # DOM节点是否可以复用?

    function reconcileSingleElement (
        returnFiber: Fiber,
        currentFirstChild: Fiber | null,
        element: ReactElement
    ): Fiber {
        const key = element.key;
        let child = currentFirstchild;
    
        // 1. 是否存在对应的DOM节点
        while(child !== null) {
          // 上一次存在更新的DOM节点
    
          // 首先比较key是否相同
          if (child.key === key) {
              // key相同, 接下来比较type是否相同
    
              if (elementType === REACT_FRAGMENT_TYPE) {
                if (child.tag === Fragment) {
                  return _existing;
                }
              } else {
                if (child.elementType === elementType) {
                  return _existing;
                }
              }
              // key相同但是type不同
              // 将该fiber及其兄弟fiber标记为删除
              deleteRemainingChildren(returnFiber, child);
          } else {
              // key不同,将该fiber标记为删除
              deleteChild(returnFiber, child);
          }
          child = child.sibling;
        }
    
        // 根据类型,调用不同的生成逻辑 createFiberFromFragment | createFiberFromElement ,返回。
         
    }
    

    # deleteRemainingChildren | deleteChild 区别

    在child !== null时, key相同,而type不相同 表示我们已经找到本次更新的元素 对应的上次fiber节点,而type不相同,不能复用。 而key不相同,只是说明当前fiber 不能被本次更新的元素 复用,后面还有兄弟fiber 咩有遍历到,所以在这个阶段只能标记当前fiber 删除。

    # 05. 多节点diff

    ul > li*2 为例 ,返回值JSX对象的children属性不是单一节点,而是包含2个对象的数组。

    调试如下:

    if (isArray(newChild)) {
      return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
    }
    
    • 节点更新(节点属性变化 | 节点类型更新)

    • 节点新增或者减少

    • 节点位置变化(顺序变化)

    同级多个节点的diff 属于以上三种类型的一种或者多种。

    # diff 思路

    提示

    虽然本次更新的JSX对象 newChildren为数组形式,但是和newChildren中每个组件进行比较的是current fiber,同级的Fiber节点是由sibling指针链接形成的单链表,即不支持双指针遍历。

    即 newChildren[0]与fiber比较,newChildren[1]与fiber.sibling比较。

    所以无法使用双指针优化。

    diff 算法经历两轮遍历。

    1. 处理更新的节点。
    2. 处理剩下的不属于更新的节点

    # 第一次遍历: 处理更新的节点

    1. let i = 0,遍历newChildren,将newChildren[i]与oldFiber比较,判断DOM节点是否可复用。

    2. 如果可复用,i++,继续比较newChildren[i]与oldFiber.sibling,可以复用则继续遍历。

    3. 如果不可复用,分两种情况:

      • key不同导致不可复用,立即跳出整个遍历,第一轮遍历结束。

      • key相同type不同导致不可复用,会将oldFiber标记为DELETION,并继续遍历

    4. 如果newChildren遍历完(即i === newChildren.length - 1)或者oldFiber遍历完(即oldFiber.sibling === null),跳出遍历,第一轮遍历结束。

    带着第一次遍历的结果进行第二次遍历。

    # 第二次遍历: 处理剩下的不属于更新的节点

    • 情况1: newChildren与oldFiber同时遍历完

    说明 在第一轮遍历进行组件更新。Diff结束

    • 情况2: newChildren 没有遍历完,oldFiber遍历完

    说明有新节点插入。 我们只需要遍历剩下的newChildren为生成的workInProgress fiber依次标记Placement。

    if (oldFiber === null) {
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
        if (newFiber === null) {
          continue;
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
          previousNewFiber = newFiber;
        }
        return resultingFirstChild;
    }
    
    • 情况3: newChildren 遍历完,oldFiber没有遍历

    说明有节点被删除了。所以需要遍历剩下的oldFiber,依次标记Deletion。

    if (newIdx === newChildren.length) {
      //我们已经到了newChildren的尽头。 我们可以删除其余的节点
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }
    
    • 情况4: newChildren和oldFiber都没有遍历

    说明有节点更新了位置

    reconcileChildrenArray

        // 将所有子项添加到键映射以进行快速查找。
        const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
        for (; newIdx < newChildren.length; newIdx++) {
          var _newFiber2 = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes);
    
          if (_newFiber2 !== null) {
            if (shouldTrackSideEffects) {
              if (_newFiber2.alternate !== null) {
                // 新的fiber正在进行中,但是如果它已经在当前产生,这意味着我们可以复用这个fiber.我们需要从子列表中删除它,这样我们就不会添加到删除列表中
                existingChildren.delete(_newFiber2.key === null ? newIdx : _newFiber2.key);
              }
            }
            // 最后一个可复用的节点在oldFiber中的位置索引
            lastPlacedIndex = (_newFiber2, lastPlacedIndex, newIdx);
    
            if (previousNewFiber === null) {
              resultingFirstChild = _newFiber2;
            } else {
              previousNewFiber.sibling = _newFiber2;
            }
    
            previousNewFiber = _newFiber2;
          }
        }
    
      function mapRemainingChildren(returnFiber, currentFirstChild) {
        var existingChildren = new Map();
        var existingChild = currentFirstChild;
    
        while (existingChild !== null) {
          if (existingChild.key !== null) {
            existingChildren.set(existingChild.key, existingChild);
          } else {
            existingChildren.set(existingChild.index, existingChild);
          }
    
          existingChild = existingChild.sibling;
        }
    
        return existingChildren;
      }
    

    # 处理移动的节点

    • oldIndex: 遍历到的可复用节点在oldFiber中的位置索引
    • lastPlaceIndex:最后一个可复用的节点在oldFiber中的位置索引

    lastIndex是不断更新的,表示访问过的节点在集合中的最右的位置。若当前访问节点在旧集合中的位置比lastPlaceIndex大,即靠右,说明它不会影响其他元素的位置,因此不用添加到差异队列中,不执行移动操作,反之则进行移动操作。

    • 如果oldIndex < lastPlacedIndex,代表本次更新该节点需要向右移动。
    • lastPlacedIndex初始为0,每遍历一个可复用的节点,如果oldIndex >= lastPlacedIndex,则lastPlacedIndex = oldIndex。

    #react
    react commit阶段
    react 状态更新

    ← react commit阶段 react 状态更新→

    最近更新
    01
    组件通信方式
    01-07
    02
    UIAbility
    01-07
    03
    ATKTS
    01-06
    更多文章>
    Theme by Vdoing | Copyright © 2022-2025 Wsh | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式