React的render阶段流程概览

979 阅读5分钟

本文系列较为基础,适合react新手阅读。阅读过程中如果发现任何错误,麻烦评论区指正.
React版本:React17.0.0。

引言

本文旨在将render过程捋顺,优先级和调度器内容非必要不会涉及。

performSyncWorkOnRoot

首先我们从上一篇文章结尾的performSyncWorkOnRoot函数开始梳理,下边是经过极限简化版的performSyncWorkOnRoot函数:

function performSyncWorkOnRoot(root) {

  var exitStatus;
  
  exitStatus = renderRootSync(root, lanes); // render阶段入口

  var finishedWork = root.current.alternate;
  root.finishedWork = finishedWork; //effectList相关
  root.finishedLanes = lanes;
  commitRoot(root); // commit 入口
  ensureRootIsScheduled(root, now());
  return null;
}

在这个函数中,我们可以很清晰的看到render阶段和commit的入口。这篇文章只了解render阶段,也就是 renderRootSync函数。

renderRootSync

function renderRootSync(root, lanes) {

 if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
     //初始化是workInProgressRoot为空,在prepareFreshStack函数中会为workInProgressRoot进行赋值
     //同时为workInProgress进行赋值,此时为RootFiber节点。
    prepareFreshStack(root, lanes);
  }

  do {
    try {
      workLoopSync();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);

} 

function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}
  1. 在prepareFreshStack函数中为workInProgress赋值,保证工作循环的开展。
  2. 在renderRootSync中会调用workLoopSync函数,该函数的作用主要是开启一个同步的工作循环,用于构建和更新React组件的Fiber树。在工作循环中,会循环调用performUnitOfWork创建Fiber节点。

performUnitOfWork

function performUnitOfWork(unitOfWork) {
  var current = unitOfWork.alternate;// currentFiber
  var next;

  next = beginWork$1(current, unitOfWork, subtreeRenderLanes);//深入优先遍历Fiber的“递”

  unitOfWork.memoizedProps = unitOfWork.pendingProps;//更新Fiber节点上的props

  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork);// 深入优先遍历的“归”
  } else {
    workInProgress = next;
  }

  ReactCurrentOwner$2.current = null;
}

在performUnitOfWork函数中,会调用beginWork$1和completeUnitOfWork,这两个方法中会分别调用beginWork和completeWork,这两个方法就是构建fiber树的"递归"过程。

beginwork(Mount)
function beginWork(current, workInProgress, renderLanes) {
  var updateLanes = workInProgress.lanes;

  if (current !== null) {
    var oldProps = current.memoizedProps;
    var newProps = workInProgress.pendingProps;

    if (oldProps !== newProps || hasContextChanged() || ( 
     workInProgress.type !== current.type )) {
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      didReceiveUpdate = false; 

      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    } else {
      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
  
        didReceiveUpdate = true;
      } else {
        didReceiveUpdate = false;
      }
    }
  } else {//mount阶段
    didReceiveUpdate = false;
  }

  workInProgress.lanes = NoLanes;

  switch (workInProgress.tag) {//匹配fiber阶段的tag 进入到不同处理方法中
    //...

    case FunctionComponent://函数组件的处理方法
      {
        var _Component = workInProgress.type;
        var unresolvedProps = workInProgress.pendingProps;
        var resolvedProps = workInProgress.elementType === _Component ? unresolvedProps : resolveDefaultProps(_Component, unresolvedProps);
        return updateFunctionComponent(current, workInProgress, _Component, resolvedProps, renderLanes);
      }


    case HostRoot:// 根节点的处理方法
      return updateHostRoot(current, workInProgress, renderLanes);

    case HostComponent: //可对应一个DOM的Fiber节点的处理方法
      return updateHostComponent(current, workInProgress, renderLanes);
        //....

  }

}

beginWork的入参解析:

  1. current:当前已经渲染完成的Fiber节点
  2. workInProgress:正在构建新的Fiber树的节点
  3. renderLanes:优先级相关本文不涉及

