最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • react原理:协调算法,reconcile(diff算法)

    正文概述 掘金(争霸爱好者)   2021-07-23   669

    讲解完函数组件和类组件是如何计算状态更新之后,这篇文章讲一下reconcile的流程,也就是我们俗称的diff算法。

    类组件的diff入口在finishClassComponent

    function finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes) {
      markRef(current, workInProgress);
      var didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags;
    
      if (!shouldUpdate && !didCaptureError) {
        // 根据shouldComponentUpdate生命周期决定是否需要更新组件
        if (hasContext) {
          invalidateContextProvider(workInProgress, Component, false);
        }
    
        return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
      }
    
      var instance = workInProgress.stateNode;
    
      ReactCurrentOwner$1.current = workInProgress;
      var nextChildren;
    
      if (didCaptureError && typeof Component.getDerivedStateFromError !== 'function') {
        // 出现错误
        nextChildren = null;
        {
          stopProfilerTimerIfRunning();
        }
      } else {
        {
          setIsRendering(true);
          // 执行render方法
          nextChildren = instance.render();
          if ( workInProgress.mode & StrictMode) {
            // 严格模式
            disableLogs();
            try {
              instance.render();
            } finally {
              reenableLogs();
            }
          }
          setIsRendering(false);
        }
      }
    
      workInProgress.flags |= PerformedWork;
    
      if (current !== null && didCaptureError) {
        forceUnmountCurrentAndReconcile(current, workInProgress, nextChildren, renderLanes);
      } else {
        // diff的入口
        reconcileChildren(current, workInProgress, nextChildren, renderLanes);
      }
    
      workInProgress.memoizedState = instance.state;
    
      if (hasContext) {
        invalidateContextProvider(workInProgress, Component, true);
      }
    
      return workInProgress.child;
    }
    

    对于函数组件,会在updateFunctionComponent中,renderWithHooks之后,调用reconcileChildren进入diff

    入口函数

    reconcileChildren方法定义如下:

    function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
      if (current === null) {
        workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
      } else {
        workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
      }
    }
    

    当组件是初次加载时,会执行mountChildFibers方法,更新时执行reconcileChildFibers,这两个方法定义如下:

    var reconcileChildFibers = ChildReconciler(true);
    var mountChildFibers = ChildReconciler(false);
    

    再看ChildReconciler

    function ChildReconciler(shouldTrackSideEffects) {
        // ... 
        return reconcileChildFibers
    }
    

    shouldTrackSideEffects表示是否有副作用。当组件初次挂载时,显然是没有副作用的,而组件更新可能会涉及到元素的删除,插入等操作,因此shouldTrackSideEffectstrue。接下来看这个方法的返回值:reconcileChildFibers

    function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
        var isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null;
    
        if (isUnkeyedTopLevelFragment) {
          newChild = newChild.props.children;
        }
    
        var isObject = typeof newChild === 'object' && newChild !== null;
    
        if (isObject) {
          switch (newChild.$$typeof) {
            case REACT_ELEMENT_TYPE:
              return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));
          }
            // ...
        }
        
        // ...
    
        if (isArray$1(newChild)) {
          return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
        }
        // ...
    }
    

    这个方法会根据不同的节点类型来进行对应的diff操作:比如对于单个react元素,会执行reconcileSingleElement,对于多个元素,执行reconcileChildrenArray。这也就是单节点diff和多节点diff,下面就会分析这两种算法流程。再开始分析之前,先看一下reconcileChildFibers的参数:

    react原理:协调算法,reconcile(diff算法)

    单节点diff

    单节点diff是指新的节点为单个节点时的diff流程。单节点diff由三种可能的情况:

    1. fiber为空
    2. fiber有一个节点
    3. fiber有多个节点

    单节点diff比较简单,只需要在老fiber中找到keytype与新的jsx节点都相同节点,然后删除剩余老节点即可。如果找不到,删除所有老节点,创建新的节点。

      function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
        var key = element.key;
        var child = currentFirstChild;
        // 循环currentFirstChild这一层的所有老fiber节点
        while (child !== null) {
          if (child.key === key) {
            switch (child.tag) {
              // ...
              default:
                {
                  if (child.elementType === element.type || (
                   isCompatibleFamilyForHotReloading(child, element) )) {
                    // 因为是单节点diff,所以找到key和type均相同的节点后,直接删除所有剩余节点即可
                    deleteRemainingChildren(returnFiber, child.sibling);
    		// 根据老fiber创建新fiber
                    var _existing3 = useFiber(child, element.props);
                    _existing3.ref = coerceRef(returnFiber, child, element);
                    _existing3.return = returnFiber;
    		// 。。。
                    // 当找到key和type均相同的节点时,直接return新fiber
                    return _existing3;
                  }
                  break;
                }
            }
    	// key相同,但是type不同
            deleteRemainingChildren(returnFiber, child);
            break;
          } else {
            // key不同,直接删除遍历到的老fiber
            deleteChild(returnFiber, child);
          }
          child = child.sibling;
        }
        // 当没有找到key和type均相同的节点时,根据jsx创建新fiber
        if (element.type === REACT_FRAGMENT_TYPE) {
          var created = createFiberFromFragment(element.props.children, returnFiber.mode, lanes, element.key);
          created.return = returnFiber;
          return created;
        } else {
          var _created4 = createFiberFromElement(element, returnFiber.mode, lanes);
    
          _created4.ref = coerceRef(returnFiber, currentFirstChild, element);
          _created4.return = returnFiber;
          return _created4;
        }
      }
    

    这里需要注意几个点:

    1. 在执行deleteChilddeleteRemainingChildren内也会调用deleteChild)时会为要删除的就fiber打上Deletiontag,表示这个旧的节点要被删除(注意,并不是真的删除这个节点,而是打上tag)。

    2. 如果要被删除的节点还有子节点,只会在要被删除的节点上打上tag,不会在其子节点上打tag

    3. 当找到可复用的fiber节点时(key和type相同),会创建一个新的fiber节点,并建立新的fiber节点和旧的fiber节点之间的联系,即设置alternate属性。但是当不能复用时,新旧fiber之间的alternate连接是不存在的。

    4. 细心的同学可能发现了,既然旧fiber会被打上Deletion的tag,那么新fiber节点呢?注意reconcileChildFibers有这样的代码:

      return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));
      

      placeSingleChild方法中才会对新增的节点打上Placement的tag

    多节点diff

    单节点diff看完后,来看一下多节点diff。回到reconcileChildFibers方法,这里会做一个特殊处理

    function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
        var isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null;
        // 如果新的jsx节点是没有key的Fragment节点,则取出它的children
        if (isUnkeyedTopLevelFragment) {
          newChild = newChild.props.children;
        }
        // 多节点diff
        if (isArray$1(newChild)) {
          return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
        }
    }
    

    可以看到,多节点diff就是新的jsx存在多个节点时的diff策略。

    这里首先说明一下,reactdiff策略并不是不惜代价地复用节点,而是在保证效率的基础上进行复用。比如一个组件发生了跨层级的移动,虽然只是位置上的变化,但是react则不会复用这个节点,这一点很多文章里也讲过,就不展开了。下面讲解一下react的多节点diff策略。

    首先是几个变量

    var resultingFirstChild = null;   // 新fiber链表中的第一个fiber节点
    
    var newIdx = 0;                   // 用于循环新jsx数组中的指针
    var previousNewFiber = null;      // 新fiber链表中,当前fiber的前一个fiber
    
    var oldFiber = currentFirstChild; // 用于循环旧fiber节点的指针
    var nextOldFiber = null;          // 老fiber链表中,当前fiber的下一个fiber
    
    // 老fiber树中最靠右的一个不需要移动的fiber节点,在老fiber树中的位置,下文会讲到
    var lastPlacedIndex = 0;          
    

    第一部分

    首先看多节点diff的第一部分:针对节点更新的循环

    // 循环新fiber和老fiber
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      // 更新fiber节点
      // 如果新的jsx返回null,或者新老fiber的key不同,updateSlot返回null
      // 如果新老fiber的key相同,但是type不同,或者老fiber不存在,说明fiber不能复用(注意前文提到的fiber复用的含义)
      // 如果新老fiber都存在,并且能够复用,则复用fiber
      var newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);
      // newFiber为null时,直接跳出循环
      if (newFiber === null) {
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      // 组件更新,shouldTrackSideEffects为true
      // 组件挂载,shouldTrackSideEffects为false,前文有提到
      if (shouldTrackSideEffects) {
        // 如果没有发生fiber复用,说明老fiber被删除
        if (oldFiber && newFiber.alternate === null) {
          deleteChild(returnFiber, oldFiber);
        }
      }
      // 确定新fiber的位置,placeChild方法下文会单独讲
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      // 记录新fiber中的第一个节点
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }
    

    react原理:协调算法,reconcile(diff算法)

    可以看出,只有在新的jsx返回null,或者新老fiberkey不同时,才会中途跳出循环。如果中途跳出了循环,会跳过下文的第二和第三部分,直接进入第四部分

    第二部分

    如果循环正常结束,没有中途跳出,会进入第二部分:

    // 如果新fiber遍历完毕,直接删除旧fiber中的剩余节点即可,并返回resultingFirstChild
    if (newIdx === newChildren.length) {
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }
    

    第三部分

    接下来是第三部分:

    // 如果老fiber遍历结束,则剩余的新fiber都是新增节点,直接新增即可
    if (oldFiber === null) {
      for (; newIdx < newChildren.length; newIdx++) {
        var _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;
    }
    

    这里需要注意,如果第一部分的循环中途退出,则新旧fiber都不会遍历完毕,因此是不会进入第二和第三部分的,而是会直接进入第四部分。

    第四部分

    // 首先将老fiber链表中没有遍历到的剩余节点放到一个map中,key是fiber的key或者在老fiber链表中的位置索引,value是fiber节点
    var existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    
    // 循环新的jsx数组中的剩余部分
    for (; newIdx < newChildren.length; newIdx++) {
      // 如果新的jsx返回null,updateFromMap返回null,跳过本轮循环
      // 新jsx不返回null,从existingChildren中找到与新jsx的key相同的老fiber,看是否能够复用fiber
      var _newFiber2 = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes);
    
      if (_newFiber2 !== null) {
        if (shouldTrackSideEffects) {
          if (_newFiber2.alternate !== null) {
            // 当新fiber非空,并且新fiber复用了老fiber,说明新老fiber存在对应关系,从existingChildren中删除老fiber
            existingChildren.delete(_newFiber2.key === null ? newIdx : _newFiber2.key);
          }
        }
        // 放置新fiber
        lastPlacedIndex = placeChild(_newFiber2, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = _newFiber2;
        } else {
          previousNewFiber.sibling = _newFiber2;
        }
        previousNewFiber = _newFiber2;
      }
    }
    

    react原理:协调算法,reconcile(diff算法)

    第五部分

    最后还有一个收尾工作,遍历existingChildren,删除掉其中的旧fiber节点,并返回新fiber链表的第一个节点

    if (shouldTrackSideEffects) {
      existingChildren.forEach(function (child) {
        return deleteChild(returnFiber, child);
      });
    }
    
    return resultingFirstChild;
    

    这样,beginWork就会拿到这个函数的返回值,并返回到performUnitOfWork中,用来修改全局变量workInProgress,从而继续执行workLoopSync循环。

    placeChild方法

    placeChild是用来确定新fiber节点在新fiber链表中的位置,并返回前文中提到的lastPlacedIndex。接下来看一下代码

    function placeChild(newFiber, lastPlacedIndex, newIndex) {
        // 确定新fiber节点的位置索引
        newFiber.index = newIndex;
        if (!shouldTrackSideEffects) {
          // 不采取任何操作
          return lastPlacedIndex;
        }
        var current = newFiber.alternate;
        if (current !== null) {
          // current不为null,说明新老fiber有关联
          var oldIndex = current.index;
          if (oldIndex < lastPlacedIndex) {
            // 老fiber在lastPlacedIndex左边,无需更新lastPlacedIndex
            newFiber.flags = Placement;
            return lastPlacedIndex;
          } else {
            // 老fiber在lastPlacedIndex的右边,说明在新fiber链表中,对应节点发生了移动
            return oldIndex;
          }
        } else {
          // current为null,说明新老fiber没有关联,直接插入新fiber
          newFiber.flags = Placement;
          return lastPlacedIndex;
        }
    }
    

    下面举个例子:

    旧fiber链表
    A -> B -> C -> D -> E
    新fiber链表
    A -> B -> D -> E -> C
    
    1. newIndex为0,进入第一部分的循环,执行到placeChild方法,由于新老fiber节点存在关联,因此current不为空,而oldIndexlastPlacedIndex都是0,因此返回了oldIndex
    2. newIndex为1,和第一步流程相同,也返回了oldIndex(1)lastPlacedIndex变为1
    3. newIndex为2,跳出第一部分的循环,进入第四部分,执行到placeChild方法,newFiber为D节点,current为老fiber链表的D节点,因此current不为空,oldIndex为3,lastPlacedIndex为1,因此返回3,lastPlacedIndex变为3
    4. newIndex为3,和第3步流程相同,lastPlacedIndex变为4
    5. newIndex为4,执行到placeChild方法,newFiber为C节点,current为老fiber链表的C节点,current不为空,oldIndex为2,lastPlacedIndex为4,此时oldIndex小于lastPlacedIndex,因此react认为C节点发生了移动,为其打上Placement的tag

    因此,lastPlacedIndex的含义就是:在老fiber链表中,最靠右的一个不需要移动的fiber节点,在老fiber链表中的位置索引。

    最后再来个整体的流程图吧

    react原理:协调算法,reconcile(diff算法)


    起源地下载网 » react原理:协调算法,reconcile(diff算法)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元