目录
2021SC@SDUSC
插入阶段
通过commitMutationEffectsOnFiber调用commitPlacement插入节点(称为子节点或目标节点),首先通过getHostParentFiber得到其DOM上的父节点,然后根据父节点的类型判断子节点所要插入的位置,此时可以重设子节点文字内容。找到before后可以执行插入方法。before节点是为了在插入节点时Fiber树的顺序不会因为映射到DOM上而改变,子节点必须插入在before节点的前面,如图所示
通过getHostSibling找到before节点,before节点与目标节点的相对位置可能有多重情况,左图只是情况的一种而已,这里给出react源码中的解决方法。
function getHostSibling(fiber: Fiber): ?Instance {
// We're going to search forward into the tree until we find a sibling host
// node. Unfortunately, if multiple insertions are done in a row we have to
// search past them. This leads to exponential search for the next sibling.
// TODO: Find a more efficient way to do this.
let node: Fiber = fiber;
siblings: while (true) {
// If we didn't find anything, let's try the next sibling.
while (node.sibling === null) {
if (node.return === null || isHostParent(node.return)) {
// If we pop out of the root or hit the parent the fiber we are the
// last sibling.
return null;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
while (
node.tag !== HostComponent &&
node.tag !== HostText &&
node.tag !== DehydratedFragment
) {
// If it is not host node and, we might have a host node inside it.
// Try to search down until we find one.
if (node.flags & Placement) {
// If we don't have a child, try the siblings instead.
continue siblings;
}
// If we don't have a child, try the siblings instead.
// We also skip portals because they are not part of this host tree.
if (node.child === null || node.tag === HostPortal) {
continue siblings;
} else {
node.child.return = node;
node = node.child;
}
}
// Check if this host node is stable or about to be placed.
if (!(node.flags & Placement)) {
// Found it!
return node.stateNode;
}
}
}
首先查看目标节点的兄弟节点是否为null,假如为null那么继续判断父节点是否为DOM节点(parent),是就直接插入(返回null值),否则继续向上寻找,当兄弟节点不为null时,判断是否为DOM节点,如果不是DOM节点,就判断是否为需要插入的节点,是的话直接continue继续寻找,不是的话寻找其子节点,没有子节点同样continue,有的话按照子节点遍历。如果兄弟节点是DOM节点,那就查看是否为需要插入的节点,是的话继续循环,不是的话就找到了before节点。另外,当要插入的节点并非DOM节点时,就需要寻找它所有的DOM子节点,注意,这里只插入所有分支的第一层子节点即可,因为这是自底向上的插入方式,底部DOM节点已经插入过其父节点。最终通过parent以及before节点完成了DOM节点的插入。
更新阶段
现在说一下DOM树的更新,通过commitMutationEffectsOnFiber调用commitWork,这其中文本只需要更新内容即可,而原生标签组件可以通过updateQueue来创建updatePayload并且通过commitUpdate以提交更新任务。
下面来详细看一下
case HostComponent: {
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
// Commit the work prepared earlier.
const newProps = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldProps = current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
// TODO: Type the updateQueue to be specific to host components.
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
finishedWork.updateQueue = null;
if (updatePayload !== null) {
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
}
}
return;
}
case HostText: {
invariant(
finishedWork.stateNode !== null,
'This should have a text node initialized. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
const textInstance: TextInstance = finishedWork.stateNode;
const newText: string = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldText: string =
current !== null ? current.memoizedProps : newText;
commitTextUpdate(textInstance, oldText, newText);
return;
}
在HostComponent的情况下,首先取出内部实例作为DOM节点,然后从中取出参数,接着从以前的current节点中取出旧参数,接着获取任务节点内部的更新队列,然后执行commitUpdate方法。在commitUpdate中的updateProperties方法会对DOM节点的属性进行更新,源码如下:
function updateDOMProperties(
domElement,
updatePayload,
wasCustomComponentTag,
isCustomComponentTag
) {
// TODO: Handle wasCustomComponentTag
for (var i = 0; i < updatePayload.length; i += 2) {
var propKey = updatePayload[i];
var propValue = updatePayload[i + 1];
if (propKey === STYLE) {
setValueForStyles(domElement, propValue);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
setInnerHTML(domElement, propValue);
} else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
} else {
setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
}
}
}
在这其中,updatePayload会被以[k1,v1,k2,v2]的形式循环取出并且分为STYLE,INNERHTML,TEXTCONTENT以及其他属性进行更新。
至于对HostText的更新,在这种情况下会直接调用commitTextUpdate方法
function commitTextUpdate(textInstance, oldText, newText) {
textInstance.nodeValue = newText;
}
这种情况下会直接将实例的nodeValue赋值为newText。
总结
大致上介绍了react中DOM节点的插入(重点在before节点的寻找)以及更新的内部流程,分析了几个更新的具体例子。