beginWork函数首先会根据current是否存在来判断是mount阶段还是update阶段。
mount阶段:就会根据workInProgress Fiber节点的tag来判断Fiber节点的类型,然后进入到相应的节点处理方法。
update阶段:则会判断能否进行复用

  1. oldProps === newProps && workInProgress.type === current.type,即props与fiber.type不变
  2. !includesSomeLane(renderLanes, updateLanes),即当前Fiber节点优先级不够。

满足以上两个条件:进入到bailoutOnAlreadyFinishedWork方法中进行复用当前的Fiber节点,也就是current。
不满足:流程会走到,updateXXX处理方法,会在方法内部进行区分是mount还是update。

reconcileChildren

下边主要看对应DOM节点的Fiber节点类型如何处理。updateHostComponent函数中主要调用的是reconcileChildren函数:

function updateHostComponent(current, workInProgress, renderLanes) {
  //....
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}
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);
  }
}
  1. RootFiber节点获取到子JSX节点是通过先前创建的update,通过计算RootFiber的state得到的。
  2. 根组件,也就是App组件的子JSX节点是通过执行函数组件,获取其返回值得到的。
  3. 对应DOM节点的Fiber节点获取子JSX节点,是在处理函数组件时,已经确立了JSX节点之间的阶段关系,放在了pendingProps中。

可以看到reconcileChildren的流程是:判断current,走对应的流程函数,并将返回值(处理好的Fiber)赋值给workInProgress.child。并且之后持续进行工作循环的条件就是workInProgress不为null。 所以beginWork的作用可以理解为根据已经存在workInProgress Fiber节点和节点的子JSX节点元素去构建下一个workInProgress

mountChildFibers与reconcileChildFibers这两个方法的逻辑基本一致。唯一的区别是:reconcileChildFibers会为生成的fiber节点带上effectTag属性,而mountChildFibers不会。

这块使用了闭包,利用ChildReconciler函数的作用域保存了shouldTrackSideEffects变量,mountChildFibers就会走shouldTrackSideEffects为false的逻辑,即不为生成的fiber节点架effectTag标记。reconcileChildFibers方法则会添加。

  function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
    var isObject = typeof newChild === 'object' && newChild !== null;

    if (isObject) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));

        case REACT_PORTAL_TYPE:
          return placeSingleChild(reconcileSinglePortal(returnFiber, currentFirstChild, newChild, lanes));

        case REACT_LAZY_TYPE:
          {
            var payload = newChild._payload;
            var init = newChild._init; // TODO: This function is supposed to be non-recursive.

            return reconcileChildFibers(returnFiber, currentFirstChild, init(payload), lanes);
          }

      }
    }

    if (typeof newChild === 'string' || typeof newChild === 'number') {
      return placeSingleChild(reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, lanes));
    }

    if (isArray$1(newChild)) {
      return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
    }

    //...


    return deleteRemainingChildren(returnFiber, currentFirstChild);
  }

进入mountChildFibers方法,实际上是进入了在ChildReconciler函数内部定义的reconcileChildFibers方法中,该方法会根据workInProgress的子JSX节点的类型来判断如何处理,比如,如果newChild的类型是对象并且$$typeof属性是REACT_ELEMENT_TYPE的话,那就是单一子节点走reconcileSingleElement处理函数,如果是数组的话,走reconcileChildrenArray处理函数。

reconcileSingleElement
  function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
    var key = element.key;
    var child = currentFirstChild;

    var _created4 = createFiberFromElement(element, returnFiber.mode, lanes);

    _created4.ref = coerceRef(returnFiber, currentFirstChild, element);
    _created4.return = returnFiber;
  }

小结:beginWork作用就是根据workInProgress,即returnFiber的JSX子节点,创建Fiber节点,然后将创建好的节点与workInProgress通过指针构成链表组成Fiber树。如果没有后代节点了,子JSX为空,最后就会返回空。那就会执行到performUnitOfWork中next判断为null的逻辑:completeUnitOfWork

function performUnitOfWork(unitOfWork) {
  var current = unitOfWork.alternate;
  setCurrentFiber(unitOfWork);
  var next;

  next = beginWork$1(current, unitOfWork, subtreeRenderLanes);

  unitOfWork.memoizedProps = unitOfWork.pendingProps;

  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }

}
completeWork(Mount)

