最近一段时间都在使用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的操作和最终视图的渲染。