前端开发入门(1) - React工作流程

最近一段时间都在使用RN开发需求,被迫学习了不少React相关的知识,在这里分享一下。本篇文章先分享一下React是如何工作的。

总体结构

对于编写纯前端应用来说,React的总体结构包括下面几个部分:

  • react

对使用方暴露的 react api。

  • render

react 的渲染器,把react-reconciler提供的虚拟节点转化为实际ui元素的节点。不同的运行端会有不同的实现。浏览器端对应的是react-dom,rn对应的则是react-native。

  • react-reconciler

名字翻译过来是协调器,也就为了协调其他模块设计的。他内部的逻辑就是创建处理虚拟节点树的任务交由 scheduler进行调度,并调用渲染器去渲染最终的虚拟节点树。

  • scheduler

react 内部的任务调度器,在并发模式下支持给任务划分时间片。实现react的可中断渲染特性。

我在网上找了一个画的非常好的react总体结构图:github.com/7kms/react-...

因为他的图里还涉及一些具体的对象和函数调用,所以我也简化了一下他的图片,更简单的展示一下react的结构:

基础概念

  • ReactElement

面向开发者的ui元素,在react里面用jsx描述的组件。

  • 虚拟节点

react代码里面用Fiber来表示虚拟节点。

  • 优先级

react 使用车道模型进行优先级管理。相关内容定义在 ReactFiberLane.js 里面。单个任务优先级定义为Lane,批量任务优先级定义为Lanes。这些都被定义为二进制变量,使用位运算可以快速的实现

    • 判断单个任务和批量任务的优先级是否重合
    • 从一个任务组里面分离出单个task的优先级

因为react-reconciler和scheduler模块有交互,所以在scheduer中也定义了优先级和fiber的lane优先级对应。

关于react优先级相关的细节,可以参考这篇文档:github.com/7kms/react-...

工作流程

我们从工作流程跟踪一下React的代码。一般我们直接使用react的时候都是通过如下的代码:

ini 复制代码
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <App/>
);

这里其实经历了如下步骤:

  • 通过jsx创建了一个 ReactElement:
  • 调用 createRoot 创建 ReactDOMRoot
  • 调用 ReactDOMRoot 的 render 方法
创建element

ReactElement本质上就是一个普通的对象,他的创建实现在 ReactJSXElement.js里的 createElement,这里实际就是创建了这个对象:

typescript 复制代码
// createElement
return ReactElement(
  type,
  key,
  ref,
  undefined,
  undefined,
  getOwner(),
  props,
  __DEV__ && enableOwnerStacks ? Error('react-stack-top-frame') : undefined,
  __DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined,
);

这里能看到三个关键字段:

  • type 这个节点对应的type,表示元素类型,一般根节点都是'div'
  • key 就是平时使用的时候给组件指定的key
  • props 组件的属性
创建ReactDOMRoot

这个代码实现在 react-dom 里面:

javascript 复制代码
export function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions,
): RootType {
  const root = createContainer(
    container,
    ConcurrentRoot,
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onUncaughtError,
    onCaughtError,
    onRecoverableError,
    transitionCallbacks,
  );
  return new ReactDOMRoot(root);
}

createContainer实现在 react-reconciler的 ReactFiberReconciler.js,创建了一个 FiberRootNode 对象。

typescript 复制代码
function FiberRootNode(
  this: $FlowFixMe,
  containerInfo: any,
  // $FlowFixMe[missing-local-annot]
  tag,
  hydrate: any,
  identifierPrefix: any,
  onUncaughtError: any,
  onCaughtError: any,
  onRecoverableError: any,
  formState: ReactFormState<any, any> | null,
) 

FiberRootNode表示的是虚拟节点树的根节点,创建的时候传的tag为 RootTag

ini 复制代码
// createFiberRoot
const root: FiberRoot = (new FiberRootNode(
  containerInfo,
  tag,
  hydrate,
  identifierPrefix,
  onUncaughtError,
  onCaughtError,
  onRecoverableError,
  formState,
): any);
const uninitializedFiber = createHostRootFiber(tag, isStrictMode);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;

这里创建了一个Fiber节点和FiberRootNode建立关系。FIber节点的组成如下:

内部包括自身属性、子节点兄弟节点、动态属性、hooks相关字段、优先级等。

建树

接下来调用 RootNode 的 render 方法来建虚拟树,这里也是调用 reconciler 里的 updateContainer, 核心代码如下:

