Appearance
completeUnitOfWork
这个阶段的主要特点是:
- 产生 stateNode (当前节点对应的DOM,此时只是保存在内存中)
- 标记
flags
和 subtreeFlags (在Commit阶段作为判断当前分支是否有DOM的操作)
栗子
仍然是beginWork的那个栗子
初次渲染
在初次渲染的时候,我们根据beginWork按照深度遍历优先的流程知道其当performUnitOfWork执行到 第一个没有child的节点(this is Comp21的Text节点),因为next为null进行第一个 completeWork的流程
代码如下
js
function completeUnitOfWork(unitOfWork: Fiber): void {
// 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.
let completedWork = unitOfWork;
do {
// 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.
// 当前Node的原FiberNode实例对象
const current = completedWork.alternate;
// 父节点
const returnFiber = completedWork.return;
// Check if the work completed or if something threw.
if ((completedWork.flags & Incomplete) === NoFlags) {
let next;
if (
!enableProfilerTimer || (completedWork.mode & ProfileMode) === NoMode
) {
next = completeWork(current, completedWork, subtreeRenderLanes);
} else {
next = completeWork(current, completedWork, subtreeRenderLanes);
}
if (next !== null) {
// Completing this fiber spawned new work. Work on that next.
workInProgress = next;
return;
}
}
// 如果存在兄弟节点 那么将兄弟节点赋给workInProgress 进入到 兄弟节点的 workLoop 的流程
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
workInProgress = siblingFiber;
return;
}
// Otherwise, return to the parent
// 如果 不存在兄弟节点 那么通过 do{} 循环执行 父节点的 completeWork流程
// 此时 completedWork指向了父节点,那么在父节点完成 completeWork流程 时也会判断其是否存在兄弟节点,这样形成一个深度优先的链表 遍历流程
completedWork = returnFiber;
// Update the next thing we're working on in case something throws.
workInProgress = completedWork;
} while (completedWork !== null);
// We've reached the root.
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootCompleted;
}
}
completeUnitOfWork(unitOfWork: Fiber)
不考虑completeWork的情况,可以看出来 completeUnitOfWork的主要作用是 配合 beginWork 完成
- 有兄弟节点的情况下,将workInProgress 指向兄弟节点
- 没有兄弟节点的情况下,执行父节点的 completeWork
beginWork(current, completedWork, subtreeRenderLanes)
bubbleProperties()
在React17的时候 我们发现多了一个 bubbleProperties()
方法,其作用是通过subtreeFlags
替代之前的 finishWork.firstEffect
副作用链表,来标记当前子树中存在副作用,从而避免在 commit的时候再次深度遍历寻找
在FiberNode树上通过FiberNode.subtreeFlags , FiberNode.flags 标记节点和子孙节点 DOM操作 类型
- FiberNode.flags 标记当前节点DOM的操作 更新、创建、删除等
- FiberNode.subtreeFlags 通过位运算标记当前节点树下子孙包含的操作类型
注意: 其跟 FiberNode.lanes 和 FiberNode.childLanes 很像
更新 FiberNode.childLanes
合并当前节点的 lanes 和 childLanes (completedWork.childLanes = mergeLanes(newChildLanes, mergeLanes(child.lanes, child.childLanes)))
在performUnitOfWork的时候,Update的FiberNode标记的是lanes ,祖先节点 标记点childLanes。
那么这时候通过lanes 和 childLanes的合并,从而更好的根据优先级找到更新的节点树分支
js
/**
* 1. 在FiberNode树上通过FiberNode.subtreeFlags , FiberNode.flags 标记节点和子孙节点 DOM操作 类型
* - FiberNode.flags 标记当前节点DOM的操作 更新、创建、删除等
* - FiberNode.subtreeFlags 通过位运算标记当前节点树下子孙包含的操作类型
* 注意: 其跟 FiberNode.lanes 和 FiberNode.childLanes 很像
* 2. 更新 FiberNode.childLanes
* 合并当前节点的 lanes 和 childLanes (completedWork.childLanes = mergeLanes(newChildLanes, mergeLanes(child.lanes, child.childLanes)))
* 在performUnitOfWork的时候,Update的FiberNode标记的是lanes ,祖先节点 标记点childLanes。
* 那么这时候通过lanes 和 childLanes的合并,从而更好的根据优先级找到更新的节点树分支
* @param {*} completedWork
* @returns
*/
function bubbleProperties(completedWork: Fiber) {
// 是否能够复用
const didBailout =
completedWork.alternate !== null &&
completedWork.alternate.child === completedWork.child;
let newChildLanes = NoLanes;
let subtreeFlags = NoFlags;
if (!didBailout) {
// Bubble up the earliest expiration time.
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
} else {
let child = completedWork.child;
while (child !== null) {
// 合并当前节点的 lanes 和 childLanes
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);
// 跟 FiberNode.lanes 和 FiberNode.childLanes 很像
// 通过位运算标记当前节点树下子孙包含的操作类型
subtreeFlags |= child.subtreeFlags;
subtreeFlags |= child.flags;
child = child.sibling;
}
}
// 通过位运算标记当前节点树下子孙包含的操作类型
completedWork.subtreeFlags |= subtreeFlags;
}
completedWork.childLanes = newChildLanes;
return didBailout;
}
createInstance()
根据FiberNode生成新的DOM元素,并在DOM上缓存 FiberNode 和 Props信息。
其还有一个差不多的 createTextInstance。差不多的意思。
主要作用就是:
根据环境的不同通过 rootDom.ownDocument去获取创建各种DOM的方法,并生成对应的DOM节点。(此时没有生成DOM节点的childens)
在DOM元素上缓存了两个属性
'__reactFiber$' + randomKey
缓存FiberNode 对象'__reactProps$' + randomKey
缓存 Props 内容
js
/**
* 根据FiberNode生成新的DOM元素,并在DOM上缓存 FiberNode 和 Props信息
* @param {*} type DOM元素的类型
* @param {*} props props
* @param {*} rootContainerInstance FiberRootNode的current 根DOM
* @param {*} hostContext 跟rootContainerInstance获取上下文环境
* @param {*} internalInstanceHandle FiberNode对象
* @returns
*/
export function createInstance(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): Instance {
let parentNamespace: string;
if (__DEV__) {
// TODO: take namespace into account when validating.
const hostContextDev = ((hostContext: any): HostContextDev);
validateDOMNesting(type, null, hostContextDev.ancestorInfo);
if (
typeof props.children === 'string' ||
typeof props.children === 'number'
) {
const string = '' + props.children;
const ownAncestorInfo = updatedAncestorInfo(
hostContextDev.ancestorInfo,
type,
);
validateDOMNesting(null, string, ownAncestorInfo);
}
parentNamespace = hostContextDev.namespace;
} else {
// 获取 parent的 namespace 命名空间
parentNamespace = ((hostContext: any): HostContextProd);
}
// 创建 DOM 元素
const domElement: Instance = createElement(
type,
props,
rootContainerInstance,
parentNamespace,
);
// 在 DOM 元素上缓存 FiberNode 对象
precacheFiberNode(internalInstanceHandle, domElement);
// 在 DOM 元素上缓存 Props 对象
updateFiberProps(domElement, props);
return domElement;
}
appendAllChildren(instance, workInProgress, false, false);
在通过createInstance、createTextInstance等创建好当前FiberNode对应的DOM元素的时候,然后通过 appendAllChildren将其子节点DOM元素插入到当前DOM元素上,然后在全部插入完毕后,将整个DOM元素保存到 workInProgress.stateNode上。
下面看源码
finalizeInitialChildren(instance,type,newProps,rootContainerInstance,currentHostContext)
在上面通过 appendAllChildren 处理完DOM元素的子元素后,现在通过finalizeInitialChildren将FiberNode上的props赋值到DOM元素 attributes上。
下面看源码
js
/**
* 1. 将 Prop的内容更新到 DOM元素 上
* 2. 对于 autoFocus 其需要标记 flags 为 Update ,在commit单独处理
*
* @param {*} domElement
* @param {*} type
* @param {*} props
* @param {*} rootContainerInstance
* @param {*} hostContext
* @returns
*/
export function finalizeInitialChildren(
domElement: Instance,
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
): boolean {
// 将 Prop的 内容更新到 DOM元素 上
setInitialProperties(domElement, type, props, rootContainerInstance);
// 是否有 autoFocus 属性 需要单独处理
// 因为 autoFocus 不像其他的事件不会产生副作用
// 对于 autoFocus 其需要标记 flags 为 Update ,在commit 进行处理
return shouldAutoFocusHostComponent(type, props);
}
其本身还是很简单的,主要是进行了两个小步骤
将 Prop的 内容更新到 DOM元素 上(style、children)
对于 autoFocus 其需要标记 flags 为 Update ,在commit单独处理
js/** * autoFocus 属性会 产生副作用 * 当返回true的时候 会标记 flags为Update,在Commit阶段作为有更新的DOM元素处理 * @param {*} type * @param {*} props * @returns */ function shouldAutoFocusHostComponent(type: string, props: Props): boolean { switch (type) { case 'button': case 'input': case 'select': case 'textarea': return !!props.autoFocus; } return false; }
下面主要看一下
setInitialProperties(domElement, type, props, rootContainerInstance);
其主要功能是处理DOM元素的属性,并按照类型分别处理
style 属性
setValueForStyles(domElement, nextProp); 进行处理
- 空值进行过滤 "" null boolean
- 单位类型的值 数字进行添加 px 单位
- float 转换为 cssFloat
dangerouslySetInnerHTML
children 属性
string 类型
这里特别处理了
<textarea>这是内容 </textarea> ,为什么?
https://github.com/facebook/react/issues/6731#issuecomment-254874553
数字类型
转换成字符串直接赋值
autoFocus
和其他事件不一起处理,而是单独拿出来,为什么?
因为autoFocus这个属性会参数副作用,需要在渲染完成后聚焦
其他 初始化注册的 事件 onClick
详情看 合成事件
其他
js
/**
* 减Props的内容更新到 DOM元素 上
* 此处主要处理以下:
* - style 属性
* - dangerouslySetInnerHTML
* - children 属性
* - autoFocus
* - 其他 初始化注册的 事件 onClick -- 通过 ensureListeningTo 绑定事件
* @param {*} tag
* @param {*} domElement
* @param {*} rootContainerElement
* @param {*} nextProps
* @param {*} isCustomComponentTag
*/
function setInitialDOMProperties(
tag: string,
domElement: Element,
rootContainerElement: Element | Document,
nextProps: Object,
isCustomComponentTag: boolean,
): void {
for (const propKey in nextProps) {
// 排除原型上的属性
if (!nextProps.hasOwnProperty(propKey)) {
continue;
}
const nextProp = nextProps[propKey];
// style 属性的更新 <div style={{ fontSize : "20px" }}></div>
if (propKey === STYLE) {
// Relies on `updateStylesByID` not mutating `styleUpdates`.
setValueForStyles(domElement, nextProp);
// dangerouslySetInnerHTML 属性
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
const nextHtml = nextProp ? nextProp[HTML] : undefined;
if (nextHtml != null) {
setInnerHTML(domElement, nextHtml);
}
// children
} else if (propKey === CHILDREN) {
if (typeof nextProp === 'string') {
// Avoid setting initial textContent when the text is empty. In IE11 setting
// textContent on a <textarea> will cause the placeholder to not
// show within the <textarea> until it has been focused and blurred again.
// https://github.com/facebook/react/issues/6731#issuecomment-254874553
const canSetTextContent = tag !== 'textarea' || nextProp !== '';
if (canSetTextContent) {
setTextContent(domElement, nextProp);
}
} else if (typeof nextProp === 'number') {
setTextContent(domElement, '' + nextProp);
}
} else if (
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING
) {
// Noop
// autoFocus 属性
} else if (propKey === AUTOFOCUS) {
// We polyfill it separately on the client during commit.
// We could have excluded it in the property list instead of
// adding a special case here, but then it wouldn't be emitted
// on server rendering (but we *do* want to emit it in SSR).
// 事件类型的属性 处理
} else if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (nextProp != null) {
if (!enableEagerRootListeners) {
ensureListeningTo(rootContainerElement, propKey, domElement);
} else if (propKey === 'onScroll') {
listenToNonDelegatedEvent('scroll', domElement);
}
}
} else if (nextProp != null) {
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
}
}
}
下面我们开始具体将每一种组件类型的处理过程
HostComponent
js
case HostComponent: {
popHostContext(workInProgress);
// 获取 Root 的 Container (div#root)
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
// current 和 stateNode 都不为空 说明是更新流程
if (current !== null && workInProgress.stateNode != null) {
// 更新 DOM
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
// 标记是否存在 ref 属性的 更新
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
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 whether we want to add them top->down or
// bottom->up. Top->down is faster in IE11.
// 服务端渲染有关
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// 服务端渲染
} else {
// 生成 DOM 元素
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 将子元素的DOM添加到当前DOM元素下
appendAllChildren(instance, workInProgress, false, false);
// 缓存DOM元素
workInProgress.stateNode = instance;
// 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.
// 处理DOM元素的属性,如果遇到autoFocus 并标记副作用
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
// 标记 flags Update
markUpdate(workInProgress);
}
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
// 标记 flags Ref
markRef(workInProgress);
}
}
return null;
}
分为更新流程 和 创建流程(SSR)
更新流程(current !== null && workInProgress.stateNode != null )
updateHostComponent
其核心是一个
prepareUpdate(instance,type ,oldProps, newProps, rootContainerInstance , currentHostContext ),并将需要更新的信息存在 workInProgress.updateQueue中
markUpdate(workInProgress); 标记有更新
ref 更新的标识 markRef(workInProgress);
HostText
文本类型的FiberNode 的 completeWork
js
case HostText: {
const 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 {
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.
}
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext();
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
if (prepareToHydrateHostTextInstance(workInProgress)) {
markUpdate(workInProgress);
}
} else {
workInProgress.stateNode = createTextInstance(
newText,
rootContainerInstance,
currentHostContext,
workInProgress,
);
}
}
bubbleProperties(workInProgress);
return null;
}
副作用标记和副作用链
React17
在旧的任务调度中,如果在 completeWork阶段发生DOM的操作(更新、添加、删除等),都会在FiberNode.flags上标记。
同时触发
js
/**
* 在根节点上按照深度优先的规则形成一个
* firstEffect指向第一个有副作用的DOM
* lastEffect指向最后一个有副作用的DOM
* 中间更新的DOM 通过 firstEffect.nextEffect.xxxxxx 形成一个单链
* 因为 CompleteWork是一个由下而上的过程,所以通过
* 将当前节点completedWork.firstEffect保存到父节点的 lastEffect.nextEffect(单链)。
* 然后一层层往上传播,直到 returnFiber !== null 即遇到根节点截止。
* 这样就可以在根节点上收集到所有的产生副作用的节点 FiberNode
*
*/
if (
returnFiber !== null &&
// Do not append effects to parents if a sibling failed to complete
(returnFiber.flags & Incomplete) === NoFlags
) {
// 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.
// 如果父节点的 firstEffect 为null 所以暂时没有,所以当前作为第一个
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
// 如果当前节点的lastEffect有值
// 1. 如果父节点的 lastEffect 有值,那么就将当前Node变成 lastEffect.nextEffect 父节点的 lastEffect变成本身
// 2. 如果没有值,那么直接将父节点的lastEffect 变成本身
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.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 flags = completedWork.flags;
// Skip both NoWork and PerformedWork tags when creating the effect
// list. PerformedWork effect is read by React DevTools but shouldn't be
// committed.
// 如果当前节点产生DOM的操作
if (flags > PerformedWork) {
// 将当前节点加入到父节点.firstEffect
// 1. 如果当前节点同一层级的前面节点没有副作用,那么这时候 父节点.firstEffect == 父节点.lastEffect == null
// 将当前节点加入到 父节点.firstEffect == 父节点.lastEffect == completedWork
// 2. 如果当前节点同一层级的前面节点也有副作用,那么其不修改父节点..lastEffect.nextEffect = completedWork ; 父节点.lastEffect = completedWork;
// 这样父节点的 fistEffect.nextEffect.nextEffect === lastEffect
//
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
这样在触发点击事件的时候在跟FiberNode.alternate上就可以看到一个
React18
在新版的任务调度中 对于副作用的标记主要通过 flags
和 subtreeFlags
去标记副作用节点分支,舍弃了之前的根节点通过副作用firstEffect、lastEffect、nextEffect
形成的单链结构,去找到当前需要更新的DOM元素的FiberNode节点。
而是使用 flags
和 subtreeFlags
位运算的操作去深度遍历标记