在completeUnitOfWork中会调用completeWork,并会根据Fiber节点的类型(tag)进入到不同的处理流程中,mount阶段处理对应DOM节点的Fiber节点的主要逻辑就是:

//通过判断current Fiber节点和判断workInProgress节点上的DOM是否创建
 if (current !== null && workInProgress.stateNode != null) {
     //...update阶段逻辑
 } else {
   var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
  workInProgress.stateNode = instance; // Certain renderers require commit-time
   if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
      markUpdate(workInProgress);
   }
 }

在createInstance函数中,主要就是根据传入的属性调用createElement方法创建DOM节点,并返回出来。将DOM节点赋值给workInProgress Fiber的stateNode属性,这样就把Fiber节点与DOM节点联系起来了。最后一步就是调用finalizeInitialChildren方法初始化刚创建的DOM的属性。

beginwork(Update)

update阶段主要是根据下边两个条件观察能否复用Fiber节点:

  1. oldProps === newProps && workInProgress.type === current.type,即props与fiber.type不变
  2. !includesSomeLane(renderLanes, updateLanes),即当前Fiber节点优先级不够。

如果可以复用的话就会走bailoutOnAlreadyFinishedWork函数复用Current Fiber节点。

function beginWork(current, workInProgress, renderLanes) {
  var updateLanes = workInProgress.lanes;

  if (current !== null) {
    var oldProps = current.memoizedProps;
    var newProps = workInProgress.pendingProps;

    if (oldProps !== newProps || hasContextChanged() || ( 
     workInProgress.type !== current.type )) {
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      didReceiveUpdate = false; 

      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
}

如果不能完全复用,需要更新的话,流程仍然会走到reconcileChildren函数中,在上文中,我们提到了在reconcileChildren函数中会根据current区分mount和update阶段。update阶段和mount阶段一样,同样会走到reconcileSingleElement函数,在该函数内将当前组件与该组件在上次更新时对应的Fiber节点比较(单节点Diff算法),将比较的结果生成新Fiber节点。beginWork的作用可以总结为以下两点:

  • mount阶段:新建Fiber节点,并建立节点之间的联系
  • update阶段:对比新老Fiber节点,根据对比结果生成新的节点
  function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
    var key = element.key;
    var child = currentFirstChild;
    
    //child节点不为null执行对比(Diff算法)
    while (child !== null) {
      // TODO: If key === null and child.key === null, then this only applies to
      // the first item in the list.
      if (child.key === key) {
        switch (child.tag) {
          case Fragment:
            {
              if (element.type === REACT_FRAGMENT_TYPE) {
                deleteRemainingChildren(returnFiber, child.sibling);
                var existing = useFiber(child, element.props.children);
                existing.return = returnFiber;

                {
                  existing._debugSource = element._source;
                  existing._debugOwner = element._owner;
                }

                return existing;
              }

              break;
            }

          case Block:
            {
              var type = element.type;

              if (type.$$typeof === REACT_LAZY_TYPE) {
                type = resolveLazyType(type);
              }

              if (type.$$typeof === REACT_BLOCK_TYPE) {
                // The new Block might not be initialized yet. We need to initialize
                // it in case initializing it turns out it would match.
                if (type._render === child.type._render) {
                  deleteRemainingChildren(returnFiber, child.sibling);

                  var _existing2 = useFiber(child, element.props);

                  _existing2.type = type;
                  _existing2.return = returnFiber;

                  {
                    _existing2._debugSource = element._source;
                    _existing2._debugOwner = element._owner;
                  }

                  return _existing2;
                }
              }
            }

          // We intentionally fallthrough here if enableBlocksAPI is not on.
          // eslint-disable-next-lined no-fallthrough

          default:
            {
              if (child.elementType === element.type || ( // Keep this check inline so it only runs on the false path:
               isCompatibleFamilyForHotReloading(child, element) )) {
                deleteRemainingChildren(returnFiber, child.sibling);

                var _existing3 = useFiber(child, element.props);

                _existing3.ref = coerceRef(returnFiber, child, element);
                _existing3.return = returnFiber;

                {
                  _existing3._debugSource = element._source;
                  _existing3._debugOwner = element._owner;
                }

                return _existing3;
              }

              break;
            }
        } // Didn't match.


        deleteRemainingChildren(returnFiber, child);
        break;
      } else {
        deleteChild(returnFiber, child);
      }

      child = child.sibling;
    }

    //...mount阶段创建Fiber节点
  }
completeWork(Update)

//通过判断current Fiber节点和判断workInProgress节点上的DOM是否存在

if (current !== null && workInProgress.stateNode != null) {
  updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);
} else {
    //...上文讲过的mount阶段
}
}

