实现一个精简React -- 更新props时进行新旧DOM树的对比并实现diff算法(6)

143 阅读3分钟

在绑定的事件中,如果在修改了某些变量,那么视图也应该相对应的发生改变,在 react 中,除了标签本身以外的所有内容都看作 props,所以只需要针对props进行更新即可。

更新props就是针对新旧两棵dom树进行遍历对比。

对比的规则就是判断新旧dom树对应的节点tag是否一样的,如果一样,则表示更新,如果不一样则表示删除、修改。

在对比之前需要考虑这样几个问题:

  1. 如何获取新的dom树
  2. 如何找到旧的节点
  3. 如何进行diff props对比

如何获取新的dom树

在考虑如何获取新的dom树之前,可以想到dom树的获取是通过render函数拿到了根节点的vdom,然后赋值给 nextUnitOfFier 后启动 perfromFiberOfUnit 函数,然后根据相应的规则创建了链表。

同理,在更新的时候可以给 nextUnitOfFier 重新赋值后再次启动 perfromFiberOfUnit 函数,然后就可以获取新的dom树。

    // 更新时可以调用update方法。
    function update() {
        nextUnitOfFier = {
            dom: currentRoot.dom, 
            props: currentRoot.props,
            alternate: currentRoot, // 表示旧的节点。
        };

        root = nextUnitOfFier;
    }

在update方法中,我们并不能像render函数一样给他传递el和container,所以可以先将el存储起来。

在commitRoot方法中通过commitWork递归拿到了最终的dom结构,所以可以在这里新建一个变量将root给存储起来,作为update中的el。

    function commitRoot() {
        commitWork(root.child);
        currentRoot = root;  // 新建currentRoot变量存储root
        root = null;
    }

至此,当调用update时,就可以重启perfromFiberOfUnit并拿到新的dom树

如何找到旧的节点

新的dom树创建完成后,我们可以通过添加一个字段 alternate(备用)来表示旧的节点,以此来做一一对应关系,如下图所示:

image.png

如何在新旧dom树种建立起这种关系呢?

在处理dom树的时候,我们将dom树转化成了链表,所以可以在处理链表时将alternate关系给表示出来。如下图所示:

image.png

所以在创建链表的时候处理:

    // 转化成链表,做好指针指向
    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) {
                // 更新
                newFiber = {
                    type: child.type,
                    props: child.props,
                    child: null,
                    sibling: null,
                    parent: fiber,
                    dom: oldFiber.dom, // 更新props,dom没有变化,所以可以使用旧dom
                    effectTag: "update", // 更新标识
                    alternate: oldFiber, // 将旧的节点给指向到alternate上建立关系
                };
            } else {
                // 新增
                newFiber = {
                    type: child.type,
                    props: child.props,
                    child: null,
                    sibling: null,
                    parent: fiber,
                    dom: null,
                    effectTag: "placement",  // 其他标识
                };
            }
    				
            // 按照创建链表的逻辑,child处理完后应该再处理sibling,所以要将sibling返回
            if (oldFiber) {
                oldFiber = oldFiber.sibling;
            }

            // ...
        });
    }

至此,新旧dom的对应关系就建立完成。

如何进行diff props对比

更新props是在处理props 即函数 updateProps 中进行的,在更新props时可以看做有三种情况:

  1. new没有 old有 => 表示删除
  2. new有 old没有 => 表示新增
  3. new有 old有 => 表示修改

在这三种情况中可以看做有两种处理方式,1 ⇒ 删除属性 2,3 ⇒ 更新属性

    /**
     * 
     * @param {*} dom 
     * @param {*} nextProps 新的props
     * @param {*} prevProps 旧的props
     */
    function updateProps(dom, nextProps, prevProps) {
        // 1. new没有  old有  删除
        Object.keys(prevProps).forEach((key) => {
            if (!(key in nextProps)) {
                if (key !== "children") {
                    dom.removeAttribute(key);
                }
            }
        });

        // 2. new有   old没有  新增
        // 3. new有   old有   更新
        Object.keys(nextProps).forEach((key) => {
            if (nextProps[key] !== prevProps[key]) {
                if (key !== "children") {
                    if (key.startsWith("on")) {
                        const eventType = key.slice(2).toLocaleLowerCase();
                        // 在添加事件之前,要把之前的事件给注销掉,否则会重复触发以前的事件
                        
                        dom.removeEventListener(eventType, prevProps[key]);
                        dom.addEventListener(eventType, nextProps[key]);
                    }
                    dom[key] = nextProps[key];
                }
            }
        });
    }

这样就可以在修改变量时调用update方法,实现视图更新。

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