React: fiber 树到 dom

796 阅读5分钟

前面两节我们主要讲解了Fiber树的生成过程;这个结构是我们理解后续功能的基础;

本节主要阐述react中更新产生的变化提交到宿主环境,我们使用浏览器环境;

本节是仿写的提交逻辑,真实可能跟源代码有出入,只是讲述思路;

下面分几种情况来看;

exmaple1: 新增节点
const App = () => {
    return (
        <div>
            <h1>title</h1>
            <section>
                <p>content: xxxx</p>
            </section>
        </div>
    );
};
createRoot(root).render(<App/>);

针对这种情况,我们先看下fiber树的结构

image.png

图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:

image.png

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进行操作呢?因为有两种情况,

  1. 这个节点的父节点就是parentNode, 这个肯定也要挂载它的sibling节点;
  2. 这个节点的父节点是非原生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;
    }
}

image.png 对于example1, 流程2只会遍历到HostRootFiber和App, 对App执行commitPlacement, 根据结构,我们要做

  1. 找到App fiber 所属的dom父节点parent
  2. 找到App 子树的根dom节点subRoot
  3. 将subRoot挂载到parent上

逻辑乍看是没有什么问题,但是如果App返回了用<></>包裹的多个节点,如下图,那么这个逻辑就有问题了,我们需要将这三个都挂载到#root上才对;

image.png

所以后面两步我们要修改下,如果fiber是HostComponent或者HostText那么就直接挂载,否则需要将子节点和子节点的sibling的子subRoot dom挂载上;

  1. 找到App fiber 所属的dom父节点parentNode
  2. 收集subRoot
  3. 收集到的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: 删除节点

image.png

image.png

image.png

观察上面三个图,我们大概能总结出删除节点的步骤;

  1. 获取需要删除节点所属的dom父节点parentNode;
  2. 收集要删除的节点(当前是非HostComponent和非HostText,就要收集子节点的所有sibling dom);
  3. 执行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: 节点移动

image.png

image.png

image.png

观察可能的情况,我们大概能总结出移动节点的步骤;

  1. 找到移动节点的有dom的fiber节点,获取parentNode;
  2. 找到移动节点插入的标志dom, 也就是insertBefore的第二个参数,没有的话我们就用appendChild代替(证明是最后一个节点)
  3. 执行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: 节点属性变化

未完待续 🐶