前面两节我们主要讲解了Fiber树的生成过程;这个结构是我们理解后续功能的基础;
本节主要阐述react中更新产生的变化提交到宿主环境,我们使用浏览器环境;
本节是仿写的提交逻辑,真实可能跟源代码有出入,只是讲述思路;
下面分几种情况来看;
exmaple1: 新增节点
const App = () => {
return (
<div>
<h1>title</h1>
<section>
<p>content: xxxx</p>
</section>
</div>
);
};
createRoot(root).render(<App/>);
针对这种情况,我们先看下fiber树的结构
图1
关注下dom生成的顺序,beginWork先到底的是title文本节点,然后进入completeWork,title没有sibling,会向上走到达h1, 对h1执行completeWork, 然后 h1 有sibling,进入Section的beginWork; 有疑惑可以参考# React :从 jsx 到 fiber 树;
每次执行completeWork,对于HostComponent,HostText会根据条件创建dom, 对于HostComponent还会挂载该dom的子节点;因为挂载的时候需要子节点要准备好,所以dom创建的顺序为什么是这种也就不难理解了;
为什么说要根据条件创建dom呢?因为有的时候我们只是改变了界面某个dom,那么其余的dom是不需要创建的;判断条件简化版
const current = wipFiber.alternate;
if (current !== null && wipFiber.stateNode) {
// update
} else {
const instance = document.createElement(wipFiber.type);
appendAllChildren(instance, wipFiber);
}
appendAllChildren就是将子节点挂载到instance上,因为子节点有可能不是原生DOM节点,所以需要一个向下寻找的过程;
可以结合图和代码看,例如对图中顶层Div进行appendAllChild:
const appendAllChildren = (parentNode, wipFiber) => {
let node = wipFiber.child;
while (node !== null) {
if (node.tag === HostCompoent || node.tag === HostText) {
appendChild(parentNode, node.stateNode);
} else {
node.child.return = node;
node = node.child;
continue;
}
if (node === wipFiber) { // 正常情况不会到这里,属于防御编程
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === wipFiber) {
return;
}
node = node.return;
}
// 继续对sibling进行操作
node.sibling.return = node.return;
node = node.sibling;
}
}
为什么在到达HostComponent节点或者HostText节点后要对sibling进行操作呢?因为有两种情况,
- 这个节点的父节点就是parentNode, 这个肯定也要挂载它的sibling节点;
- 这个节点的父节点是非原生node 类型的fiber节点,那么它的sibling找到的原生fiber也是parentNode的子节点;例如图中四个div在dom树中是同一级
stateNode的生成可以参考# React :从 jsx 到 fiber 树;
回到图1,只有App标记了Placement,这是因为我们在mount时做了优化,这样先构造一颗离屏Dom,然后一次插入就可,避免频繁插入导致的浏览器频繁渲染;
那么我们是如何只给它添加Placement呢?这个其实看图1就可以清晰了解,因为只有HostRootFiber此时有alternate,所以生成它的child的时候,只跟踪有alternate的副作用,即指标记App的flags为Placement即可;
fiber上有一个flags属性(用来记录节点执行的操作),一个subFlags属性(代表该节点fiber子树是否有要执行的操作),subFlags是completeWork的时候对子树进行一次遍历收集起来的;
执行完了beginWork和completeWork, 就要将变化提交到dom了;也就是commitRoot; commitRoot从HostRootFiber开始, DFS遍历fiber树,遍历到subtreeFlags为空的节点后停止,然后对该节点的sibling进行遍历;流程如下,记作流程2
let currentEffect = HostRootFiber;
while (currentEffect !== null) {
if ((currentEffect.subtreeFlags & MutationMask) !== NoFlags && currentEffect.child) {
// 如果当前节点的subtreeFlags 不为空 并且 当前节点有child
currentEffect = currentEffect.child
} else {
up: while (currentEffect !== null) {
commitMutaionOnFiber(currentEffect);
const sibling = currentEffect.sibling;
if (sibling !== null) {
currentEffect = sibling;
break up;
}
currentEffect = currentEffect.return;
}
}
}
commitMutationOnFiber 按照 flags 分别执行 commitPlacement, commitUpdate, commitDeletion;
const commitMutaionOnFiber = (fiber) => {
if ((fiber.flags & Placement) !== NoFlags) {
commitPlacement(fiber);
fiber &= ~Placement;
}
if ((fiber.flags & Update) !== NoFlags) {
commitUpdate(fiber);
fiber &= ~Update;
}
if ((flags & ChildDeletion) !== NoFlags) {
const deletions = fiber.deletions;
if (deletions !== null) {
deletiions.forEach(commitDeletion);
}
fiber.flags ~= ChildDeletion;
}
}
对于example1, 流程2只会遍历到HostRootFiber和App, 对App执行commitPlacement, 根据结构,我们要做
- 找到App fiber 所属的dom父节点parent
- 找到App 子树的根dom节点subRoot
- 将subRoot挂载到parent上
逻辑乍看是没有什么问题,但是如果App返回了用<></>包裹的多个节点,如下图,那么这个逻辑就有问题了,我们需要将这三个都挂载到#root上才对;
所以后面两步我们要修改下,如果fiber是HostComponent或者HostText那么就直接挂载,否则需要将子节点和子节点的sibling的子subRoot dom挂载上;
- 找到App fiber 所属的dom父节点parentNode
- 收集subRoot
- 收集到的subRoo依次挂载到parenNodet上
寻找parent节点
const getHostParent(fiber) {
let node = fiber.return;
while (node !== null) {
switch (node.tag) {
case HostComponent:
return node.stateNode;
case HostRoot:
return node.stateNode.container;
}
node = node.return;
}
return null;
}
寻找fiber子树的根dom节点:
const getSubRoots = (fiber) => {
let collects = [];
if (fiber.tag === HostComponent || fiber.tag === HostText) {
collects = [fiber.stateNode];
return collects;
}
let node = fiber.child;
while (node !== null) {
collects = collects.concat(getSubRoots(node));
node = node.sibling;
}
return collects;
}
挂载:
collects.forEach((child) => {
parentNode.appendChild(child);
});
这样执行完commitPlacement,我们就可以在dom树上显示了;(本步骤 parentNode(div#root), collects([<div><h1>...</section></div>]))
example2: 删除节点
观察上面三个图,我们大概能总结出删除节点的步骤;
- 获取需要删除节点所属的dom父节点parentNode;
- 收集要删除的节点(当前是非HostComponent和非HostText,就要收集子节点的所有sibling dom);
- 执行removeChild;
步骤跟新增挂载节点相同,但是忽略了一点,就是删除的时候我们要执行被删除的子树上组件的卸载流程; 所以我们要遍历子树;遍历的时候收集;
// 遍历流程
const traveral = (delFiber, operateCallback) => {
const node = delFiber;
do {
operateCallback(node);
if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
while (node.sibling === null) {
if (node.return === delFiber || node.return === null) {
return;
}
node = node.return;
}
node.sibling.return = node;
node = node.sibling;
} while (node !== delFiber);
}
const collects = [];
traveral(delFiber, (unmountFiber) => {
switch (unmountFiber.tag) {
case HostComponent:
// 解绑ref
recordHostChildToDelete(collects, unmountFiber, delFiber);
return;
case HostText:
recordHostChildToDelete(collects, umountFiber, delFiber);
return;
case FunctionComponent:
// useEffect umount ref解绑
return;
...
}
});
function recordHostChildToDelete (collects, unmountFiber, delFiber) {
if (delFiber === unmountFiber) {
collects = [unmountFiber];
return;
}
// 只有子树顶层的节点收集
if (collects.length === 0) {
collects = findDomSameLevel(unmountFiber);
}
}
function findDomSameLevel (fiber) {
const innerCollect = [];
let node = fiber;
while (true) {
if (node.tag === HostComponent || node.tag === HostText) {
innerCollect.push(node);
node = node.sibling;
continue;
}
if (node === null) {
break;
}
if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
while (node.sibling === null) {
if (node.return === null || node.return === fiber) {
break;
}
node = node.return;
}
node = node.sibling;
}
return innerCollect;
}
依次执行removeChild;
collects.forEach((deleteNode) => {
parent.removeChild(deleteNode);
});
example3: 节点移动
观察可能的情况,我们大概能总结出移动节点的步骤;
- 找到移动节点的有dom的fiber节点,获取parentNode;
- 找到移动节点插入的标志dom, 也就是insertBefore的第二个参数,没有的话我们就用appendChild代替(证明是最后一个节点)
- 执行parent.insertBefore / parent.appendChild;
只列出寻找sibling节点的方法
const getHostSibling = (fiber) => {
let node = fiber;
findSibling: while (true) {
while (node.sibling === null) { // 图3
const parent = node.return;
if (parent === null || parent.tag === HostComponent ) {
return null;
}
node = parent;
}
node.sibling.return = node.return;
node = node.sibling;
while (node.tag !== HostText && node.tag !== HostCompoent) {
if ((node.flags & Placement) !== NoFlags) { // 不稳定节点,继续
continue findSibling;
}
if (node.child === null) {
continue findSibling;
} else {
node.child.return = node;
node = node.child;
}
}
if ((node.flags & Placement) === NoFlags) {
return node.stateNode;
}
}
}
example4: 节点属性变化
未完待续 🐶