前言:
在 React源碼解析之completeUnitOfWork 中,提到了completeWork()
的做用是更新該節點(commit
階段會將其轉成真實的DOM
節點)javascript
本文來解析下completeWork()
源碼html
1、completeWork
做用:
根據組件類型的不一樣,進行不一樣的更新操做java
源碼:node
//更新不一樣的組件/節點
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
//組件的初始狀態
case IndeterminateComponent:
break;
//懶(動態)加載組件
//https://zh-hans.reactjs.org/docs/react-api.html#reactlazy
//也能夠看下這篇文章:React的動態加載(lazy import)https://www.jianshu.com/p/27cc69eb4556
case LazyComponent:
break;
//和 React.memo 相似
//https://zh-hans.reactjs.org/docs/react-api.html#reactmemo
case SimpleMemoComponent:
//函數組件
//https://zh-hans.reactjs.org/docs/components-and-props.html#function-and-class-components
case FunctionComponent:
break;
//類/class 組件
//https://zh-hans.reactjs.org/docs/components-and-props.html#function-and-class-components
case ClassComponent: {
const Component = workInProgress.type;
//======context 相關,暫時跳過==========================
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
break;
}
//fiberRoot 節點的更新
case HostRoot: {
//出棧操做
//將 valueStack 棧中指定位置的 value 賦值給不一樣 StackCursor.current
popHostContainer(workInProgress);
//同上
popTopLevelLegacyContextObject(workInProgress);
// context 相關,可跳過
const fiberRoot = (workInProgress.stateNode: FiberRoot);
if (fiberRoot.pendingContext) {
fiberRoot.context = fiberRoot.pendingContext;
fiberRoot.pendingContext = null;
}
if (current === null || current.child === null) {
// If we hydrated, pop so that we can delete any remaining children
// that weren't hydrated.
popHydrationState(workInProgress);
// This resets the hacky state to fix isMounted before committing.
// TODO: Delete this when we delete isMounted and findDOMNode.
workInProgress.effectTag &= ~Placement;
}
updateHostContainer(workInProgress);
break;
}
//DOM 節點的更新,涉及到 virtual dom
//https://zh-hans.reactjs.org/docs/faq-internals.html#___gatsby
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
//若是不是第一次渲染的話
if (current !== null && workInProgress.stateNode != null) {
//更新 DOM 時進行 diff 判斷
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
//ref指向有變更的話,更新 ref
if (current.ref !== workInProgress.ref) {
////添加 Ref 的 EffectTag
markRef(workInProgress);
}
}
else {
//若是是第一次渲染的話
//多是意外終止了
if (!newProps) {
invariant(
workInProgress.stateNode !== null,
'We must have new props for new mounts. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
// This can happen when we abort work.
break;
}
const currentHostContext = getHostContext();
// TODO: Move createInstance to beginWork and keep it on a context
// "stack" as the parent. Then append children as we go in beginWork
// or completeWork depending on we want to add then top->down or
// bottom->up. Top->down is faster in IE11.
//曾是服務端渲染
let wasHydrated = popHydrationState(workInProgress);
//若是是服務端渲染的話,暫時跳過
if (wasHydrated) {
// TODO: Move this and createInstance step into the beginPhase
// to consolidate.
if (
prepareToHydrateHostInstance(
workInProgress,
rootContainerInstance,
currentHostContext,
)
) {
// If changes to the hydrated node needs to be applied at the
// commit-phase we mark this as such.
markUpdate(workInProgress);
}
}
//不是服務端渲染
else {
//建立 fiber 實例,即 DOM 實例
let instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
appendAllChildren(instance, workInProgress, false, false);
// Certain renderers require commit-time effects for initial mount.
// (eg DOM renderer supports auto-focus for certain elements).
// Make sure such renderers get scheduled for later work.
if (
//初始化事件監聽
//若是該節點可以自動聚焦的話
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
//添加 EffectTag,方便在 commit 階段 update
markUpdate(workInProgress);
}
//將處理好的節點實例綁定到 stateNode 上
workInProgress.stateNode = instance;
}
//若是 ref 引用不爲空的話
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
//添加 Ref 的 EffectTag
markRef(workInProgress);
}
}
break;
}
//文本節點的更新
case HostText: {
let newText = newProps;
//若是不是第一次渲染的話
if (current && workInProgress.stateNode != null) {
const oldText = current.memoizedProps;
// If we have an alternate, that means this is an update and we need
// to schedule a side-effect to do the updates.
//若是與workInProgress相對於的alternate存在的話,說明有更新
//那麼就添加 Update 的 effectTag
updateHostText(current, workInProgress, oldText, newText);
}
//若是是第一次渲染的話
else {
//當文本節點更新的內容不是 string 類型的話,說明 React 內部出現了 error
if (typeof newText !== 'string') {
invariant(
workInProgress.stateNode !== null,
'We must have new props for new mounts. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
// This can happen when we abort work.
}
// context 相關,暫時跳過
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext();
//曾是服務端渲染
let wasHydrated = popHydrationState(workInProgress);
//若是是服務端渲染的話,暫時跳過
if (wasHydrated) {
if (prepareToHydrateHostTextInstance(workInProgress)) {
markUpdate(workInProgress);
}
}
//不是服務端渲染
else {
//第一次渲染的話,建立文本節點的實例並賦值給 stateNode
workInProgress.stateNode = createTextInstance(
newText,
rootContainerInstance,
currentHostContext,
workInProgress,
);
}
}
break;
}
//React.forwardRef 組件的更新
//https://zh-hans.reactjs.org/docs/react-api.html#reactforwardref
case ForwardRef:
break;
//suspense 組件的更新
//https://zh-hans.reactjs.org/docs/concurrent-mode-reference.html#suspense
case SuspenseComponent: {
popSuspenseContext(workInProgress);
const nextState: null | SuspenseState = workInProgress.memoizedState;
if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
// Something suspended. Re-render with the fallback children.
workInProgress.expirationTime = renderExpirationTime;
// Do not reset the effect list.
return workInProgress;
}
const nextDidTimeout = nextState !== null;
let prevDidTimeout = false;
if (current === null) {
// In cases where we didn't find a suitable hydration boundary we never
// downgraded this to a DehydratedSuspenseComponent, but we still need to
// pop the hydration state since we might be inside the insertion tree.
popHydrationState(workInProgress);
} else {
const prevState: null | SuspenseState = current.memoizedState;
prevDidTimeout = prevState !== null;
if (!nextDidTimeout && prevState !== null) {
// We just switched from the fallback to the normal children.
// Delete the fallback.
// TODO: Would it be better to store the fallback fragment on
// the stateNode during the begin phase?
const currentFallbackChild: Fiber | null = (current.child: any)
.sibling;
if (currentFallbackChild !== null) {
// Deletions go at the beginning of the return fiber's effect list
const first = workInProgress.firstEffect;
if (first !== null) {
workInProgress.firstEffect = currentFallbackChild;
currentFallbackChild.nextEffect = first;
} else {
workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChild;
currentFallbackChild.nextEffect = null;
}
currentFallbackChild.effectTag = Deletion;
}
}
}
if (nextDidTimeout && !prevDidTimeout) {
// If this subtreee is running in batched mode we can suspend,
// otherwise we won't suspend.
// TODO: This will still suspend a synchronous tree if anything
// in the concurrent tree already suspended during this render.
// This is a known bug.
if ((workInProgress.mode & BatchedMode) !== NoMode) {
// TODO: Move this back to throwException because this is too late
// if this is a large tree which is common for initial loads. We
// don't know if we should restart a render or not until we get
// this marker, and this is too late.
// If this render already had a ping or lower pri updates,
// and this is the first time we know we're going to suspend we
// should be able to immediately restart from within throwException.
const hasInvisibleChildContext =
current === null &&
workInProgress.memoizedProps.unstable_avoidThisFallback !== true;
if (
hasInvisibleChildContext ||
hasSuspenseContext(
suspenseStackCursor.current,
(InvisibleParentSuspenseContext: SuspenseContext),
)
) {
// If this was in an invisible tree or a new render, then showing
// this boundary is ok.
renderDidSuspend();
} else {
// Otherwise, we're going to have to hide content so we should
// suspend for longer if possible.
renderDidSuspendDelayIfPossible();
}
}
}
if (supportsPersistence) {
// TODO: Only schedule updates if not prevDidTimeout.
if (nextDidTimeout) {
// If this boundary just timed out, schedule an effect to attach a
// retry listener to the proimse. This flag is also used to hide the
// primary children.
workInProgress.effectTag |= Update;
}
}
if (supportsMutation) {
// TODO: Only schedule updates if these values are non equal, i.e. it changed.
if (nextDidTimeout || prevDidTimeout) {
// If this boundary just timed out, schedule an effect to attach a
// retry listener to the proimse. This flag is also used to hide the
// primary children. In mutation mode, we also need the flag to
// *unhide* children that were previously hidden, so check if the
// is currently timed out, too.
workInProgress.effectTag |= Update;
}
}
break;
}
//React.Fragment 的更新
//https://zh-hans.reactjs.org/docs/react-api.html#reactfragment
case Fragment:
break;
//暫時不知道是什麼組件/節點
case Mode:
break;
//Profiler 組件的更新
//https://zh-hans.reactjs.org/docs/profiler.html#___gatsby
case Profiler:
break;
//React.createportal 節點的更新
//https://zh-hans.reactjs.org/docs/react-dom.html#createportal
case HostPortal:
popHostContainer(workInProgress);
updateHostContainer(workInProgress);
break;
//Context.Provider 組件的更新
//https://zh-hans.reactjs.org/docs/context.html#contextprovider
case ContextProvider:
// Pop provider fiber
popProvider(workInProgress);
break;
//Context.Consumer 組件的更新
//https://zh-hans.reactjs.org/docs/context.html#contextconsumer
case ContextConsumer:
break;
//React.Memo 組件的更新
//https://zh-hans.reactjs.org/docs/react-api.html#reactmemo
case MemoComponent:
break;
//未完成/被中斷的 class 組件的更新
case IncompleteClassComponent: {
// Same as class component case. I put it down here so that the tags are
// sequential to ensure this switch is compiled to a jump table.
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
break;
}
//不是 server 端渲染的 suspense 組件的更新
case DehydratedSuspenseComponent: {
if (enableSuspenseServerRenderer) {
popSuspenseContext(workInProgress);
if (current === null) {
let wasHydrated = popHydrationState(workInProgress);
invariant(
wasHydrated,
'A dehydrated suspense component was completed without a hydrated node. ' +
'This is probably a bug in React.',
);
if (enableSchedulerTracing) {
markSpawnedWork(Never);
}
skipPastDehydratedSuspenseInstance(workInProgress);
} else if ((workInProgress.effectTag & DidCapture) === NoEffect) {
// This boundary did not suspend so it's now hydrated.
// To handle any future suspense cases, we're going to now upgrade it
// to a Suspense component. We detach it from the existing current fiber.
current.alternate = null;
workInProgress.alternate = null;
workInProgress.tag = SuspenseComponent;
workInProgress.memoizedState = null;
workInProgress.stateNode = null;
}
}
break;
}
//SuspenseList 組件的更新
//https://zh-hans.reactjs.org/docs/concurrent-mode-reference.html#suspenselist
case SuspenseListComponent: {
popSuspenseContext(workInProgress);
const renderState: null | SuspenseListRenderState =
workInProgress.memoizedState;
if (renderState === null) {
// We're running in the default, "independent" mode. We don't do anything
// in this mode.
break;
}
let didSuspendAlready =
(workInProgress.effectTag & DidCapture) !== NoEffect;
let renderedTail = renderState.rendering;
if (renderedTail === null) {
// We just rendered the head.
if (!didSuspendAlready) {
// This is the first pass. We need to figure out if anything is still
// suspended in the rendered set.
const renderedChildren = workInProgress.child;
// If new content unsuspended, but there's still some content that
// didn't. Then we need to do a second pass that forces everything
// to keep showing their fallbacks.
// We might be suspended if something in this render pass suspended, or
// something in the previous committed pass suspended. Otherwise,
// there's no chance so we can skip the expensive call to
// hasSuspendedChildrenAndNewContent.
let cannotBeSuspended =
renderHasNotSuspendedYet() &&
(current === null || (current.effectTag & DidCapture) === NoEffect);
let needsRerender =
!cannotBeSuspended &&
hasSuspendedChildrenAndNewContent(workInProgress, renderedChildren);
if (needsRerender) {
// Rerender the whole list, but this time, we'll force fallbacks
// to stay in place.
// Reset the effect list before doing the second pass since that's now invalid.
workInProgress.firstEffect = workInProgress.lastEffect = null;
// Reset the child fibers to their original state.
resetChildFibers(workInProgress, renderExpirationTime);
// Set up the Suspense Context to force suspense and immediately
// rerender the children.
pushSuspenseContext(
workInProgress,
setShallowSuspenseContext(
suspenseStackCursor.current,
ForceSuspenseFallback,
),
);
return workInProgress.child;
}
// hasSuspendedChildrenAndNewContent could've set didSuspendAlready
didSuspendAlready =
(workInProgress.effectTag & DidCapture) !== NoEffect;
}
if (didSuspendAlready) {
cutOffTailIfNeeded(renderState, false);
}
// Next we're going to render the tail.
} else {
// Append the rendered row to the child list.
if (!didSuspendAlready) {
if (isShowingAnyFallbacks(renderedTail)) {
workInProgress.effectTag |= DidCapture;
didSuspendAlready = true;
cutOffTailIfNeeded(renderState, true);
} else if (
now() > renderState.tailExpiration &&
renderExpirationTime > Never
) {
// We have now passed our CPU deadline and we'll just give up further
// attempts to render the main content and only render fallbacks.
// The assumption is that this is usually faster.
workInProgress.effectTag |= DidCapture;
didSuspendAlready = true;
cutOffTailIfNeeded(renderState, false);
// Since nothing actually suspended, there will nothing to ping this
// to get it started back up to attempt the next item. If we can show
// them, then they really have the same priority as this render.
// So we'll pick it back up the very next render pass once we've had
// an opportunity to yield for paint.
const nextPriority = renderExpirationTime - 1;
workInProgress.expirationTime = workInProgress.childExpirationTime = nextPriority;
if (enableSchedulerTracing) {
markSpawnedWork(nextPriority);
}
}
}
if (renderState.isBackwards) {
// The effect list of the backwards tail will have been added
// to the end. This breaks the guarantee that life-cycles fire in
// sibling order but that isn't a strong guarantee promised by React.
// Especially since these might also just pop in during future commits.
// Append to the beginning of the list.
renderedTail.sibling = workInProgress.child;
workInProgress.child = renderedTail;
} else {
let previousSibling = renderState.last;
if (previousSibling !== null) {
previousSibling.sibling = renderedTail;
} else {
workInProgress.child = renderedTail;
}
renderState.last = renderedTail;
}
}
if (renderState.tail !== null) {
// We still have tail rows to render.
if (renderState.tailExpiration === 0) {
// Heuristic for how long we're willing to spend rendering rows
// until we just give up and show what we have so far.
const TAIL_EXPIRATION_TIMEOUT_MS = 500;
renderState.tailExpiration = now() + TAIL_EXPIRATION_TIMEOUT_MS;
}
// Pop a row.
let next = renderState.tail;
renderState.rendering = next;
renderState.tail = next.sibling;
next.sibling = null;
// Restore the context.
// TODO: We can probably just avoid popping it instead and only
// setting it the first time we go from not suspended to suspended.
let suspenseContext = suspenseStackCursor.current;
if (didSuspendAlready) {
suspenseContext = setShallowSuspenseContext(
suspenseContext,
ForceSuspenseFallback,
);
} else {
suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
}
pushSuspenseContext(workInProgress, suspenseContext);
// Do a pass over the next row.
return next;
}
break;
}
//事件組件 的更新,暫未找到相關資料
case EventComponent: {
if (enableFlareAPI) {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const responder = workInProgress.type.responder;
let eventComponentInstance: ReactEventComponentInstance<
any,
any,
any,
> | null =
workInProgress.stateNode;
if (eventComponentInstance === null) {
let responderState = null;
if (__DEV__ && !responder.allowMultipleHostChildren) {
const hostChildrenCount = getEventComponentHostChildrenCount(
workInProgress,
);
warning(
(hostChildrenCount || 0) < 2,
'A "<%s>" event component cannot contain multiple host children.',
getComponentName(workInProgress.type),
);
}
const getInitialState = responder.getInitialState;
if (getInitialState !== undefined) {
responderState = getInitialState(newProps);
}
eventComponentInstance = workInProgress.stateNode = createEventComponentInstance(
workInProgress,
newProps,
responder,
rootContainerInstance,
responderState || {},
false,
);
markUpdate(workInProgress);
} else {
// Update the props on the event component state node
eventComponentInstance.props = newProps;
// Update the current fiber
eventComponentInstance.currentFiber = workInProgress;
updateEventComponent(eventComponentInstance);
}
}
break;
}
default:
invariant(
false,
'Unknown unit of work tag. This error is likely caused by a bug in ' +
'React. Please file an issue.',
);
}
return null;
}
複製代碼
解析:
乍一看很長,可是 是根據fiber
對象的tag
屬性區分不一樣的組件/節點,而後不一樣的case
內,有不一樣的操做react
應該是羅列了 React 中全部類型的組件和節點,絕大部分能在開發層面中用到git
① 在開發層面用到的組件/節點,均註釋了官網連接,可前去查看做用及使用github
② 主要講HostComponent
(下篇文章講)和HostText
的更新,由於這兩個是涉及到DOM
/文本標籤的更新,典型且經常使用web
2、HostText
做用:
建立或更新文本節點api
源碼:數組
//文本節點的更新
case HostText: {
//因爲是文本節點,因此 newProps 是 string 字符串
let newText = newProps;
//若是不是第一次渲染的話
if (current && workInProgress.stateNode != null) {
const oldText = current.memoizedProps;
// If we have an alternate, that means this is an update and we need
// to schedule a side-effect to do the updates.
//若是與workInProgress相對於的alternate存在的話,說明有更新
//那麼就添加 Update 的 effectTag
updateHostText(current, workInProgress, oldText, newText);
}
//若是是第一次渲染的話
else {
//當文本節點更新的內容不是 string 類型的話,說明 React 內部出現了 error
if (typeof newText !== 'string') {
invariant(
workInProgress.stateNode !== null,
'We must have new props for new mounts. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
// This can happen when we abort work.
}
// context 相關,暫時跳過
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext();
//曾是服務端渲染
let wasHydrated = popHydrationState(workInProgress);
//若是是服務端渲染的話,暫時跳過
if (wasHydrated) {
if (prepareToHydrateHostTextInstance(workInProgress)) {
markUpdate(workInProgress);
}
}
//不是服務端渲染
else {
//第一次渲染的話,建立文本節點的實例並賦值給 stateNode
workInProgress.stateNode = createTextInstance(
newText,
rootContainerInstance,
currentHostContext,
workInProgress,
);
}
}
break;
}
複製代碼
解析:
(1) 若是不是第一次渲染的話,則執行updateHostText()
來更新文本節點
(2) 若是是第一次渲染的話,則執行createTextInstance()
,來建立文本節點的實例並賦值給 stateNode
3、updateHostText
做用:
判斷更新文本節點
源碼:
//判斷文本節點是否須要更新
updateHostText = function(
current: Fiber,
workInProgress: Fiber,
oldText: string,
newText: string,
) {
// If the text differs, mark it as an update. All the work in done in commitWork.
//因爲文本就是 string,可直接經過 === 判斷便可
if (oldText !== newText) {
//添加 Update 的 EffectTag
markUpdate(workInProgress);
}
};
複製代碼
解析:
文本節點判斷是否更新,直接使用===
便可
4、markUpdate
做用:
添加Update
的EffectTag
源碼:
//添加 Update 的 EffectTag
function markUpdate(workInProgress: Fiber) {
// Tag the fiber with an update effect. This turns a Placement into
// a PlacementAndUpdate.
workInProgress.effectTag |= Update;
}
複製代碼
解析:
添加反作用後,會在 commit 階段進行真正的更新
5、createTextInstance
做用:
建立文本節點的實例
源碼:
//建立文本節點的實例
export function createTextInstance(
text: string,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): TextInstance {
//刪除了 dev 代碼
//建立文本節點
const textNode: TextInstance = createTextNode(text, rootContainerInstance);
//將 fiber 對象做爲文本節點的屬性 __reactInternalInstance,
//方便從節點上找到 fiber 對象
precacheFiberNode(internalInstanceHandle, textNode);
return textNode;
}
複製代碼
解析:
(1) 執行createTextNode()
來建立文本節點
(2) 執行precacheFiberNode()
,將fiber
對象做爲文本節點的屬性
我寫了一個簡單的例子來驗證:
function App() {
const [text, setText] = React.useState(null);
useEffect(()=>{
setTimeout(()=>{
setText('aaa')
},5000)
},[])
return (
<>
{text}
</>
);
}
export default App;
複製代碼
5秒後建立文本節點:
經過斷點能夠看到:經過textNode
的__reactInternalInstance$mru28dy3wf
屬性,可找到其fiber
對象
6、createTextNode
做用:
建立文本節點
源碼:
//建立文本節點
export function createTextNode(
text: string,
rootContainerElement: Element | Document,
): Text {
//獲取 document 對象後,經過 document.createTextNode 來建立文本節點
//詳情請看:https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createTextNode
return getOwnerDocumentFromRootContainer(rootContainerElement).createTextNode(
text,
);
}
複製代碼
解析:
(1) 執行getOwnerDocumentFromRootContainer
獲取document
對象:
//獲取根節點的 document 對象
function getOwnerDocumentFromRootContainer(
rootContainerElement: Element | Document,
): Document {
return rootContainerElement.nodeType === DOCUMENT_NODE
? (rootContainerElement: any)
: rootContainerElement.ownerDocument;
}
複製代碼
(2) 調用document.createTextNode()
來建立文本節點,詳情請看:
developer.mozilla.org/zh-CN/docs/…
注意:
這裏還處於reconciliation
(diff
階段),因此textNode
是一個對象,
到了commit
(操做DOM
階段)後,才轉爲DOM
中的文本節點
7、precacheFiberNode
做用:
將fiber
對象做爲textNode
的屬性
源碼:
const randomKey = Math.random()
//轉成 36 進制
.toString(36)
//從index=2開始截取
.slice(2);
//隨機 key
const internalInstanceKey = '__reactInternalInstance$' + randomKey;
//將 fiber 對象賦值到 DOM 節點上
export function precacheFiberNode(hostInst, node) {
node[internalInstanceKey] = hostInst;
}
複製代碼
解析:
8、GitHubReactFiberCompleteWork.js
:
github.com/AttackXiaoJ…
ReactDOMHostConfig.js
:
github.com/AttackXiaoJ…
ReactDOMComponent.js
:
github.com/AttackXiaoJ…
(完)