scss 复制代码
function updateContainerImpl(
  rootFiber: Fiber,
  lane: Lane,
  element: ReactNodeList,
  container: OpaqueRoot, 
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
):void {
  const root = enqueueUpdate(rootFiber, update, lane);
  if (root != null) {
    startUpdateTimerByLane(lane);
    scheduleUpdateOnFiber(root, rootFiber, lane);
    entangleTransitions(root, rootFiber, lane);
  }
}

scheduleUpdateOnFiber里面代码比较长就不贴了,贴了也会看晕掉,我把他画成一个时序图:

renderRootSync/renderRootConcurrent 是开始构造fiber树的阶段。

finishConcurrentRender则是fiber树构造完成提交给render处理的阶段。

renderRootSync

我们主要看下renderRootSync,这里最后执行的就是workLoopSync:

scss 复制代码
// workLoopSync
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

这里起了一个循环,只要当前处理节点存在,循环就一直工作。

ini 复制代码
function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate;
  let next;
  next = beginWork(current, unitOfWork, entangledRenderLanes);
  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
    workInprogress = next;
  }
}

这里每个步骤都拆成了一个工作单元执行 beginWork,全部单元执行完毕才会执行 completeUnitOfWork。每个单元应该都对应了一个子节点。

beiginWork

beginWork前半部分会判断阶段是否需要更新:

ini 复制代码
// beginWork
if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    if (oldProps !== newProps || hasLegacyContextChanged()) {
      // 属性或者上下文出现变化
      didReceiveUpdate = true;
    } else {
      const hasScheduleUpdateOrContext = checkScheduleUpdateOrContext(current,renderLanes);
      if (!hasScheduleUpdateOrContext && (workInProgress.flags & DidCapture) === NoFlags) {
        didReceiveUpdate = false;
        return;
      }
      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        didReceiveUpdate = true;
      } else {
        didReceiveUpdate = false;
      }
    }
  } else {
    didReceiveUpdate = false;
  }

  • 旧节点属性变化
  • flags里面设置了 ForceUpdateForLegacySuspense 这个Flag

didReceiveUpdate为true,否则为false。这是个全局变量,后面会用到。

beginWork后半部分会根据当前节点的tag创建Fiber节点,还记得前面建树的时候root传入的tag吗,就是 HostRoot:

arduino 复制代码
// beginWork
case HostRoot:
  return updateHostRoot(current, workInProgress, renderLanes);

如果是普通的节点,我们现在写的比较多的是函数组件和类组件,分别是

ini 复制代码
 case FunctionComponent: {
  const Component = workInProgress.type;
  const unresolvedProps = workInProgress.pendingProps;
  const resolvedProps = disableDefaultPropsExceptForClasses || workInProgress.elementType === Component
      ? unresolvedProps
      : resolveDefaultPropsOnNonClassComponent(Component, unresolvedProps);
  return updateFunctionComponent(
    current,
    workInProgress,
    Component,
    resolvedProps,
    renderLanes,
  );
}
case ClassComponent: {
  const Component = workInProgress.type;
  const unresolvedProps = workInProgress.pendingProps;
  const resolvedProps = resolveClassComponentProps(
    Component,
    unresolvedProps,
    workInProgress.elementType === Component,
  );
  return updateClassComponent(
    current,
    workInProgress,
    Component,
    resolvedProps,
    renderLanes,
  );
}

HostRoot

这个会按路径调用reconcileChildren -> reconcileChildFibers -> createChildReconciler -> reconcileChildFibers -> reconcileChildFibersImpl -> reconcileSingleElement -> createFiberFromElement :

ini 复制代码
export function createFiberFromElement(
  element: ReactElement,
  mode: TypeOfMode,
  lanes: Lanes,
): Fiber {
  const type = element.type;
  const key = element.key;
  const pendingProps = element.props;
  const fiber = createFiberFromTypeAndProps(
    type,
    key,
    pendingProps,
    owner,
    mode,
    lanes,
  );
  return fiber;
}

createFiberFromTypeAndProps里面会创建Fiber对象,HostRoot 根节点的 fiberTag传的是 HostComponent。这里需要关注下,reconcileChildren 在其他类型的组件 beginWork 的时候都是会调用的。毕竟他的职责就是创建fiber节点。

FunctionComponent

updateFunctionComponent的核心逻辑是调用renderWithHooks:

