React源碼解析系列文章歡迎閱讀:
React16源碼解析(一)- 圖解Fiber架構
React16源碼解析(二)-創建更新
React16源碼解析(三)-ExpirationTime
React16源碼解析(四)-Scheduler
React16源碼解析(五)-更新流程渲染階段1
React16源碼解析(六)-更新流程渲染階段2
React16源碼解析(七)-更新流程渲染階段3
React16源碼解析(八)-更新流程提交階段
正在更新中...
還記得我們在performUnitOfWork中調用了beginWork,beginWork會沿着子樹一直更新,每次都會返回當前節點的child。就算有多個child也只會返回第一個。那麼沿着樹的結構到達葉子節點的時候,已經沒有child了,所以beginWork返回null。如果返回null的話,就會調用completeUnitOfWork。
再瞧一眼代碼:
// 開始組件更新
function performUnitOfWork(workInProgress: Fiber): Fiber | null {
// 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.
// 獲得 fiber 的替身,調和這一階段都是在替身上完成的
// 然後直接看 beginWork
const current = workInProgress.alternate;
// ......
let next;
// .....
// 開始工作
next = beginWork(current, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;
// ......
// 當前fiber樹已經到達葉子節點了
if (next === null) {
// If this doesn't spawn new work, complete the current work.
next = completeUnitOfWork(workInProgress);
}
ReactCurrentOwner.current = null;
return next;
}
completeUnitOfWork
這個completeUnitOfWork幹了什麼呢?主要有以下三點:
1、根據是否中斷調用不同的處理方法
2、判斷是否有兄弟節點來執行不同的操作
3、完成節點之後賦值effect鏈
function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
// 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.
while (true) {
// 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 = workInProgress.alternate;
const returnFiber = workInProgress.return;
const siblingFiber = workInProgress.sibling;
// 沒有錯誤捕獲,正常的渲染邏輯
if ((workInProgress.effectTag & Incomplete) === NoEffect) {
// This fiber completed.
// 完成節點的更新
nextUnitOfWork = completeWork(
current,
workInProgress,
nextRenderExpirationTime,
);
// 重置 childExpirationTime
resetChildExpirationTime(workInProgress, nextRenderExpirationTime);
// 構建 effect 鏈,供 commitRoot 提交階段使用
if (
returnFiber !== null &&
// Do not append effects to parents if a sibling failed to complete
(returnFiber.effectTag & Incomplete) === NoEffect
) {
// Append all the effects of the subtree and this fiber onto the effect
// list of the parent. The completion order of the children affects the
// side-effect order.
// 把自己身上的effect鏈粘在父節點的effect後面
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = workInProgress.firstEffect;
}
if (workInProgress.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber. .nextEffect = workInProgress.firstEffect;
}
returnFiber.lastEffect = workInProgress.lastEffect;
}
// If this fiber had side-effects, we append it AFTER the children's
// side-effects. We can perform certain side-effects earlier if
// needed, by doing multiple passes over the effect list. We don't want
// to schedule our own side-effect on our own list because if end up
// reusing children we'll schedule this effect onto itself since we're
// at the end.
const effectTag = workInProgress.effectTag;
// Skip both NoWork and PerformedWork tags when creating the effect list.
// PerformedWork effect is read by React DevTools but shouldn't be committed.
// 發現自己本身也有effect , 那麼要把自己也加入父節點的effect鏈上
if (effectTag > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = workInProgress;
} else {
returnFiber.firstEffect = workInProgress;
}
returnFiber.lastEffect = workInProgress;
}
}
// 有兄弟節點返回兄弟節點,繼續走beinWork
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
return siblingFiber;
} else if (returnFiber !== null) {
// 沒有兄弟節點找父節點
// If there's no more work in this returnFiber. Complete the returnFiber.
workInProgress = returnFiber;
continue;
} else {
// We've reached the root.
// 一直向上或者向右找兄弟節點,找到null到達root頂點結束,更新階段完成準備進入commitRoot提交階段
return null;
}
} else {
// ......
return null;
}
}
// Without this explicit null return Flow complains of invalid return type
// TODO Remove the above while(true) loop
// eslint-disable-next-line no-unreachable
return null;
}
completeWork
通過下面函數我們可以看到,函數根據workInProgress.tag對不同的類型節點做不同的處理,對大部分 tag 不進行操作或者只是 pop context,只有 HostComponent, HostText, SuspenseComponent 有稍微複雜點的操作。接下來我主要分析HostComponent和HostText。SuspenseComponent後續再進行講解。
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
break;
case LazyComponent:
break;
case SimpleMemoComponent:
case FunctionComponent:
break;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
break;
}
case HostRoot: {
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
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;
}
case HostComponent: {
// 這裏稍微複雜,稍後講解
break;
}
case HostText: {
// 稍後講解
break;
}
case ForwardRef:
break;
case SuspenseComponent: {
const nextState = workInProgress.memoizedState;
const prevState = current !== null ? current.memoizedState : null;
const nextDidTimeout = nextState !== null && nextState.didTimeout;
const prevDidTimeout = prevState !== null && prevState.didTimeout;
if (nextDidTimeout !== prevDidTimeout) {
// If this render commits, and it switches between the normal state
// and the timed-out state, schedule an effect.
workInProgress.effectTag |= Update;
}
break;
}
case Fragment:
break;
case Mode:
break;
case Profiler:
break;
case HostPortal:
popHostContainer(workInProgress);
updateHostContainer(workInProgress);
break;
case ContextProvider:
// Pop provider fiber
popProvider(workInProgress);
break;
case ContextConsumer:
break;
case MemoComponent:
break;
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;
}
default:
invariant(
false,
'Unknown unit of work tag. This error is likely caused by a bug in ' +
'React. Please file an issue.',
);
}
return null;
}
HostComponent
之前我們已經講過,tag 爲 HostComponent 表示普通 dom 節點,如: div。
簡單概括:
1、createInstance: 創建dom
2、appendAllChildren: 將children的host Component添加到剛創建的dom上 組成dom樹。
3、finalizeInitialChildren: 給dom設置屬性。
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
// 首次渲染
// ......
// 創建instance , 就是創建dom節點對象 , 這個對象包含了fiber,和 props 信息
let instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 構建dom樹,因爲我們是從下往上的,所以我們只需把我下面第一層子節點append到自己下面就好了
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,
)
) {
// 如果需要 auto focus
// 標記effect爲UPDATE
markUpdate(workInProgress);
}
// stateNode指向創建好的dom節點
workInProgress.stateNode = instance;
// ......
}
break;
}
接下來我們先將首次渲染的情況
createInstance
1、創建 dom 節點
2、在 dom 節點對象上記錄此次創建的 fiber 和 props 信息
export function createInstance(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object, // 傳入的當前節點的workInProgress
): Instance {
let parentNamespace: string;
// ......
parentNamespace = ((hostContext: any): HostContextProd);
// 創建dom節點
const domElement: Instance = createElement(
type,
props,
rootContainerInstance,
parentNamespace,
);
// 給domElement[__reactInternalInstance$] = internalInstanceHandle。
// 也就是指向了對應的fiber節點
precacheFiberNode(internalInstanceHandle, domElement);
// domElement[__reactEventHandlers$] = props
updateFiberProps(domElement, props);
return domElement;
}
appendAllChildren
因爲我們是從下往上的,所以我們只需把我下面第一層子節點append到自己下面就好了。
1、對node 的 sibling兄弟節點進行遍歷
2、如果是dom原生節點或者是文本,直接appendChild
3、如果是其他節點但是有子節點,那麼轉而去遍歷它的子節點,直到找到dom原生節點或者是文本
appendAllChildren = function(
parent: Instance,
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean,
) {
// We only have the top Fiber that was created but we need recurse down its
// children to find all the terminal nodes.
let node = workInProgress.child;
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
// 如果是dom原生節點或者是文本,直接appendChild
appendInitialChild(parent, node.stateNode);
} else if (node.tag === HostPortal) {
// If we have a portal child, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else if (node.child !== null) {
// 如果是其他節點但是有子節點,那麼轉而去遍歷它的子節點,直到找到dom原生節點或者是文本
node.child.return = node;
node = node.child;
continue;
}
if (node === workInProgress) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
// 對node 的 sibling兄弟節點進行遍歷
node.sibling.return = node.return;
node = node.sibling;
}
};
finalizeInitialChildren
主要是設置dom元素的一些初始值。在設置初始值的時候對應不同的dom元素有特殊的處理。這些處理都在setInitialProperties函數中。
export function finalizeInitialChildren(
domElement: Instance,
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
): boolean {
// 把props對應的應該在dom節點上展現的attributes,如何在掛載到Dom,還有一些事件監聽相關。
setInitialProperties(domElement, type, props, rootContainerInstance);
// 是否需要 auto focus
return shouldAutoFocusHostComponent(type, props);
}
updateHostComponent
1、調用prepareUpdate得到新老props比較後的結果
2、把結果放到workInProgress.updateQueue
3、標記當前節點的effect 爲 UPDATE
注:比較後形成的結果是這樣的:updatePayload: [k1,null,k2,v2,k3,v3]
updateHostComponent = function(
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
rootContainerInstance: Container,
) {
// If we have an alternate, that means this is an update and we need to
// schedule a side-effect to do the updates.
// 之前的oldProps
const oldProps = current.memoizedProps;
if (oldProps === newProps) {
// In mutation mode, this is sufficient for a bailout because
// we won't touch this node even if children changed.
return;
}
// If we get updated because one of our children updated, we don't
// have newProps so we'll have to reuse them.
// TODO: Split the update API as separate for the props vs. children.
// Even better would be if children weren't special cased at all tho.
// 當前節點的dom對象
const instance: Instance = workInProgress.stateNode;
const currentHostContext = getHostContext();
// TODO: Experiencing an error where oldProps is null. Suggests a host
// component is hitting the resume path. Figure out why. Possibly
// related to `hidden`.
// 得到新老props比較後的結果
const updatePayload = prepareUpdate(
instance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext,
);
// TODO: Type this specific to this type of component.
// 把結果放到workInProgress.updateQueue
workInProgress.updateQueue = (updatePayload: any);
// If the update payload indicates that there is a change or if there
// is a new ref we mark this as an update. All the work is done in commitWork.
if (updatePayload) {
// 標記當前節點的effect 爲 UPDATE
markUpdate(workInProgress);
}
};
prepareUpdate:
這個函數只是調用了diffProperties並且返回
export function prepareUpdate(
domElement: Instance,
type: string,
oldProps: Props,
newProps: Props,
rootContainerInstance: Container,
hostContext: HostContext,
): null | Array<mixed> {
// ......
return diffProperties(
domElement,
type,
oldProps,
newProps,
rootContainerInstance,
);
}
diffProperties
1、根據不同標籤節點提取新老 props 準備比較
2、第一次遍歷老 props 把要刪除的屬性都設置爲 null
3、第二次遍歷新 props , 把新的props push 到updatePayload
4、最後生成updatePayload: [k1,null,k2,v2,k3,v3]
注:這裏不同的屬性會有不同的特殊處理,比如STYLE的話,就需要展開處理等等。
// Calculate the diff between the two objects.
export function diffProperties(
domElement: Element,
tag: string,
lastRawProps: Object,
nextRawProps: Object,
rootContainerElement: Element | Document,
): null | Array<mixed> {
let updatePayload: null | Array<any> = null;
let lastProps: Object;
let nextProps: Object;
// 1、根據不同標籤節點提取新老 props 準備比較
switch (tag) {
case 'input':
lastProps = ReactDOMInput.getHostProps(domElement, lastRawProps);
nextProps = ReactDOMInput.getHostProps(domElement, nextRawProps);
updatePayload = [];
break;
case 'option':
lastProps = ReactDOMOption.getHostProps(domElement, lastRawProps);
nextProps = ReactDOMOption.getHostProps(domElement, nextRawProps);
updatePayload = [];
break;
case 'select':
lastProps = ReactDOMSelect.getHostProps(domElement, lastRawProps);
nextProps = ReactDOMSelect.getHostProps(domElement, nextRawProps);
updatePayload = [];
break;
case 'textarea':
lastProps = ReactDOMTextarea.getHostProps(domElement, lastRawProps);
nextProps = ReactDOMTextarea.getHostProps(domElement, nextRawProps);
updatePayload = [];
break;
default:
lastProps = lastRawProps;
nextProps = nextRawProps;
if (
typeof lastProps.onClick !== 'function' &&
typeof nextProps.onClick === 'function'
) {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
}
break;
}
assertValidProps(tag, nextProps);
// 2、第一次遍歷老 props 把要刪除的屬性都設置爲 null
let propKey;
let styleName;
let styleUpdates = null;
for (propKey in lastProps) {
if (
nextProps.hasOwnProperty(propKey) ||
!lastProps.hasOwnProperty(propKey) ||
lastProps[propKey] == null
) {
continue;
}
if (propKey === STYLE) {
const lastStyle = lastProps[propKey];
for (styleName in lastStyle) {
if (lastStyle.hasOwnProperty(styleName)) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = '';
}
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) {
// Noop. This is handled by the clear text mechanism.
} else if (
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING
) {
// Noop
} else if (propKey === AUTOFOCUS) {
// Noop. It doesn't work on updates anyway.
} else if (registrationNameModules.hasOwnProperty(propKey)) {
// This is a special case. If any listener updates we need to ensure
// that the "current" fiber pointer gets updated so we need a commit
// to update this element.
if (!updatePayload) {
updatePayload = [];
}
} else {
// For all other deleted properties we add it to the queue. We use
// the whitelist in the commit phase instead.
(updatePayload = updatePayload || []).push(propKey, null);
}
}
// 3、第二次遍歷新 props , 把新的props push 到updatePayload
for (propKey in nextProps) {
const nextProp = nextProps[propKey];
const lastProp = lastProps != null ? lastProps[propKey] : undefined;
if (
!nextProps.hasOwnProperty(propKey) ||
nextProp === lastProp ||
(nextProp == null && lastProp == null)
) {
continue;
}
if (propKey === STYLE) {
if (lastProp) {
// Unset styles on `lastProp` but not on `nextProp`.
for (styleName in lastProp) {
if (
lastProp.hasOwnProperty(styleName) &&
(!nextProp || !nextProp.hasOwnProperty(styleName))
) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = '';
}
}
// Update styles that changed since `lastProp`.
for (styleName in nextProp) {
if (
nextProp.hasOwnProperty(styleName) &&
lastProp[styleName] !== nextProp[styleName]
) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = nextProp[styleName];
}
}
} else {
// Relies on `updateStylesByID` not mutating `styleUpdates`.
if (!styleUpdates) {
if (!updatePayload) {
updatePayload = [];
}
updatePayload.push(propKey, styleUpdates);
}
styleUpdates = nextProp;
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
const nextHtml = nextProp ? nextProp[HTML] : undefined;
const lastHtml = lastProp ? lastProp[HTML] : undefined;
if (nextHtml != null) {
if (lastHtml !== nextHtml) {
(updatePayload = updatePayload || []).push(propKey, '' + nextHtml);
}
} else {
// TODO: It might be too late to clear this if we have children
// inserted already.
}
} else if (propKey === CHILDREN) {
if (
lastProp !== nextProp &&
(typeof nextProp === 'string' || typeof nextProp === 'number')
) {
(updatePayload = updatePayload || []).push(propKey, '' + nextProp);
}
} else if (
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING
) {
// Noop
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp != null) {
// We eagerly listen to this even though we haven't committed yet.
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
ensureListeningTo(rootContainerElement, propKey);
}
if (!updatePayload && lastProp !== nextProp) {
// This is a special case. If any listener updates we need to ensure
// that the "current" props pointer gets updated so we need a commit
// to update this element.
updatePayload = [];
}
} else {
// For any other property we always add it to the queue and then we
// filter it out using the whitelist during the commit.
(updatePayload = updatePayload || []).push(propKey, nextProp);
}
}
if (styleUpdates) {
(updatePayload = updatePayload || []).push(STYLE, styleUpdates);
}
// 4、最後生成updatePayload: [k1,null,k2,v2,k3,v3]
return updatePayload;
}
HostText
1、更新的話,調用updateHostText
2、首次渲染的話,調用createTextInstance
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.
updateHostText(current, workInProgress, oldText, newText);
} else {
// ......
// 首次渲染
workInProgress.stateNode = createTextInstance(
newText,
rootContainerInstance,
currentHostContext,
workInProgress,
);
}
break;
}
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.
if (oldText !== newText) {
markUpdate(workInProgress);
}
};
createTextInstance
這個方法也很簡單,就是創建了一個TextNode文本節點。
以及給textElement[__reactInternalInstance$] = internalInstanceHandle = 當前的fiber節點。
export function createTextInstance(
text: string,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): TextInstance {
const textNode: TextInstance = createTextNode(text, rootContainerInstance);
precacheFiberNode(internalInstanceHandle, textNode);
return textNode;
}
任世界紛繁複雜,仍舊保持可愛。
我是小柚子小仙女。文章如有不妥,歡迎指正~