前言
React17自去年十月发布以来,出现了几个比较重要的变化。首先,17作为一个过渡版本,其明确了在react中的定位,即:承上启下,作为渐进式框架的首版本,在后续的18、19等版本中会进行渐进升级而不是强制进行硬切换;其次,17结合最新的浏览器的特性做了一些更改和优化,比如对合成事件系统的优化;最后,自16以来的基于Fiber架构的模式对整个react性能优化在每个小版本中也会不断的进行逐步的微调,每次的微调都透露着react大佬们的一些思路与思考。本文以react17.0.0版本的源码入手,着重从react-reconciler和scheduler这两个模块中的部分源码进行拆解和浅析,希望能够窥一斑而见全豹,揣度各位大佬的一些架构思路和想法,从而拓宽一些个人的眼界,然个人水平有限,难免管窥蠡测,对react的理解也可能有所偏颇,本着想将这样一个庞大的架构简洁分析出来的想法,希望能对各位同学有所启发,对于更为经典的部分,仍需要各位去品读源码,我们仍然应该对源码保持着一颗敬畏之心,随着技术的提升,每每品读都会有不同的感受!
目录结构
react目录比较大,涉猎的也比较多,这里只显示可能会涉及部分的目录
- packages
- react
- src
- jsx
- ReactJSX.js
- ReactJSXElement.js (定义了jsx)
- ReactJSXElementValidator.js
- React.js
- ReactBaseClasses.js (setState及forceState)
- ReactContext.js (context上下文及Provider和Consumer)
- ReactElement.js (定义了ReactElement的格式)
- ReactForwardRef.js (Ref的定义)
- ReactHooks.js (ReactHooks相关,不是本文重点,可以参看之前的文章)
- ReactLazy.js
- ReactMemo.js
- ReactMutableSource.js
- ReactStartTransition.js (批量更新的事务的概念)
- jsx
- src
- react-dom
- src
- clients
- ReactDOMHostConfig.js
- ReactDOMLegacy.js
- ReactDOMRoot.js (三种模式:legacy模式、blocking模式、concurrent模式)
- events
- EventListeners.js
- ReactDOMEventListeners.js
- SyntheticEvent.js
- server (React Sever Component相关,不展开讲了)
- clients
- src
- react-reconciler
- src
- ReactChildFiber.js
- ReactFiber.js
- ReactFiberBeginWork.js
- ReactFiberCommitWork.js
- ReactFiberCompleteWork.js
- ReactFiberLane.js
- ReactFiberReconciler.js
- ReactFiberRoot.js
- ReactFiberWorkLoop.js
- src
- scheduler
- src
- Scheduler.js
- SchedulerMinHeap.js
- SchedulerPostTask.js
- SchedulerProfiling.js
- src
- react
源码解析
整个React的Fiber架构的核心在于对浏览器进行了时间分片处理(ps:Firefox新版本自身也实现了时间分片),抹掉了平台的差异,从而使得浏览器处理时候可以将控制权交出去,避免了js线程过多占用而阻塞渲染线程,实现了更细粒度的调度,即为:协程或纤程的调度
React
| 文件名 | 作用 | 备注 |
|---|---|---|
| jsx-runtime.js | jsx解释器 | 编译jsx |
| ReactElement.js | React元素的格式 | React的结点格式信息 |
jsx-runtime.js
react17之后不再需要对每个react组件进行react的import,其内置了一个jsx-runtime的运行时,感兴趣的同学可以看一下这个react-jsx-dev-runtime.development.js,简单来说就是利用正则对jsx进行了一层浅的转化,本质jsx是对js的一种扩展
ReactElement.js
const ReactElement = function(
type,
key,
ref,
self,
source,
owner,
props
) {
const element = {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props
}
}
React-DOM
这里主要对合成事件及DOM的一些处理进行了阐述
DOM处理
| 文件名 | 作用 | 备注 |
|---|---|---|
| ReactDOMHostConfig.js | appendChildToContainer等 | 原生dom操作 |
| ReactDOMLegacy.js | legacyRenderSubtreeIntoContainer | 未启用异步渲染的dom操作 |
| ReactDOMRoot.js | createRoot、createLegacyRoot、createBlockingRoot | 三种模式的根组件 |
React17的进行了模式的设置,分别为:Legacy模式、Concurrent模式、Blocking模式,其中Concurrent模式是启用fiber分片的异步渲染方式,而Legacy模式则仍是15的同步渲染模式,Blocking则是介于二者之间的模式,React有意按照这样一种渐进的方式进行过度
合成事件
| 文件名 | 作用 | 备注 |
|---|---|---|
| EventListeners.js | addEventCaptureListener、addEventBubbleListener | 原生事件监听 |
| ReactDOMEventListeners.js | dispatchEvent | React的事件 |
| SyntheticEvent.js | createSyntheticEvent | 合成事件 |
ReactDOMEventListeners.js
核心是dispatchEvent进行事件的分发,17之后不再将事件全部冒泡到document去代理,这和浏览器的改进有关,不再需要代理绑定,浏览器可以对更细粒度的区域进行监听
function dispatchDiscreateEvent() {
}
function dispatchBlockingEvent() {
}
export function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): void {
if (
allowReplay &&
hasQueuedDiscreteEvents() &&
isReplayableDiscreteEvent(domEventName)
) {
queueDiscreteEvent(
null, // Flags that we're not actually blocked on anything as far as we know.
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
return;
}
const blockedOn = attemptToDispatchEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
if (blockedOn === null) {
// We successfully dispatched this event.
if (allowReplay) {
clearIfContinuousEvent(domEventName, nativeEvent);
}
return;
}
if (allowReplay) {
if (isReplayableDiscreteEvent(domEventName)) {
queueDiscreteEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
return;
}
if (
queueIfContinuousEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
)
) {
return;
}
clearIfContinuousEvent(domEventName, nativeEvent);
}
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
null,
targetContainer,
);
}
Scheduler
本质是根据任务开始时间和过期时间利用小顶堆的优先队列而进行的时间分片处理及调度
| 文件名 | 作用 | 备注 |
|---|---|---|
| Scheduler.js | workLoop | 调度入口 |
| SchedulerMinHeap.js | 小顶堆 | 优先队列的小顶堆 |
| SchedulerPostTask.js | unstable_scheduleCallback、unstable_shouldYield | 调度方法 |
Schdeuler.js
function advanceTimers(currentTime) {
let timer = peek(timerQueue);
while (timer !== null) {
if (timer.callback === null) {
pop(timerQueue);
} else if (timer.startTime <= currentTime) {
pop(timerQueue);
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
if (enableProfiling) {
markTaskStart(timer, currentTime);
timer.isQueued = true;
}
} else {
return;
}
timer = peek(timerQueue);
}
}
function handleTimeout(currentTime) {
isHostTimeoutScheduled = false;
advanceTimers(currentTime);
if (!isHostCallbackScheduled) {
if (peek(taskQueue) !== null) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (
currentTask !== null &&
!(enableSchedulerDebugging && isSchedulerPaused)
) {
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
const callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
markTaskRun(currentTask, currentTime);
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
markTaskYield(currentTask, currentTime);
} else {
if (enableProfiling) {
markTaskCompleted(currentTask, currentTime);
currentTask.isQueued = false;
}
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime);
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
// Return whether there's additional work
if (currentTask !== null) {
return true;
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
SchedulerMinHeap.js
小顶堆的实现,可对比优先队列的考察,具体可以看一下leetcode的这道题 23. 合并K个升序链表,以及对fiber应用的扩展思考 86. 分隔链表
type Heap = Array<Node>;
type Node = {|
id: number,
sortIndex: number,
|};
export function push(heap: Heap, node: Node): void {
const index = heap.length;
heap.push(node);
siftUp(heap, node, index);
}
export function peek(heap: Heap): Node | null {
const first = heap[0];
return first === undefined ? null : first;
}
export function pop(heap: Heap): Node | null {
const first = heap[0];
if (first !== undefined) {
const last = heap.pop();
if (last !== first) {
heap[0] = last;
siftDown(heap, last, 0);
}
return first;
} else {
return null;
}
}
function siftUp(heap, node, i) {
let index = i;
while (true) {
const parentIndex = (index - 1) >>> 1;
const parent = heap[parentIndex];
if (parent !== undefined && compare(parent, node) > 0) {
// The parent is larger. Swap positions.
heap[parentIndex] = node;
heap[index] = parent;
index = parentIndex;
} else {
// The parent is smaller. Exit.
return;
}
}
}
function siftDown(heap, node, i) {
let index = i;
const length = heap.length;
while (index < length) {
const leftIndex = (index + 1) * 2 - 1;
const left = heap[leftIndex];
const rightIndex = leftIndex + 1;
const right = heap[rightIndex];
// If the left or right node is smaller, swap with the smaller of those.
if (left !== undefined && compare(left, node) < 0) {
if (right !== undefined && compare(right, left) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
heap[index] = left;
heap[leftIndex] = node;
index = leftIndex;
}
} else if (right !== undefined && compare(right, node) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
// Neither child is smaller. Exit.
return;
}
}
}
function compare(a, b) {
// Compare sort index first, then task id.
const diff = a.sortIndex - b.sortIndex;
return diff !== 0 ? diff : a.id - b.id;
}
SchedulePostTask.js
const getCurrentTime = perf.now.bind(perf);
export function unstable_shouldYield() {
return getCurrentTime() >= deadline;
}
export function unstable_scheduleCallback<T>(
priorityLevel: PriorityLevel,
callback: SchedulerCallback<T>,
options?: {delay?: number},
): CallbackNode {
let postTaskPriority;
switch (priorityLevel) {
case ImmediatePriority:
case UserBlockingPriority:
postTaskPriority = 'user-blocking';
break;
case LowPriority:
case NormalPriority:
postTaskPriority = 'user-visible';
break;
case IdlePriority:
postTaskPriority = 'background';
break;
default:
postTaskPriority = 'user-visible';
break;
}
const controller = new TaskController();
const postTaskOptions = {
priority: postTaskPriority,
delay: typeof options === 'object' && options !== null ? options.delay : 0,
signal: controller.signal,
};
const node = {
_controller: controller,
};
scheduler
.postTask(
runTask.bind(null, priorityLevel, postTaskPriority, node, callback),
postTaskOptions,
)
.catch(handleAbortError);
return node;
}
function runTask<T>(
priorityLevel: PriorityLevel,
postTaskPriority: PostTaskPriorityLevel,
node: CallbackNode,
callback: SchedulerCallback<T>,
) {
deadline = getCurrentTime() + yieldInterval;
try {
currentPriorityLevel_DEPRECATED = priorityLevel;
const didTimeout_DEPRECATED = false;
const result = callback(didTimeout_DEPRECATED);
if (typeof result === 'function') {
const continuation: SchedulerCallback<T> = (result: any);
const continuationController = new TaskController();
const continuationOptions = {
priority: postTaskPriority,
signal: continuationController.signal,
};
node._controller = continuationController;
scheduler
.postTask(
runTask.bind(
null,
priorityLevel,
postTaskPriority,
node,
continuation,
),
continuationOptions,
)
.catch(handleAbortError);
}
} catch (error) {
setTimeout(() => {
throw error;
});
} finally {
currentPriorityLevel_DEPRECATED = NormalPriority;
}
}
Reconciler & Renderer
在经过了Scheduler的分片及调度后,将分片后的单元调度进合成器中,Reconciler阶段的主要目的是找寻不同,从而对虚拟dom的不同进行fiber层级的派发和合并;对于浏览器的分片可以利用setTimeout及MessageChannel来实现,具体浏览器是如何实现setTimeout的,可以看一下这个浏览器工作原理(16) - setTimeout实现原理,
| 文件名 | 作用 | 备注 |
|---|---|---|
| ReactChildFiber.js | ChildReconciler | 子fiber |
| ReactFiber.js | Fiber | 创建fiber等 |
| ReactFiberBeginWork.js | beginwork | 开始任务 |
| ReactFiberCommitWork.js | commitwork | 提交任务 |
| ReactFiberCompleteWork.js | completework | 完成任务 |
| ReactFiberLane.js | lane | 车道模型 |
| ReactFiberReconciler.js | createContainer、updateContainer | 容器 |
| ReactFiberRoot.js | createFiberRoot | Fiber根节点 |
| ReactFiberWorkLoop.js | all | 循环的各种方法 |
ReactChildFiber.js
function ChildReconciler(shouldTrackSideEffects) {
function deleteChild() {}
function deleteRemainingChildren() {}
function mapRemainingChildren() {}
function useFiber() {}
function placeChild() {}
function placeSingleChild() {}
function updateTextNode() {}
function updateElement() {}
function updatePortal() {}
function updateFragment() {}
function createChild() {}
function updateSlot() {}
function updateFromMap() {}
function warnOnInvalidKey() {}
function reconcileChildrenArray() {}
function reconcileChildrenIterator() {}
function reconcileSingleElement() {}
function reconcileSinglePortal() {}
function reconcileChildFibers() {}
return reconcileChildFibers;
}
ReactFiber.js
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null;
if (enableProfilerTimer) {
// Note: The following is done to avoid a v8 performance cliff.
//
// Initializing the fields below to smis and later updating them with
// double values will cause Fibers to end up having separate shapes.
// This behavior/bug has something to do with Object.preventExtension().
// Fortunately this only impacts DEV builds.
// Unfortunately it makes React unusably slow for some applications.
// To work around this, initialize the fields below with doubles.
//
// Learn more about this here:
// https://github.com/facebook/react/issues/14365
// https://bugs.chromium.org/p/v8/issues/detail?id=8538
this.actualDuration = Number.NaN;
this.actualStartTime = Number.NaN;
this.selfBaseDuration = Number.NaN;
this.treeBaseDuration = Number.NaN;
// It's okay to replace the initial doubles with smis after initialization.
// This won't trigger the performance cliff mentioned above,
// and it simplifies other profiler code (including DevTools).
this.actualDuration = 0;
this.actualStartTime = -1;
this.selfBaseDuration = 0;
this.treeBaseDuration = 0;
}
if (__DEV__) {
// This isn't directly used but is handy for debugging internals:
this._debugID = debugCounter++;
this._debugSource = null;
this._debugOwner = null;
this._debugNeedsRemount = false;
this._debugHookTypes = null;
if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
Object.preventExtensions(this);
}
}
}
const createFiber = function() {}
export const createWorkInProgress = function() {}
export const resetWorkInProgress = function() {}
export const createHostRootFiber = function() {}
ReactFiberBeginWork.js
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const updateLanes = workInProgress.lanes;
if (__DEV__) {
if (workInProgress._debugNeedsRemount && current !== null) {
// This will restart the begin phase with a new fiber.
return remountFiber(
current,
workInProgress,
createFiberFromTypeAndProps(
workInProgress.type,
workInProgress.key,
workInProgress.pendingProps,
workInProgress._debugOwner || null,
workInProgress.mode,
workInProgress.lanes,
),
);
}
}
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
// Force a re-render if the implementation changed due to hot reload:
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// If props or context changed, mark the fiber as having performed work.
// This may be unset if the props are determined to be equal later (memo).
didReceiveUpdate = true;
} else if (!includesSomeLane(renderLanes, updateLanes)) {
didReceiveUpdate = false;
// This fiber does not have any pending work. Bailout without entering
// the begin phase. There's still some bookkeeping we that needs to be done
// in this optimized path, mostly pushing stuff onto the stack.
switch (workInProgress.tag) {
case HostRoot:
pushHostRootContext(workInProgress);
resetHydrationState();
break;
case HostComponent:
pushHostContext(workInProgress);
break;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
pushLegacyContextProvider(workInProgress);
}
break;
}
case HostPortal:
pushHostContainer(
workInProgress,
workInProgress.stateNode.containerInfo,
);
break;
case ContextProvider: {
const newValue = workInProgress.memoizedProps.value;
pushProvider(workInProgress, newValue);
break;
}
case Profiler:
if (enableProfilerTimer) {
// Reset effect durations for the next eventual effect phase.
// These are reset during render to allow the DevTools commit hook a chance to read them,
const stateNode = workInProgress.stateNode;
stateNode.effectDuration = 0;
stateNode.passiveEffectDuration = 0;
}
break;
case SuspenseComponent: {
const state: SuspenseState | null = workInProgress.memoizedState;
if (state !== null) {
if (enableSuspenseServerRenderer) {
if (state.dehydrated !== null) {
pushSuspenseContext(
workInProgress,
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
);
// We know that this component will suspend again because if it has
// been unsuspended it has committed as a resolved Suspense component.
// If it needs to be retried, it should have work scheduled on it.
workInProgress.flags |= DidCapture;
// We should never render the children of a dehydrated boundary until we
// upgrade it. We return null instead of bailoutOnAlreadyFinishedWork.
return null;
}
}
// If this boundary is currently timed out, we need to decide
// whether to retry the primary children, or to skip over it and
// go straight to the fallback. Check the priority of the primary
// child fragment.
const primaryChildFragment: Fiber = (workInProgress.child: any);
const primaryChildLanes = primaryChildFragment.childLanes;
if (includesSomeLane(renderLanes, primaryChildLanes)) {
// The primary children have pending work. Use the normal path
// to attempt to render the primary children again.
return updateSuspenseComponent(
current,
workInProgress,
renderLanes,
);
} else {
// The primary child fragment does not have pending work marked
// on it
pushSuspenseContext(
workInProgress,
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
);
// The primary children do not have pending work with sufficient
// priority. Bailout.
const child = bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
if (child !== null) {
// The fallback children have pending work. Skip over the
// primary children and work on the fallback.
return child.sibling;
} else {
return null;
}
}
} else {
pushSuspenseContext(
workInProgress,
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
);
}
break;
}
case SuspenseListComponent: {
const didSuspendBefore = (current.flags & DidCapture) !== NoFlags;
const hasChildWork = includesSomeLane(
renderLanes,
workInProgress.childLanes,
);
if (didSuspendBefore) {
if (hasChildWork) {
// If something was in fallback state last time, and we have all the
// same children then we're still in progressive loading state.
// Something might get unblocked by state updates or retries in the
// tree which will affect the tail. So we need to use the normal
// path to compute the correct tail.
return updateSuspenseListComponent(
current,
workInProgress,
renderLanes,
);
}
// If none of the children had any work, that means that none of
// them got retried so they'll still be blocked in the same way
// as before. We can fast bail out.
workInProgress.flags |= DidCapture;
}
// If nothing suspended before and we're rendering the same children,
// then the tail doesn't matter. Anything new that suspends will work
// in the "together" mode, so we can continue from the state we had.
const renderState = workInProgress.memoizedState;
if (renderState !== null) {
// Reset to the "together" mode in case we've started a different
// update in the past but didn't complete it.
renderState.rendering = null;
renderState.tail = null;
}
pushSuspenseContext(workInProgress, suspenseStackCursor.current);
if (hasChildWork) {
break;
} else {
// If none of the children had any work, that means that none of
// them got retried so they'll still be blocked in the same way
// as before. We can fast bail out.
return null;
}
}
case OffscreenComponent:
case LegacyHiddenComponent: {
// Need to check if the tree still needs to be deferred. This is
// almost identical to the logic used in the normal update path,
// so we'll just enter that. The only difference is we'll bail out
// at the next level instead of this one, because the child props
// have not changed. Which is fine.
// TODO: Probably should refactor `beginWork` to split the bailout
// path from the normal path. I'm tempted to do a labeled break here
// but I won't :)
workInProgress.lanes = NoLanes;
return updateOffscreenComponent(current, workInProgress, renderLanes);
}
}
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} else {
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
// This is a special case that only exists for legacy mode.
// See https://github.com/facebook/react/pull/19216.
didReceiveUpdate = true;
} else {
// An update was scheduled on this fiber, but there are no new props
// nor legacy context. Set this to false. If an update queue or context
// consumer produces a changed value, it will set this to true. Otherwise,
// the component will assume the children have not changed and bail out.
didReceiveUpdate = false;
}
}
} else {
didReceiveUpdate = false;
}
// Before entering the begin phase, clear pending update priority.
// TODO: This assumes that we're about to evaluate the component and process
// the update queue. However, there's an exception: SimpleMemoComponent
// sometimes bails out later in the begin phase. This indicates that we should
// move this assignment out of the common path and into each branch.
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
updateLanes,
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 SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, renderLanes);
case HostPortal:
return updatePortalComponent(current, workInProgress, renderLanes);
case ForwardRef: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === type
? unresolvedProps
: resolveDefaultProps(type, unresolvedProps);
return updateForwardRef(
current,
workInProgress,
type,
resolvedProps,
renderLanes,
);
}
case Fragment:
return updateFragment(current, workInProgress, renderLanes);
case Mode:
return updateMode(current, workInProgress, renderLanes);
case Profiler:
return updateProfiler(current, workInProgress, renderLanes);
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes);
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderLanes);
case MemoComponent: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
// Resolve outer props first, then resolve inner props.
let resolvedProps = resolveDefaultProps(type, unresolvedProps);
if (__DEV__) {
if (workInProgress.type !== workInProgress.elementType) {
const outerPropTypes = type.propTypes;
if (outerPropTypes) {
checkPropTypes(
outerPropTypes,
resolvedProps, // Resolved for outer only
'prop',
getComponentName(type),
);
}
}
}
resolvedProps = resolveDefaultProps(type.type, resolvedProps);
return updateMemoComponent(
current,
workInProgress,
type,
resolvedProps,
updateLanes,
renderLanes,
);
}
case SimpleMemoComponent: {
return updateSimpleMemoComponent(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
updateLanes,
renderLanes,
);
}
case IncompleteClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return mountIncompleteClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case SuspenseListComponent: {
return updateSuspenseListComponent(current, workInProgress, renderLanes);
}
case FundamentalComponent: {
if (enableFundamentalAPI) {
return updateFundamentalComponent(current, workInProgress, renderLanes);
}
break;
}
case ScopeComponent: {
if (enableScopeAPI) {
return updateScopeComponent(current, workInProgress, renderLanes);
}
break;
}
case Block: {
if (enableBlocksAPI) {
const block = workInProgress.type;
const props = workInProgress.pendingProps;
return updateBlock(current, workInProgress, block, props, renderLanes);
}
break;
}
case OffscreenComponent: {
return updateOffscreenComponent(current, workInProgress, renderLanes);
}
case LegacyHiddenComponent: {
return updateLegacyHiddenComponent(current, workInProgress, renderLanes);
}
}
invariant(
false,
'Unknown unit of work tag (%s). This error is likely caused by a bug in ' +
'React. Please file an issue.',
workInProgress.tag,
);
}
ReactFiberCommitWork.js
对不同的真实dom类型进行对应提交
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
if (!supportsMutation) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
// Layout effects are destroyed during the mutation phase so that all
// destroy functions for all fibers are called before any create functions.
// This prevents sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
}
return;
}
case Profiler: {
return;
}
case SuspenseComponent: {
commitSuspenseComponent(finishedWork);
attachSuspenseRetryListeners(finishedWork);
return;
}
case SuspenseListComponent: {
attachSuspenseRetryListeners(finishedWork);
return;
}
case HostRoot: {
if (supportsHydration) {
const root: FiberRoot = finishedWork.stateNode;
if (root.hydrate) {
// We've just hydrated. No need to hydrate again.
root.hydrate = false;
commitHydratedContainer(root.containerInfo);
}
}
break;
}
case OffscreenComponent:
case LegacyHiddenComponent: {
return;
}
}
commitContainer(finishedWork);
return;
}
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
// Layout effects are destroyed during the mutation phase so that all
// destroy functions for all fibers are called before any create functions.
// This prevents sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
}
return;
}
case ClassComponent: {
return;
}
case HostComponent: {
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
// Commit the work prepared earlier.
const newProps = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldProps = current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
// TODO: Type the updateQueue to be specific to host components.
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
finishedWork.updateQueue = null;
if (updatePayload !== null) {
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
}
}
return;
}
case HostText: {
invariant(
finishedWork.stateNode !== null,
'This should have a text node initialized. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
const textInstance: TextInstance = finishedWork.stateNode;
const newText: string = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldText: string =
current !== null ? current.memoizedProps : newText;
commitTextUpdate(textInstance, oldText, newText);
return;
}
case HostRoot: {
if (supportsHydration) {
const root: FiberRoot = finishedWork.stateNode;
if (root.hydrate) {
// We've just hydrated. No need to hydrate again.
root.hydrate = false;
commitHydratedContainer(root.containerInfo);
}
}
return;
}
case Profiler: {
return;
}
case SuspenseComponent: {
commitSuspenseComponent(finishedWork);
attachSuspenseRetryListeners(finishedWork);
return;
}
case SuspenseListComponent: {
attachSuspenseRetryListeners(finishedWork);
return;
}
case IncompleteClassComponent: {
return;
}
case FundamentalComponent: {
if (enableFundamentalAPI) {
const fundamentalInstance = finishedWork.stateNode;
updateFundamentalComponent(fundamentalInstance);
return;
}
break;
}
case ScopeComponent: {
if (enableScopeAPI) {
const scopeInstance = finishedWork.stateNode;
prepareScopeUpdate(scopeInstance, finishedWork);
return;
}
break;
}
case OffscreenComponent:
case LegacyHiddenComponent: {
const newState: OffscreenState | null = finishedWork.memoizedState;
const isHidden = newState !== null;
hideOrUnhideAllChildren(finishedWork, isHidden);
return;
}
}
invariant(
false,
'This unit of work tag should not have side-effects. This error is ' +
'likely caused by a bug in React. Please file an issue.',
);
}
ReactFiberCompleteWork.js
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
// 一坨case
}
invariant(
false,
'Unknown unit of work tag (%s). This error is likely caused by a bug in ' +
'React. Please file an issue.',
workInProgress.tag,
);
}
实践
import {useState} from 'react';
function App() {
let [count, setCount] = useState(100);
function add() {
setCount(count++)
console.log('add',count)
};
console.log('render',count);
return (
<div>
<h1>{count}</h1>
<p>我是兄弟元素</p >
<button onClick={add}>点我+1</button>
</div>
);
}
export default App;
通过一个cra的实践,看一下整过react的过程,如下:
createRootFiber => FiberRootNode => initialUpdateQueue => updateContainer => createUpdate => scheduleUpdateOnFiber => renderRootSync => workLoopSync => performUnitOfWork => beginWork => updateHostRoot => processUpdateQueue => reconcileChildFibers => reconcileSingleElement => createFiberFromElement => completeUnitWork => completeWork => createInstance => createElement => finalizeInitialChildren
总结
react16之后通过fiber对整个运行时的stack reconciler进行了修改,实现了分片的协程调度,对于层级较深的js调用栈可以实现停止与启动更细粒度的控制,从而避免js线程的长时间占用而导致的渲染线程的卡死,整体的设计体现了react架构人员的计算机素养相当的扎实,对操作系统乃至整体数据结构把控能力之强,可见一斑,从这个层面上看,国外程序员设计者确实在优化性能等方面总是从计算机最底层的思路去着手,值得我们学习与思考。
参考
- react17官方源码
- 结合 React 源码,五分钟带你掌握优先队列
- 如何看待 React Server Components?(网易云音乐前端团队)
- 漫谈 React Fiber
- React17新特性:启发式更新算法
- React Fiber 源码解析
- 深入浅出搞定 React
- React16源码解析
- 彻底搞懂React源码调度原理(Concurrent模式)
- react中的requestIdleCallback实现
- 浅谈React16框架 - Fiber
- react scheduler 再解析篇
- 浅谈React Scheduler任务管理
- 探究 React Work Loop 原理
- react17 lane 算法:位运算和素数相除
- React 源码解析(V16.8.4)
- react源码剖析
- React技术揭秘