react源码阅读二
Renderer 篇
- Fiber节点如何被创建,并且构建Fiber树?
render 阶段 开始于 performSyncWorkOnRoot 和performConcurrentWorkOnRoot 这两个方法的调用,至此,先不学这两个方法,
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
可以看到,两者区别只是,是否调用shouldYield 方法,若是当前帧没得剩余时间,shouldYield ,会终止循环,等到浏览器有剩余时间,继续遍历。
注:
workInprogress 代表当前创建的Fiber。
performUnitOfWork方法会创建下一个Fiber节点并赋值给 workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树。
Fiber Reconciler 的 ‘’ 递’’ 和’‘归’’ 阶段
- Fiber Reconciler 首先是从 Stack Reconciler 阶段重构来的,通过遍历的方式,实现可中断的递归,
- “ 递”的阶段
首先从rootFiber开始向下深度优先遍历。为遍历到的每个Fiber节点调用beginWork方法 。该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。
// 源码的位置 根据传入的fiber节点来创建子Fiber节点,
function beginWork(
// 当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate
current: Fiber | null,
// 当前组件对应的Fiber节点
workInProgress: Fiber,
// 优先级相关,在学习Scheduler时再讲解
renderLanes: Lanes,
): Fiber | null {
let 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,
),
);
}
}
// **update 阶段**
//注释解:**oldProps === newProps && workInProgress.type === current.type,即props与fiber.type不变
//!includesSomeLane(renderLanes, updateLanes),即当前Fiber节点优先级不够,会在讲解Scheduler时介绍**
if (current !== null) {
// TODO: The factoring of this block is weird.
if (
enableLazyContextPropagation &&
!includesSomeLane(renderLanes, updateLanes)
) {
const dependencies = current.dependencies;
if (dependencies !== null && checkIfContextChanged(dependencies)) {
updateLanes = mergeLanes(updateLanes, renderLanes);
}
}
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);
if (enableCache) {
const root: FiberRoot = workInProgress.stateNode;
const cache: Cache = current.memoizedState.cache;
pushCacheProvider(workInProgress, cache);
pushRootCachePool(root);
}
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;
const context: ReactContext<any> = workInProgress.type._context;
pushProvider(workInProgress, context, newValue);
break;
}
case Profiler:
if (enableProfilerTimer) {
// Profiler should only call onRender when one of its descendants actually rendered.
const hasChildWork = includesSomeLane(
renderLanes,
workInProgress.childLanes,
);
if (hasChildWork) {
workInProgress.flags |= Update;
}
if (enableProfilerCommitHooks) {
// 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 {
// Note: We can return `null` here because we already checked
// whether there were nested context consumers, via the call to
// `bailoutOnAlreadyFinishedWork` above.
return null;
}
}
} else {
pushSuspenseContext(
workInProgress,
setDefaultShallowSuspenseContext(suspenseStackCursor.current),
);
}
break;
}
case SuspenseListComponent: {
const didSuspendBefore = (current.flags & DidCapture) !== NoFlags;
let hasChildWork = includesSomeLane(
renderLanes,
workInProgress.childLanes,
);
if (enableLazyContextPropagation && !hasChildWork) {
// Context changes may not have been propagated yet. We need to do
// that now, before we can decide whether to bail out.
// TODO: We use `childLanes` as a heuristic for whether there is
// remaining work in a few places, including
// `bailoutOnAlreadyFinishedWork` and
// `updateDehydratedSuspenseComponent`. We should maybe extract this
// into a dedicated function.
lazilyPropagateParentContextChanges(
current,
workInProgress,
renderLanes,
);
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;
renderState.lastEffect = 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);
}
case CacheComponent: {
if (enableCache) {
const cache: Cache = current.memoizedState.cache;
pushCacheProvider(workInProgress, cache);
}
break;
}
}
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;
// mount 阶段
// 以下case 省略
switch (workInProgress.tag) {
case IndeterminateComponent: {}
case LazyComponent: {}
case FunctionComponent: {}
case ClassComponent: {}
case HostRoot:;
case HostComponent:
case HostText:;
case SuspenseComponent:;
case HostPortal:;
case ForwardRef: {}
case Fragment:;
case Mode:;
case Profiler:;
case ContextProvider:;
case ContextConsumer:{}
case SimpleMemoComponent: {}
case IncompleteClassComponent: {}
case SuspenseListComponent: {}
case ScopeComponent: {}
case OffscreenComponent: {}
case LegacyHiddenComponent: {}
case CacheComponent: {
if (enableCache) {
return updateCacheComponent(
current,
workInProgress,
updateLanes,
renderLanes,
);
}
break;
}
}
invariant(
false,
'Unknown unit of work tag (%s). This error is likely caused by a bug in ' +
'React. Please file an issue.',
workInProgress.tag,
);
}
- 对于我们常见的类组件、函数组件、原生组件,最后会进入reconcileChildren这个方法。如下:
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
if (current === null) {
// If this is a fresh new component that hasn't been rendered yet, we
// won't update its child set by applying minimal side-effects. Instead,
// we will add them all to the child before it gets rendered. That means
// we can optimize this reconciliation pass by not tracking side-effects.
// mount 的组件
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// If the current child is the same as the work in progress, it means that
// we haven't yet started any work on these children. Therefore, we use
// the clone algorithm to create a copy of all the current children.
// If we had any progressed work already, that is invalid at this point so
// let's throw it out.
// update 组件
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
- effectTag
通常render阶段是在内存中进行的,当结束之后会通知render 需要执行的Dom操作。具体操作的Dom类型在
packages/react-reconciler/src/ReactWorkTags.js中。- 若要通知,render将fiber节点插入到Dom中,则需要两个条件:
- fiber.stateNode存在,即Fiber节点中保存了对应的DOM节点
- (fiber.effectTag & Placement) !== 0,即Fiber节点存在Placement effectTag
我们知道,mount时,fiber.stateNode === null,且在reconcileChildren中调用的mountChildFibers不会为Fiber节点赋值effectTag。那么首屏渲染如何完成呢?针对第一个问题,fiber.stateNode会在completeWork中创建,我们会在下一节介绍。第二个问题的答案十分巧妙:假设mountChildFibers也会赋值effectTag,那么可以预见mount时整棵Fiber树所有节点都会有Placement effectTag。那么commit阶段在执行DOM操作时每个节点都会执行一次插入操作,这样大量的DOM操作是极低效的。为了解决这个问题,在mount时只有rootFiber会赋值Placement effectTag,在commit阶段只会执行一次插入操作。
- 若要通知,render将fiber节点插入到Dom中,则需要两个条件:
模拟react-Fiber链表的遍历实现
- 声明一个节点类
class Node {
constructor(instance) {
this.instance = instance;
this.child = null;
this.Sibling = null;
this.return = null;
}
}
- 获取 一个节点数组,并且将它们链接在一起的函数 使用它来 链接 render 方法
function link(parent, elements) {
if (elements === null) elements = [];
parent.child = elements.reduceRight((previous, current) => {
const node = new Node(current);
node.return = parent;
node.sibling = previous;
return node;
}, null);
return parent.child;
-
该函数迭代从最后一个节点开始的节点数组,并且将它们链接到单独的链表中。返回列表中第一个同级的引用,下面演示如何工作:
const children = [{name: 'b1'}, {name: 'b2'}]; const parent = new Node({name: 'a1'}); const child = link(parent, children); console.log(child.instance.name === 'b1'); console.log(child.sibling.instance === children[1]);
-
我们还将实现一个helper函数,为节点执行一些工作。在我们的例子中,它将记录组件的名称。但除此之外,它还检索组件的子级并将它们链接在一起:
function doWork(node) {
console.log(node.instance.name);
const children = node.instance.render();
return link(node, children);
}
- 父级优先,fiber树的深度遍历
function walk(o) {
let root = o;
let current = o;
while (true) {
// 此循环我们可以随时停止遍历,稍后再继续。这种效果刚好满足我们的条件,可以使用新的requestIdleCallback 编程接口
// perform work for a node, retrieve & link the children
let child = doWork(current);
// if there's a child, set it as the current active node
if (child) {
current = child;
continue;
}
// if we've returned to the top, exit the function
if (current === root) {
return;
}
// keep going up until we find the sibling
while (!current.sibling) {
// if we've returned to the top, exit the function
if (!current.return || current.return === root) {
return;
}
// set the parent as the current active node
current = current.return;
}
// if found, set the sibling as the current active node
current = current.sibling;
}
}