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;
}
任世界纷繁复杂, 仍旧保持可爱。
我是小柚子小仙女。文章如有不妥,欢迎指正~