updateHostComponent$1->prepareUpdate-> diffProperties 这是updateHostComponent$1函数的调用栈,最终会进入diffProperties函数。 下边的代码涉及到了一个知识点for in不仅仅会遍历对象自身的属性,还会遍历其原型链上的可枚举属性。所以会通过hasOwnProperty方法进行判断。

function diffProperties(domElement, tag, lastRawProps, nextRawProps, rootContainerElement) {

  var updatePayload = null;
  var lastProps;
  var nextProps;

  switch (tag) {
    //...
    default:
      lastProps = lastRawProps;
      nextProps = nextRawProps;
      break;
  }

  var propKey;
  var styleName;
  var styleUpdates = null;

  for (propKey in lastProps) {
  // 新老props都存在,update情况
  // 属性是存在原型链上,不是在对象自身上
  // 老props属性为空
  // 3种情况跳过,走下一轮循环 
    if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey) || lastProps[propKey] == null) {
      continue;
    }
    // 能走到这里的都是新props不存在,老props存在的属性,也就是需要删除的属性
    
    //处理style
    if (propKey === STYLE) {
      var lastStyle = lastProps[propKey];

      for (styleName in lastStyle) {
        if (lastStyle.hasOwnProperty(styleName)) {
          if (!styleUpdates) {
            styleUpdates = {};
          }

          styleUpdates[styleName] = '';
        }
      }
    } 
    // 处理特殊props
    else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) ; else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING) ; else if (propKey === AUTOFOCUS) ; else if (registrationNameDependencies.hasOwnProperty(propKey)) {
      if (!updatePayload) {
        updatePayload = [];
      }
    } else {
    // 处理常规props,将清空propsKey的动作加入到updateQueue中
      (updatePayload = updatePayload || []).push(propKey, null);
    }
  }

  for (propKey in nextProps) {
    var nextProp = nextProps[propKey];
    var lastProp = lastProps != null ? lastProps[propKey] : undefined;

    if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp || nextProp == null && lastProp == null) {
      continue;
    }

    if (propKey === STYLE) {
      {
        if (nextProp) {
          // Freeze the next style object so that we can assume it won't be
          // mutated. We have already warned for this in the past.
          Object.freeze(nextProp);
        }
      }

      if (lastProp) {
        // 同第一个循环处理style样式
        for (styleName in lastProp) {
          if (lastProp.hasOwnProperty(styleName) && (!nextProp || !nextProp.hasOwnProperty(styleName))) {
            if (!styleUpdates) {
              styleUpdates = {};
            }

            styleUpdates[styleName] = '';
          }
        } // Update styles that changed since `lastProp`.

         // 更新style
        for (styleName in nextProp) {
          if (nextProp.hasOwnProperty(styleName) && lastProp[styleName] !== nextProp[styleName]) {
            if (!styleUpdates) {
              styleUpdates = {};
            }

            styleUpdates[styleName] = nextProp[styleName];
          }
        }
      } else {
      // lastProp不存在,就说明是新增style,加入到updateQueue中
        if (!styleUpdates) {
          if (!updatePayload) {
            updatePayload = [];
          }
            
          updatePayload.push(propKey, styleUpdates);
        }
        styleUpdates = nextProp;
      }
    } 
    //...一大堆特殊prop
    else {
    // 将需要更新的props加入到updateQueue中
      (updatePayload = updatePayload || []).push(propKey, nextProp);
    }
  }

  if (styleUpdates) {
    // 将上文处理的styleUpdates加入到updateQueue中
    (updatePayload = updatePayload || []).push(STYLE, styleUpdates);
  }

