实现一个精简React -- 更新props时节点的创建与删除(7)

49 阅读3分钟

在上一节 更新props 中,我们判断如果新旧dom的对应tag如果是一样的,name表示更新逻辑,如果不一样,则表示新建或删除。

从一个例子中去理解:

let show = true
const Counter = () => {
    function handleClick() {
        show = !show
        React.update()
    }

    const Foo = () => <div>foo</div>
    const Bar = () => <p>bar</p>

    // 这里通过show变了来控制显示哪个组件
    return (
        <div>
            count:
            <div>{show ? <Foo></Foo> : <Bar></Bar>}</div>
            <button onClick={handleClick}>click</button>
        </div>
    )
}

上面例子中对应的链表图示如下。 image.png

从新旧链表中可以知道,需要将旧的组件<Foo />删掉,并新建一个<Bar />组件

实现思路:

  1. 先将需要删除的dom(也就是新旧节点不一样的旧dom)收集起来。
  2. 在dom的挂载之前将需要删除的dom删掉,然后执行挂载操作。

1.收集需要删除的dom

let deletions = []  // 新建一个全局变量deletions,将需要删除的节点收集起来
function initChildren(fiber, children) {
    let oldFiber = fiber.alternate?.child;
    let prevChild = null;
    children.forEach((child, index) => {
        // 判断旧的tag与新tag是否相同
        // true:更新节点
        // false:创建、删除节点
        const isSameTag = oldFiber && oldFiber.type === child.type;
        let newFiber = null;
        if (isSameTag) {
            // 更新
            // ...
        } else {
            // 新增
            // 这里新建节点时会有一个边缘情况产生
            if (child) {
                newFiber = {
                    type: child.type,
                    props: child.props,
                    child: null,
                    sibling: null,
                    parent: fiber,
                    dom: null,
                    effectTag: "placement",
                };
            }

            // 删除逻辑,将需要删除的节点收集起来
            if (oldFiber) {
                deletions.push(oldFiber);
            }
        }

        // ...
    });

    // 如果旧的节点的子节点多于新节点的子节点,则需要把旧节点的子节点全部删除
    // 所以循环调用,将sibling全部添加到deletions中
    while (oldFiber) {
        deletions.push(oldFiber);
        oldFiber = oldFiber.sibling;
    }
}

2.挂载之前删除dom

dom挂载环节是在链表创建完成后的 commitRoot() 函数中执行的,所以在挂载之前执行删除dom操作。

function commitRoot() {
    deletions.forEach(commitDeletion); // 执行删除操作
    commitWork(wipRoot.child);
    currentRoot = wipRoot;
    wipRoot = null;
    deletions = []; // 删除完后清空
}
function commitDeletion(fiber) {
    // step1.直接删除节点
    // 问题:如果是函数组件的话,函数组件的dom是null
    // 解决:递归查找子节点
    // fiber.parent.dom.removeChild(fiber.dom)

    // step2.递归查找子节点
    // 问题:函数组件查找到子节点后,因为函数组件dom为null,执行删除时会发现fiber.parent.dom为null,同样有问题
    // 解决:循环查找dom不为null的父节点
    // if (fiber.dom) {
    //     fiber.parent.dom.removeChild(fiber.dom);
    // } else {
    //     commitDeletion(fiber.child);
    // }

    // step3.循环查找dom不为null的父节点
    if (fiber.dom) {
        let fiberParent = fiber.parent;
        while (!fiberParent.dom) {
            fiberParent = fiberParent.parent;
        }
        fiberParent.dom.removeChild(fiber.dom);
    } else {
        commitDeletion(fiber.child);
    }
}

在新建节点时会有一个边缘情况 edge case:

我们经常会遇到一个情况,如以下代码所示,通过一个变量来控制一个组件是否显示。

let show = false
const Counter = () => {

    function handleClick() {
        show = !show
        React.update()
    }

    const Foo = () => <div>foo</div>

    return (
        <div>
            count
            <div>{show && <Foo></Foo>}</div>
            <button onClick={handleClick}>click</button>
        </div>
    )
}

这种情况下会导致我们的代码报错

image.png

分析过程:

首先是逻辑与&&的解释

逻辑与(&&)运算符从左到右对操作数求值,遇到第一个假值操作数时立即返回;如果所有的操作数都是真值,则返回最后一个操作数的值。

由此可知:{show && <Foo></Foo>} 中,show为false则返回false,为true则返回<Foo></Foo>

所以,上述报错是由于返回了false导致的。

在react创建dom的方式是通过解析 jsx 后调用 createElement() 函数去创建dom

const createElement = (type, props, ...children) => {
    return {
        type,
        props: {
            ...props,
            children: children.map((child) => {
                const isTextNode =
                    typeof child === "string" || typeof child === "number";
                return isTextNode ? createTextEl(child) : child;
            }),
        },
    };
};

所以从以上代码可以清晰的看到,如果child为Boolean类型的话,isTextNode判断为false,将直接返回child,也就是将变量给return回去了。

但由于这个变量并不是一个dom,这样我们在遍历Vdom创建链表的过程中会发现dom属性是undefined

image.png

然后就在创建dom的函数中,这里将会直接创建一个 <undefined></undefined> 的dom,导致了后面 updateProps 的报错。

function updateHostComponent(fiber) {
    if (!fiber.dom) {
        // 1.创建dom节点
        const dom = (fiber.dom = createDom(fiber.type));
        console.log("dom", dom); // <undefined></undefined>

        // 2.设置props
        updateProps(dom, fiber.props, {});
    }

    const children = fiber.props.children;
    reconcileChildren(fiber, children);
}

解决问题:

在创建链表时,如果child的判断为false,则直接略过

function reconcileChildren(fiber, children) {
    // ...
    children.forEach((child, index) => {
        const isSameTag = oldFiber && oldFiber.type === child.type;
        let newFiber = null;
        if (isSameTag) {
            // ...
        } else {
            // 新增 / 删除
            if (child) {  // 判断child,为false则直接略过
                newFiber = {
                    type: child.type,
                    props: child.props,
                    child: null,
                    sibling: null,
                    parent: fiber,
                    dom: null,
                    effectTag: "placement",
                };
            }
            
            // ...
        }
        // ...

        // 如果child为false,newFiber则为undefined,所以就不用再赋值了
        if (newFiber) {
            prevChild = newFiber;
        }
    });
}

项目源码:github.com/Cuimc/mini-…