前言:
接上篇— —React源码解析之FunctionComponent(上)
一、reconcileSingleElement
作用:
当子节点不为 null,则复用子节点并删除其兄弟节点;
当子节点为 null,则创建新的 fiber 节点
源码:
//当子节点不为 null,则复用子节点并删除其兄弟节点;
//当子节点为 null,则创建新的 fiber 节点
function reconcileSingleElement(
returnFiber: Fiber,
//旧
currentFirstChild: Fiber | null,
//新
element: ReactElement,
expirationTime: ExpirationTime,
): Fiber {
const key = element.key;
let child = currentFirstChild;
//从当前已有的所有子节点中,找到可以复用的 fiber 对象,并删除它的 兄弟节点
while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
//key 相同的话复用节点
//ReactElement里面的key,也就是开发过程中加的 key,如<div key={'a'}></div>
//所以当有多个相同 element 放在同一组时,React 建议设置 key,方便不产生更新的节点能复用
//但是我自己试验了下,发现打印出的 ReactElement 的 key('a') 和_owner 下fiber 节点的 key(null) 是不一样的,
//而且设置 ReactElement 的 key 不影响 fiber 对象的 key 值一直为 null
//所以这边 fiber.key 和 ReactElement.key 相等的情况,大多数应为 null===null
if (child.key === key) {
//如果节点类型未改变的话
if (
child.tag === Fragment
? element.type === REACT_FRAGMENT_TYPE
: child.elementType === element.type ||
// Keep this check inline so it only runs on the false path:
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false)
) {
//复用 child,删除它的兄弟节点
//因为旧节点它有兄弟节点,新节点只有它一个
deleteRemainingChildren(returnFiber, child.sibling);
//复制 fiber 节点,并重置 index 和 sibling
//existing就是复用的节点
const existing = useFiber(
child,
element.type === REACT_FRAGMENT_TYPE
? element.props.children
: element.props,
expirationTime,
);
//设置正确的 ref
existing.ref = coerceRef(returnFiber, child, element);
//父节点
existing.return = returnFiber;
//删除了 dev 代码
return existing;
} else {
deleteRemainingChildren(returnFiber, child);
break;
}
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
//上面是子节点不为 null 的情况,能执行到这边说明之前是 null,现在要新建
//创建新的节点
if (element.type === REACT_FRAGMENT_TYPE) {
//创建Fragment类型的 fiber 节点
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
expirationTime,
element.key,
);
created.return = returnFiber;
return created;
} else {
//创建Element类型的 fiber 节点
const created = createFiberFromElement(
element,
returnFiber.mode,
expirationTime,
);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
解析:
(1) 流程图如下:
(2) 针对child.key === ReactElement.key
的情况,在开发过程中,大多数的 React 组件都是复用的,因为它们都是“列表”中的第一项,所以fiber.key(nulll)=ReactElement.key(null)
为true
(3) deleteRemainingChildren()
在本文后面有讲到
(3) 执行useFiber()
,通过复制 fiber 节点达到「复用」fiber 节点的目的,里面用到了double-buffer
的技巧,本文后面会讲到
(4) 不能根据旧节点复用成新节点的话,则通过createFiberFromElement()
创建Fragment
或Element
类型的 fiber 节点
(5) 最后,返回处理过的节点
二、useFiber
作用:
复制 fiber 节点,并重置 index 和 sibling
源码:
//复制 fiber 节点,并重置 index 和 sibling
function useFiber(
fiber: Fiber,
pendingProps: mixed,
expirationTime: ExpirationTime,
): Fiber {
// We currently set sibling to null and index to 0 here because it is easy
// to forget to do before returning it. E.g. for the single child case.
//为防止忘记,提前将 index 置为 0,兄弟节点置为 null
//通过 doubleBuffer 重用未更新的 fiber 对象
const clone = createWorkInProgress(fiber, pendingProps, expirationTime);
clone.index = 0;
clone.sibling = null;
return clone;
}
解析:
执行createWorkInProgress()
,通过 doubleBuffer 重用未更新的 fiber 对象,并将返回的 fiber 节点的 index、sibling 置为初始状态
三、createWorkInProgress
作用:
通过 doubleBuffer 重用未更新的 fiber 对象
源码:
// This is used to create an alternate fiber to do work on.
//通过 doubleBuffer 重用未更新的 fiber 对象
export function createWorkInProgress(
current: Fiber,
pendingProps: any,
expirationTime: ExpirationTime,
): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
// We use a double buffering pooling technique because we know that we'll
// only ever need at most two versions of a tree. We pool the "other" unused
// node that we're free to reuse. This is lazily created to avoid allocating
// extra objects for things that are never updated. It also allow us to
// reclaim the extra memory if needed.
//因为一棵 fiber 树顶多有两个版本,所以当某一 fiber 节点不更新时,在更新 fiber 树的时候,
//不会去重新创建跟之前一样的 fiber 节点,而是从另一个版本的 fiber 树上重用它
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
//删除了 dev 代码
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
workInProgress.pendingProps = pendingProps;
// We already have an alternate.
// Reset the effect tag.
workInProgress.effectTag = NoEffect;
// The effect list is no longer valid.
workInProgress.nextEffect = null;
workInProgress.firstEffect = null;
workInProgress.lastEffect = null;
if (enableProfilerTimer) {
// We intentionally reset, rather than copy, actualDuration & actualStartTime.
// This prevents time from endlessly accumulating in new commits.
// This has the downside of resetting values for different priority renders,
// But works for yielding (the common case) and should support resuming.
workInProgress.actualDuration = 0;
workInProgress.actualStartTime = -1;
}
}
workInProgress.childExpirationTime = current.childExpirationTime;
workInProgress.expirationTime = current.expirationTime;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
// Clone the dependencies object. This is mutated during the render phase, so
// it cannot be shared with the current fiber.
const currentDependencies = current.dependencies;
workInProgress.dependencies =
currentDependencies === null
? null
: {
expirationTime: currentDependencies.expirationTime,
firstContext: currentDependencies.firstContext,
events: currentDependencies.events,
};
// These will be overridden during the parent's reconciliation
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
if (enableProfilerTimer) {
workInProgress.selfBaseDuration = current.selfBaseDuration;
workInProgress.treeBaseDuration = current.treeBaseDuration;
}
//删除了 dev 代码
return workInProgress;
}
解析:
React 会准备 fiber 树的两个版本(新版本和旧版本),当新版本的某一新节点在旧版本上有时,可以复用旧 fiber 的属性,而不是重新创建新的节点。
新旧 fiber 树相互复用的思路来源于doubleBuffer
。
四、reconcileSingleTextNode
作用:
复用或创建文本节点
源码:
//复用或创建文本节点
function reconcileSingleTextNode(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
textContent: string,
expirationTime: ExpirationTime,
): Fiber {
// There's no need to check for keys on text nodes since we don't have a
// way to define them.
//判断第一个节点(优化)
if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
// We already have an existing node so let's just update it and delete
// the rest.
//删掉旧节点
//returnFiber是当前正在更新的节点
//currentFirstChild是第一个子节点
deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
//复用
const existing = useFiber(currentFirstChild, textContent, expirationTime);
//指定父节点
existing.return = returnFiber;
return existing;
}
// The existing first child is not a text node so we need to create one
// and delete the existing ones.
//如果第一个节点不是文本节点的话,删除所有
deleteRemainingChildren(returnFiber, currentFirstChild);
//创建新的文本节点
const created = createFiberFromText(
textContent,
returnFiber.mode,
expirationTime,
);
created.return = returnFiber;
return created;
}
解析:
仍有则复用,没有则执行createFiberFromText()
来新建
五、createFiberFromText
作用:
创建文本类型的 fiber
源码:
export function createFiberFromText(
content: string,
mode: TypeOfMode,
expirationTime: ExpirationTime,
): Fiber {
const fiber = createFiber(HostText, content, null, mode);
fiber.expirationTime = expirationTime;
return fiber;
}
解析:
content 就是要更新的文本
createFiber()
里面逻辑不复杂,就不解析了,放下源码:
//pendingProps就是 props.children
const createFiber = function(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
): Fiber {
// $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
return new FiberNode(tag, pendingProps, key, mode);
};
FiberNode()
的源码:
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// Effects
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
this.expirationTime = NoWork;
this.childExpirationTime = NoWork;
this.alternate = null;
if (enableProfilerTimer) {
// Note: The following is done to avoid a v8 performance cliff.
//
// Initializing the fields below to smis and later updating them with
// double values will cause Fibers to end up having separate shapes.
// This behavior/bug has something to do with Object.preventExtension().
// Fortunately this only impacts DEV builds.
// Unfortunately it makes React unusably slow for some applications.
// To work around this, initialize the fields below with doubles.
//
// Learn more about this here:
// https://github.com/facebook/react/issues/14365
// https://bugs.chromium.org/p/v8/issues/detail?id=8538
this.actualDuration = Number.NaN;
this.actualStartTime = Number.NaN;
this.selfBaseDuration = Number.NaN;
this.treeBaseDuration = Number.NaN;
// It's okay to replace the initial doubles with smis after initialization.
// This won't trigger the performance cliff mentioned above,
// and it simplifies other profiler code (including DevTools).
this.actualDuration = 0;
this.actualStartTime = -1;
this.selfBaseDuration = 0;
this.treeBaseDuration = 0;
}
//删除了 dev 代码
}
六、deleteRemainingChildren
作用:
如果旧节点存在,但是更新的节点是 null 的话,需要删除旧节点的内容
源码:
//删除旧节点
function deleteRemainingChildren(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
): null {
//第一次渲染的情况
//是没有子节点的,所以直接删除
if (!shouldTrackSideEffects) {
// Noop.
return null;
}
// TODO: For the shouldClone case, this could be micro-optimized a bit by
// assuming that after the first child we've already added everything.
//从当前节点的第一个子节点开始,进行删除操作
let childToDelete = currentFirstChild;
//删除目标节点的所有子节点,并循环寻找兄弟节点,删除它们的子节点
while (childToDelete !== null) {
deleteChild(returnFiber, childToDelete);
childToDelete = childToDelete.sibling;
}
return null;
}
解析:
注意最后 return 的是 null
七、deleteChild
作用:
为要删除的子节点们做Deletion标记
源码:
//为要删除的子节点们做Deletion标记
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
if (!shouldTrackSideEffects) {
// Noop.
return;
}
// Deletions are added in reversed order so we add it to the front.
// At this point, the return fiber's effect list is empty except for
// deletions, so we can just append the deletion to the list. The remaining
// effects aren't added until the complete phase. Once we implement
// resuming, this may not be true.
//fiber 链表的 effect 除了是deletions外,都是空的,可以根据这个进行删除
//标记副作用,以便在 commit 阶段进行删除
const last = returnFiber.lastEffect;
//做标记
if (last !== null) {
//要删除的节点(fiber)
last.nextEffect = childToDelete;
returnFiber.lastEffect = childToDelete;
} else {
returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
}
childToDelete.nextEffect = null;
//这里并未执行删除操作,而仅仅是给effectTag赋值了Deletion
//因为这里仍是对 fiber 树的更新,未涉及到真正的 DOM 节点
//真正的删除留到 commit 阶段
childToDelete.effectTag = Deletion;
}
解析:
在 fiber 树上,循环每一个子节点,并做上 Deletion 标记,以便在commit 阶段进行真删除
本文主要讲了reconcileSingleElement()
、reconcileSingleTextNode()
和deleteRemainingChildren()
的方法,下篇文章会继续讲FunctionComponent
中的数组节点的更新:
//数组节点
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
expirationTime,
);
}
GitHub:
ReactChildFiber.js
(完)
网友评论