//返回出去
  return updatePayload;
}


  updateHostComponent$1 = function (current, workInProgress, type, newProps, rootContainerInstance) {
    //...

    var updatePayload = prepareUpdate(instance, type, oldProps, newProps, rootContainerInstance, currentHostContext); // TODO: Type this specific to this type of component.

    workInProgress.updateQueue = updatePayload; //将上边处理好的updatePayload 赋值
    if (updatePayload) {
      markUpdate(workInProgress);
    }
}
function markUpdate(workInProgress) {
  workInProgress.flags |= Update;
}

通过prepareUpdate函数可以获取到当前workInProgressFiber节点的updatePayload,如果updatePayload存在,也就是节点新旧props存在差异(需要更新),就会调用markUpdate函数,该函数的作用是为Fiber节点的flags加上Update的标签。

completeUnitOfWork构建effectList
function completeUnitOfWork(unitOfWork) {
  var completedWork = unitOfWork;

  do {
    var current = completedWork.alternate;
    var returnFiber = completedWork.return; 

    if ((completedWork.flags & Incomplete) === NoFlags) {//判断completedWork上有副作用,并且没有结束

      next = completeWork(current, completedWork, subtreeRenderLanes); //进入到completeWork根据fiber的tag进行DOM增删改。

      if (next !== null) {
        workInProgress = next;
        return;
      }

      resetChildLanes(completedWork);

      if (returnFiber !== null && 
      (returnFiber.flags & Incomplete) === NoFlags) {
        if (returnFiber.firstEffect === null) {
          returnFiber.firstEffect = completedWork.firstEffect;
        }

        if (completedWork.lastEffect !== null) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
          }

          returnFiber.lastEffect = completedWork.lastEffect;
        }

        var flags = completedWork.flags;

        if (flags > PerformedWork) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork;
          } else {
            returnFiber.firstEffect = completedWork;
          }

          returnFiber.lastEffect = completedWork;
        }
      }
    } else {
        //....
    } 

    var siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    } 
    completedWork = returnFiber; 
    workInProgress = completedWork;
  } while (completedWork !== null); // We've reached the root.

}

构建effectList大致可以分为两步走:

  1. 将子Fiber的firstEffect和lastEffect赋值或拼接给父Fiber。
  2. 将拥有副作用的子节点与父节点的firstEffect和lastEffect串成链表。

第一步

if (returnFiber.firstEffect === null) {// returnFiber的firstEffect为空的时候
//直接将子节点的firstEffect进行赋值。
  returnFiber.firstEffect = completedWork.firstEffect;
}

if (completedWork.lastEffect !== null) {// 子节点有lastEffect,也就是说明子节点的下级是有副作用节点的
//没有副作用节点就没必要走下边的逻辑,构建effectList是为了commit更新使用
  if (returnFiber.lastEffect !== null) {// 拥有多个子节点的情况 将子节点的effectList串在父节点上
    returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
  }

  returnFiber.lastEffect = completedWork.lastEffect;//将父节点的lastEffect指针指在子节点的lastEffect上。
}

第二步
对节点的firstEffect和lastEffect进行赋值,将effect通过nextEffect进行连接。

var flags = completedWork.flags;// 之前beginWork阶段进行达标
if (flags > PerformedWork) {//存在副作用
  if (returnFiber.lastEffect !== null) {
    returnFiber.lastEffect.nextEffect = completedWork;
  } else {
    returnFiber.firstEffect = completedWork;
  }

  returnFiber.lastEffect = completedWork;
}

下一步

下一步要进入到commitRoot函数中 开始commit阶段。

function performSyncWorkOnRoot(root) {

   //......
  commitRoot(root); // commit 入口
  //......
  return null;
}

总结

首先从rootFiber开始向下深度优先遍历。为遍历到的每个Fiber节点调用beginwork函数。

该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。

当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。

在“归”阶段会调用completeWork处理Fiber节点。

当某个Fiber节点执行完completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段。

如果不存在兄弟Fiber,会进入父级Fiber的“归”阶段。

“递”和“归”阶段会交错执行直到“归”到rootFiber。