typescript 复制代码
// renderWithHooks
export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
  // 指定Dispatcher
  ReactSharedInternals.H = current === null || current.memoizedState === null
    ? HooksDispatcherOnMount // 创建节点的时候是onMount
    : HooksDispatcherOnUpdate;
  let children = Component(props, secondArg);
  return children;
}

这里就是把Component执行了一遍并取了返回结果。接着调用 reconcileChildren创建fiber节点。

ClassComponent

创建fiber树的时候如果节点是class组件,那么调用 updateClassComponent:

ini 复制代码
function updateClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
) {
  const instance = workInProgress.stateNode;
  if (instance === null) {
    //创建
    constructClassInstance(workInProgress, Component, nextProps);
    mountClassInstance(workInProgress, Component, nextProps, renderLanes);
    shouldUpdate = true;
  } else {
    // ...
  }
  const nextUnitOfWork = finishClassComponent(
    current,
    workInProgress,
    Component,
    shouldUpdate,
    hasContext,
    renderLanes,
  );
  return nextUnitOfWork;
}

这里先创建class组件,然后执行他的mount生命周期。在finishClassComponent里面执行render方法,并调用reconcileChildren创建fiber节点。

从上述代码也能看出workLoopSync本质上就是递归深度优先遍历的去创建fiber节点,构造一颗fiber树。

遍历到叶子节点时执行completeUnitOfWork。completeUnitOfWork也是在循环里重复调用completeWork。直到父节点为null。

看 completeWork 之前,我们先看一下每个类型tag在执行 completeWork 的时候都会执行的方法 bubleProperties。

bubbleProperties
ini 复制代码
function bubbleProperties(completedWork: Fiber) {
  // 关键逻辑
  let child = completedWork.child;
  while (child !== null) {
    newChildLanes = mergeLanes(
      newChildLanes,
      mergeLanes(child.lanes, child.childLanes),
    );

    subtreeFlags |= child.subtreeFlags;
    subtreeFlags |= child.flags;
    
    child.return = completedWork;
    child = child.sibling;
  }
}

这个方法的作用是把child的一些flags与到父节点上去,例如副作用相关的flag让父组件也能感知。

completeWork

回到completeWork, 这里也会根据workInProgress的tag来处理不同类型的节点:

HostComponent

想看根节点:

ini 复制代码
//核心逻辑
const rootContainerInstance = getRootHostContainer();
const instance = createInstance(
  type,
  newProps,
  rootContainerInstance,
  currentHostContext,
  workInProgress,
);
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
finalizeInitialChildren(instance, type, newProps, currentHostContext);
bubbleProperties(workInProgress);

通过 createInstance 创建真实的dom元素,然后设置给 workInprogress 的 stateNode。

ini 复制代码
if (typeof props.is === 'string') {
  domElement = ownerDocument.createElement(type, {is: props.is});
} else {
  domElement = ownerDocument.createElement(type);
}

appendAllChildren是添加dom元素,底层通过appendChild实现。调用finalizeInitialChildren来更新dom节点的属性,最后通过 bubblePropertes 更新标记位。

FunctionComponent

只是调用了一下bubbleProperties

ClassComponent

ini 复制代码
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
  popLegacyContext(workInProgress);
}
bubbleProperties(workInProgress);
return null;

这里除了处理了一下上下文,也就是调用了一下 bubbleProperties。

综上所述,我们可以画一下workSync的核心流程:

提交commit

finishConcurrentRender按如下路径调用:finishConcurrentRender -> commitRoot -> commitRootImpl

less 复制代码
function commitRootImpl(
  root: FiberRoot,
  recoverableErrors: null | Array<CapturedValue<mixed>>,
  transitions: Array<Transition> | null,
  didIncludeRenderPhaseUpdate: boolean,
  renderPriorityLevel: EventPriority,
  spawnedLane: Lane,
  updatedLanes: Lanes,
  suspendedRetryLanes: Lanes,
  suspendedCommitReason: SuspendedCommitReason, // Profiling-only
  completedRenderStartTime: number, // Profiling-only
  completedRenderEndTime: number, // Profiling-only
) {
  // 核心逻辑
  const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
    root,
    finishedWork,
  );
  commitMutationEffects(root, finishedWork, lanes);
  commitLayoutEffects(finishedWork, root, lanes);
  requestPaint();
}

核心逻辑主要分成三个阶段

before mutation

