一、前言
最近的几次面试都被问到了框架原理。记忆力不太行的我往往背诵完后很快就会忘记,因此打算花21个小时(分3天,每天7小时)去好好的阅读下React框架,了解其中几个关键功能点的执行逻辑。
框架版本:React 17,enableNewReconciler=false
学习方式:通过debug一步步调试JS代码的方式,来深入了解React框架的主流程
文中的代码片段,只保留作者认为重要的代码
二、DAY 1 目标
- mount阶段的renderRootSync流程
- 常用对象的数据结构
- 常用API罗列
三、mount阶段renderRootSync流程图
-
创建FiberRoot对象
-
更新Container
-
【调和阶段】根据FiberRoot创建整个Fiber链表
- workInProgress指向给当前Fiber
- 通过reconcileFhildren初始化子节点(为每一个子节点创建Fiber)
- workInProgress指向下一个Fiber(有子节点则为第一个子节点,没有子节点则为兄弟节点)
- 重复a ~ c 步骤,直到遍历完所有节点,workInProgress = null
-
【commit阶段】根据FiberRoot.firstEffect字段,进行真实DOM渲染
四、常用对象的数据结构
Fiber
根节点、组件、DOM元素、Text,都有一个与之对应的FiberNode
packages/react-reconciler/src/ReactInternalTypes.js
重要属性
- memoizedState 用于创建输出的状态
- pendingProps 新的变动带来的新的props,即nextProps
- stateNode 当前fiber的本地状态, 如果fiber对应的是class组件,则为class实例;如果是普通节点,则是 DOM元素;如果是函数组件,则为null
- alternate current fiber指向work in progress fiber;working in progress fiber指向current fiber
- updateQueue: 状态更新,回调和DOM更新的队列,Fiber对应的组件,所产生的update,都会放在该队列中。{shared: {pending: {}}}
- nextEffect: 链表格式,链接下一个有副作用的fiber
- firstEffect | lastEffect 当子节点有副作用时,标记这个字段
export type Fiber = {|
// These first fields are conceptually members of an Instance. This used to
// be split into a separate type and intersected with the other Fiber fields,
// but until Flow fixes its intersection bugs, we've merged them into a
// single type.
// An Instance is shared between all versions of a component. We can easily
// break this out into a separate object to avoid copying so much to the
// alternate versions of the tree. We put this on a single object for now to
// minimize the number of objects created during the initial render.
// Tag identifying the type of fiber.
tag: WorkTag,
// Unique identifier of this child.
key: null | string,
// The value of element.type which is used to preserve the identity during
// reconciliation of this child.
elementType: any,
// The resolved function/class/ associated with this fiber.
type: any,
// The local state associated with this fiber.
stateNode: any,
// Conceptual aliases
// parent : Instance -> return The parent happens to be the same as the
// return fiber since we've merged the fiber and instance.
// Remaining fields belong to Fiber
// The Fiber to return to after finishing processing this one.
// This is effectively the parent, but there can be multiple parents (two)
// so this is only the parent of the thing we're currently processing.
// It is conceptually the same as the return address of a stack frame.
return: Fiber | null,
// Singly Linked List Tree Structure.
child: Fiber | null,
sibling: Fiber | null,
index: number,
// The ref last used to attach this node.
// I'll avoid adding an owner field for prod and model that as functions.
ref:
| null
| (((handle: mixed) => void) & {_stringRef: ?string, ...})
| RefObject,
// Input is the data coming into process this fiber. Arguments. Props.
pendingProps: any, // This type will be more specific once we overload the tag.
memoizedProps: any, // The props used to create the output.
// A queue of state updates and callbacks.
updateQueue: mixed,
// The state used to create the output
memoizedState: any,
// Dependencies (contexts, events) for this fiber, if it has any
dependencies: Dependencies | null,
// Bitfield that describes properties about the fiber and its subtree. E.g.
// the ConcurrentMode flag indicates whether the subtree should be async-by-
// default. When a fiber is created, it inherits the mode of its
// parent. Additional flags can be set at creation time, but after that the
// value should remain unchanged throughout the fiber's lifetime, particularly
// before its child fibers are created.
mode: TypeOfMode,
// Effect
flags: Flags,
subtreeFlags: Flags,
deletions: Array<Fiber> | null,
// Singly linked list fast path to the next fiber with side-effects.
nextEffect: Fiber | null,
// The first and last fiber with side-effect within this subtree. This allows
// us to reuse a slice of the linked list when we reuse the work done within
// this fiber.
firstEffect: Fiber | null,
lastEffect: Fiber | null,
lanes: Lanes,
childLanes: Lanes,
// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
// memory if we need to.
alternate: Fiber | null,
// Time spent rendering this Fiber and its descendants for the current update.
// This tells us how well the tree makes use of sCU for memoization.
// It is reset to 0 each time we render and only updated when we don't bailout.
// This field is only set when the enableProfilerTimer flag is enabled.
actualDuration?: number,
// If the Fiber is currently active in the "render" phase,
// This marks the time at which the work began.
// This field is only set when the enableProfilerTimer flag is enabled.
actualStartTime?: number,
// Duration of the most recent render time for this Fiber.
// This value is not updated when we bailout for memoization purposes.
// This field is only set when the enableProfilerTimer flag is enabled.
selfBaseDuration?: number,
// Sum of base times for all descendants of this Fiber.
// This value bubbles up during the "complete" phase.
// This field is only set when the enableProfilerTimer flag is enabled.
treeBaseDuration?: number,
// Conceptual aliases
// workInProgress : Fiber -> alternate The alternate used for reuse happens
// to be the same as work in progress.
// __DEV__ only
_debugID?: number,
_debugSource?: Source | null,
_debugOwner?: Fiber | null,
_debugIsCurrentlyTiming?: boolean,
_debugNeedsRemount?: boolean,
// Used to verify that the order of hooks does not change between renders.
_debugHookTypes?: Array<HookType> | null,
|};
FiberRoot
根节点对象
packages/react-reconciler/src/ReactInternalTypes.js
重要属性
- containerInfo 根节点对应的DOM元素
- current FiberRoot对应创建的fiber
export type FiberRoot = {
...BaseFiberRootProperties,
...ProfilingOnlyFiberRootProperties,
...SuspenseCallbackOnlyFiberRootProperties
};
type BaseFiberRootProperties = {|
// The type of root (legacy, batched, concurrent, etc.)
tag: RootTag,
// Any additional information from the host associated with this root.
containerInfo: any,
// Used only by persistent updates.
pendingChildren: any,
// The currently active root fiber. This is the mutable root of the tree.
current: Fiber,
pingCache: WeakMap<Wakeable, Set<mixed>> | Map<Wakeable, Set<mixed>> | null,
// A finished work-in-progress HostRoot that's ready to be committed.
finishedWork: Fiber | null,
// Timeout handle returned by setTimeout. Used to cancel a pending timeout, if
// it's superseded by a new one.
timeoutHandle: TimeoutHandle | NoTimeout,
// Top context object, used by renderSubtreeIntoContainer
context: Object | null,
pendingContext: Object | null,
// Determines if we should attempt to hydrate on the initial mount
+hydrate: boolean,
// Used by useMutableSource hook to avoid tearing during hydration.
mutableSourceEagerHydrationData?: Array<
MutableSource<any> | MutableSourceVersion,
> | null,
// Node returned by Scheduler.scheduleCallback. Represents the next rendering
// task that the root will work on.
callbackNode: *,
callbackPriority: LanePriority,
eventTimes: LaneMap<number>,
expirationTimes: LaneMap<number>,
pendingLanes: Lanes,
suspendedLanes: Lanes,
pingedLanes: Lanes,
expiredLanes: Lanes,
mutableReadLanes: Lanes,
finishedLanes: Lanes,
entangledLanes: Lanes,
entanglements: LaneMap<Lanes>,
|};
type ProfilingOnlyFiberRootProperties = {|
interactionThreadID: number,
memoizedInteractions: Set<Interaction>,
pendingInteractionMap: Map<Lane | Lanes, Set<Interaction>>,
|};
export type SuspenseHydrationCallbacks = {
onHydrated?: (suspenseInstance: SuspenseInstance) => void,
onDeleted?: (suspenseInstance: SuspenseInstance) => void,
...
};
ReactElement
通过react.createElement方法调用后产生的element对象。可以描述组件和DOM元素
packages/shared/ReactElementType.js
重要属性
- type 描述组件时,存储组件函数;描述DOM元素时,存储DOM标签名
- props 描述element的节点属性,以及element的children
type ReactElement = {|
$$typeof: any,
type: any,
key: any,
ref: any,
props: any,
// ReactFiber
_owner: any,
// __DEV__
_store: {validated: boolean, ...},
_self: React$Element<any>,
_shadowChildren: any,
_source: Source,
|};
五、mount阶段reconcile关键API
Demo与编译后代码对比
JSX是react.createElement的语法糖
Demo-JSX
class SupComp extends React.Component{
render() {
return (
<div>
<p onClick={this.clickDiv}>sup</p>
<p onClick={this.clickDiv2}>sup2{this.state.param2}</p>
<div>last node {this.state.isParent.toString()}</div>
</div>
)
}
}
ReactDOM.render(<SupComp />, mountNode);
编译后-JSX
class SupComp extends React.Component{
render() {
return react.createElement("div", {
__source: {
fileName: _jsxFileName,
lineNumber: 195
},
__self: this
}, react.createElement("p", {
onClick: this.clickDiv,
__source: {
fileName: _jsxFileName,
lineNumber: 196
},
__self: this
}, "sup"), react.createElement("p", {
onClick: this.clickDiv2,
__source: {
fileName: _jsxFileName,
lineNumber: 197
},
__self: this
}, "sup2", this.state.param2), react.createElement("div", {
__source: {
fileName: _jsxFileName,
lineNumber: 208
},
__self: this
}, "last node ", this.state.isParent.toString()));
}
}
reactdom.render(react.createElement(SupComp, {
__source: {
fileName: _jsxFileName,
lineNumber: 214
},
__self: undefined
}), mountNode);
React.createElement
packages/react/src/ReactElement.js
关键功能
- 定义props对象,把config里的属性,children,type.defaultProps依次放到props里
- 通过ReactElement方法,生成ReactElement对象,并返回
主要代码
function createElement(type, config, children) {
let propName;
// Reserved names are extracted
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
if (config != null) {
// Remaining properties are added to a new props object
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
// Resolve default props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
performUnitOfWork
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
关键功能
- 初始化或更新workInProgress。
- 同时workInProgress指向下一个Fiber
-
- Fiber指向第一个子节点
- Fiber指向相邻节点
- Fiber指向null【workInProgress Fiber链表已执行完毕,去commit阶段】
主要代码
function performUnitOfWork(unitOfWork: Fiber): void {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);
let next;
next = beginWork(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;
}
ReactCurrentOwner.current = null;
}
beginWork
packages/react-reconciler/src/ReactFiberBeginWork.old.js
关键功能
根据不同的workInProgress.tag,进行不同的逻辑处理。
其中,最常用的是:
- updateHostRoot(根节点处理)
- updateClassComponent(class组件处理)
- mountIndeterminateComponent(函数组件初始入口,function只有在返回的时候才知道是函数组件,因此都会统一进入到这里处理)
- updateHostComponent(原生DOM节点处理)
- updateHostText(原生Text节点处理)
不同执行过程中,会对Fiber里的不同字段进行各自的处理。
而这些方法的共同功能是:
- 判断当前Fiber是否有子节点,如果有,则通过reconcileChildren方法进行对子节点挨个初始化(新建Fiber)。
- 最终用Fiber的child和return字段把父节点和第一个子节点互相关联起来(伪代码可以看下面的reconcileChildren部分)。
- 返回第一个子节点
主要代码
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ...
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
// ... case more
}
}
completeUnitOfWork
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
关键功能
当WorkInProgress没有子节点的时候,会进入到completeWork方法。
completeWork方法,主要提供了如下功能(completedWork:当前Fiber节点;returnFiber:父Fiber节点):
- 对completedWork执行completeWork方法,完成最终的FIber属性计算和处理
- 通过completedWork的firstEffect和lastEffect对returnFiber的firstEffect和lastEffect进行赋值处理(并一层层向上传递)
- 如果completedWork需要更新,则赋值returnFiber.firstEffect = completedWork
- 如果completedWork有兄弟节点,则workInProgress = siblingFiber
- 如果completedWork没有兄弟节点,则执行 completedWork = returnFiber 和 workInProgress = completedWork
- 重复执行1~5步骤,直至 completedWork 为null(遍历完了workInProgress Fiber链表的所有节点)
\
主要代码
function completeUnitOfWork(unitOfWork: Fiber): void {
// Attempt to complete the current unit of work, then move to the next
// sibling. If there are no more siblings, return to the parent fiber.
let completedWork = unitOfWork;
do {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
const current = completedWork.alternate;
const returnFiber = completedWork.return;
// Check if the work completed or if something threw.
if ((completedWork.flags & Incomplete) === NoFlags) {
next = completeWork(current, completedWork, subtreeRenderLanes);
if (next !== null) {
// Completing this fiber spawned new work. Work on that next.
workInProgress = next;
return;
}
if (
returnFiber !== null &&
// Do not append effects to parents if a sibling failed to complete
(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;
}
const flags = completedWork.flags;
if (flags > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
} else {
// This fiber did not complete because something threw. Pop values off
// the stack without entering the complete phase. If this is a boundary,
// capture values if possible.
}
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
workInProgress = siblingFiber;
return;
}
// Otherwise, return to the parent
completedWork = returnFiber;
// Update the next thing we're working on in case something throws.
workInProgress = completedWork;
} while (completedWork !== null);
}
reconcileChildren / reconcileChildFibers
packages/react-reconciler/src/ReactChildFiber.old.js
关键功能
reconcileChildren只是对reconcileChildFibers进行简单的包装。主要处理在reconcileChildFibers。
reconcileChildFibers方法对workInProgress Fiber的子节点进行初始化并根据child类型(Object/String/Number/Array)创建对应Fiber。并返回第一个子节点Fiber。
\
总结
只要搞清楚以下3点,那么对理解React的mount阶段有很大帮助
- 创建完根节点(FiberRoot和对于的Fiber)后,通过workLoopSync方法,循环创建workInProgress Fiber链表。
- performUnitOfWork方法如何去执行并创建Fiber,以及workInProgress对象在不同情况下的赋值。
- 在创建workInProgress Fiber时,如果没有子节点,那么通过completeUnitOfWork方法去寻找自己的兄弟节点(或父节点的兄弟节点)。如果没有找到兄弟节点,则返回到根节点,并赋值workInProgress为null。结束reconcile阶段,进入commit阶段。