主要是执行 commitBeforeMutationEffectsOnFiber:

php 复制代码
// commitBeforeMutationEffectsOnFiber
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
  switch (finishedWork.tag) {
    case ClassComponent:
      if (current !== null) {
        commitClassSnapshot(finishedWork, current);
      }
  }
}

主要是类组件会有一个处理snapshot的逻辑。

mutation

这个阶段在执行commitMutationEffectsOnFiber的时候,每种类型的组件都会执行recursivelyTraverseMutationEffects和commitReconciliationEffects:, 并且 recursivelyTraverseMutationEffects 是兄弟节点递归执行。

commitReconciliationEffects则处理节点真实dom的增删改:

ini 复制代码
function commitReconciliationEffects(finishedWork: Fiber) {
  const flags = finishedWork.flags;
  if (flags & Placement) {
    commitHostPlacement(finishedWork);
    finishedWork.flags &= ~Placement;
  }
}

commitHostPlacement底层通过 Element.insertBefore 和 Element.appendChild等浏览器dom api操作dom节点。

接下来会处理effect相关的hooks:

scss 复制代码
// commitMutationEffectsOnFiber
case FunctionComponent:
  recursivelyTraverseMutationEffects(root, finishedWork, lanes);
  commitReconciliationEffects(finishedWork);
  // hooks
  if (flags & Update) {
    commitHookEffectListUnmount(HookInsertion | HookHasEffect,finishedWork,finishedWork.return);
    commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
    commitHookLayoutUnmountEffects(finishedWork,finishedWork.return,HookLayout | HookHasEffect);
  }

这里会执行旧hooks的销毁逻辑,执行新组件的hooks回调。

layout

调用路径 commitLayoutEffects -> commitLayoutEffectOnFiber

scss 复制代码
function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
  const flags = finishedWork.flags;
  switch (finishedWork.tag) {
    case FunctionComponent:
      recursivelyTraverseLayoutEffects(
        finishedRoot,
        finishedWork,
        committedLanes,
      );
      if (flags & Update) {
        commitHookLayoutEffects(finishedWork, HookLayout | HookHasEffect);
      }
      break;
    case ClassComponent: {
      recursivelyTraverseLayoutEffects(
        finishedRoot,
        finishedWork,
        committedLanes,
      );
      if (flags & Update) {
        commitClassLayoutLifecycles(finishedWork, current);
      }

      if (flags & Callback) {
        commitClassCallbacks(finishedWork);
      }

      if (flags & Ref) {
        safelyAttachRef(finishedWork, finishedWork.return);
      }
      break;
    }
    //...
  }
}

这里如果是函数组件会执行useLayoutEffect的hooks。

如果是类组件会在创建组件的时候执行componentDidMount生命周期方法。

总结

读到这里,这里我们就入门了React的基础流程和核心原理。现在我们总结一下关键点:

  • React 三棵树,分别是 React.Element(组件)、FIber(虚拟节点)、ui节点(dom),创建流程和映射关系依次为 Element -> Fiber -> dom。
  • react-reconclier负责创建虚拟dom树,对应的执行阶段为建立循环(workLoop)-> 递归 -> 开启工作单元(beginWork)-> 为每个单元的节点创建Fiber节点 -> 回溯 -> 结束工作单元(completeWork) -> 创建dom元素。
  • 提交渲染阶段包括 beforemutation、mutation、layout、paint几个阶段。react在这一步会最终确定dom节点的增删改以及dom的操作和最终视图的渲染。
相关推荐
逆旅行天涯10 分钟前
【vitePress】基于github快速添加评论功能(giscus)
前端·github
我有一棵树28 分钟前
style标签没有写lang=“scss“引发的 bug 和反思
前端·bug·scss
陈奕迅本讯1 小时前
HTML5和CSS3拔高
前端·css3·html5
画船听雨眠aa2 小时前
vue项目创建与运行(idea)
前端·javascript·vue.js
大码猴2 小时前
用好git的几个命令,领导都夸你干的好~
前端·后端·面试
℡52Hz★2 小时前
如何正确定位前后端bug?
前端·vue.js·vue·bug
学不完了是吧2 小时前
html、js、css实现爱心效果
前端·css·css3
小丁爱养花2 小时前
Spring MVC:设置响应
java·开发语言·前端
优联前端2 小时前
Web 音视频(二)在浏览器中解析视频
前端·javascript·音视频·优联前端·webav