美文网首页
手写虚拟DOM(二)—— VirtualDOM Diff

手写虚拟DOM(二)—— VirtualDOM Diff

作者: 青叶小小 | 来源:发表于2020-12-25 10:45 被阅读0次

    本文为系列文章:

    手写虚拟DOM(一)—— VirtualDOM介绍
    手写虚拟DOM(二)—— VirtualDOM Diff
    手写虚拟DOM(三)—— Diff算法优化
    手写虚拟DOM(四)—— 进一步提升Diff效率之关键字Key
    手写虚拟DOM(五)—— 自定义组件
    手写虚拟DOM(六)—— 事件处理
    手写虚拟DOM(七)—— 异步更新

    一、前言

    本文继续上一节的Virtual DOM内容,来聊一聊,当内容即dom tree发生变化时,
    如何基于Virtual DOM来进行局部刷新渲染,而不是整棵树都重新渲染。

    二、思路

    使用 Virtual DOM 的框架,一般的设计思路都是页面等于页面状态的映射,即 UI = render(state)。当需要更新页面的时候,无需关心 DOM 具体的变换方式,只需要改变state即可,剩下的事情(render)将由框架代劳。我们考虑最简单的情况,当 state 发生变化时,我们重新生成整个 Virtual DOM ,触发比较的操作。

    上述过程分为以下四步:

    • state 变化,生成新的 Virtual DOM;
    • 比较新 Virtual DOM 与之前 Virtual DOM 的差异;
    • 生成差异对象(patch);
    • 遍历差异对象并更新 DOM;

    差异对象的数据结构是下面这个样子,与每一个 VDOM 元素一一对应:

    {
        type,
        vdom,
        props: [{
            type,
            key,
            value
        }],
        children
    }
    

    DOM 元素的 type 对应的变化类型,有 4 种:新建、删除、替换和更新。
    props 变化的 type 只有2种:更新和删除。

    枚举值如下:

    const TYPES = {
        CREATE: 'create',
        DELETE: 'delete',
        UPDATE: 'update',
        REPLACE: 'replace'
    };
    

    三、代码实现

    此例通过每3秒,新增一个 div 块元素,并修改根元素的属性(data-number)

    3.1、改造原有代码

    // 新增状态对象
    const state = {
        number: 0
    };
    
    // 新增上一次的Virtual DOM内容
    let vdomPre;
    

    // 根据 state.number 来计算有多少个 div
    function view() {
        return (
            <div data-number={state.number}>
                Hello World!
                {
                    [...Array(state.number).keys()].map(idx => (
                        <div id={'div' + idx} data-idx={idx}>-> {idx}</div>
                    ))
                }
            </div>
        );
    }
    

    /*************************************************
     * 在 render 中:
     * 1. 首次创建 dom;
     * 2. 非首次则比较前后两次 Virtual DOM 差异,并更新
     * 3. 记录本次的 Virtual DOM
     *************************************************/
    function render(container) {
        const vdom = view();
        if (!vdomPre) {
            container.appendChild(createElement(vdom));
        } else {
            patch(diff(vdomPre, vdom), container);
        }
        vdomPre = vdom;
    
        setTimeout(() => {
            state.number += 1;
            render(container);
        }, 3000);
    }
    
    // 执行render
    render(document.getElementById("app"));
    

    3.2、diff的算法实现(简单版)

    /***************************************
     * 这里的diff,实际上就是一个递归的过程:
     *
     * 1. diff根元素;
     * 2. diff根元素的props;
     * 3. diff根元素的children;
     * 4. 每个child的diff又是调用diff方法
     ***************************************/
    function diff(pre, post) {
        // 原vdom没有,新vdom有,则表明是新增节点
        if (pre === undefined) {
            return {
                type: TYPES.CREATE,
                vdom: post
            };
        }
    
        // 原vdom有,新vdom没有,则表明是移除节点
        if (post === undefined) {
            return {
                type: TYPES.DELETE
            };
        }
    
        // 新老节点(类型不同 or tag不同 or 内容不同),则表明是替换节点
        if (typeof pre !== typeof post || pre.tag !== post.tag ||
            (typeof pre === 'string' || typeof pre === 'number') && pre !== post) {
            return {
                type: TYPES.REPLACE,
                vdom: post
            };
        }
    
        // 至此,只有可能是当前vdom的自身props变化 or 其children发生变化
        if (pre.tag) {
            const props = diffProps(pre.props, post.props);
            const children = diffChildren(pre, post);
            if (props.length > 0 || children.length > 0) {
                return {
                    type: TYPES.UPDATE,
                    props: props,
                    children: children
                };
            }
        }
    }
    
    function diffProps(preProps, postProps) {
        const patches = [];
    
        // 合并所有props的键值(后者替换前者)
        const all = {...preProps, ...postProps};
    
        // 遍历props的所有键值
        Object.keys(all).forEach(key => {
            const ov = preProps[key];
            const nv = postProps[key];
    
            // 新vdom没有该属性
            if (nv === undefined) {
                patches.push({
                    pType: TYPES.DELETE,
                    key
                });
            }
    
            // 老vdom没有该属性,or 该属性值与新vdom的属性值不一致
            if (ov === undefined || ov !== nv) {
                patches.push({
                    pType: TYPES.UPDATE,
                    key,
                    value: nv
                });
            }
        });
    
        return patches;
    }
    
    function diffChildren(pre, post) {
        const patches = [];
    
        // 子元素最大长度
        const len = Math.max(pre.children.length, post.children.length);
    
        // 依次遍历并diff子元素
        for (let i = 0; i < len; i ++) {
            const param = diff(pre.children[i], post.children[i]);
            if (param) {
                param['idx'] = i;
                patches.push(param);
            }
        }
    
        return patches;
    }
    

    3.3、差异(patch)更新

    /***************************************************************
     * Virtual DOM 增量更新
     ***************************************************************/
    function patch(dif, parent, cid = 0) {
        const len = parent.childNodes.length;
        const child = cid >= len ? null : parent.childNodes[cid];
        switch (dif.type) {
            case TYPES.CREATE:
                parent.appendChild(createElement(dif.vdom));
                return;
    
            case TYPES.DELETE:
                parent.removeChild(child);
                return;
    
            case TYPES.REPLACE:
                parent.replaceChild(createElement(dif.vdom), child);
                return;
    
            case TYPES.UPDATE:
                patchProps(child, dif.props);
                patchChildren(child, dif.children);
                break;
        }
    }
    
    function patchProps(element, props = []) {
        if (!props || props.length === 0) {
            return;
        }
    
        props.forEach(p => {
            if (p.pType === TYPES.DELETE) {
                element.removeAttribute(p.key);
            } else if (p.pType === TYPES.UPDATE) {
                element.setAttribute(p.key, p.value);
            }
        });
    }
    
    function patchChildren(element, children = []) {
        if (!children || children.length === 0) {
            return;
        }
    
        children.forEach(child => {
            patch(child, element, child.idx);
        });
    }
    

    3.4、NodeJS环境

    本次的代码实现,用到了ES6,所以,需要添加 @babel/core 和 @babel/preset-env;
    同时,需要移除原 babel-cli,换成 @babel/cli

    // package.json 的修改
    "dependencies": {
      "@babel/cli": "^7.8.4",
      "@babel/core": "^7.9.6",
      "@babel/preset-env": "^7.9.6",
      "babel-plugin-transform-react-jsx": "^6.24.1"
    }
    

    // .babelrc 的修改
    {
      "plugins": [
        [
          "transform-react-jsx",
          {
            "pragma": "v"
          }
        ]
      ],
      "presets": ["@babel/preset-env"]
    }
    

    四、总结

    本文详细介绍如何实现一个简单的 Virtual DOM Diff 算法,再根据计算出的差异去更新真实的 DOM 。然后对性能做了一个简单的分析,得出使用 Virtual DOM 在减少渲染时间的同时增加了 JS 计算时间的结论。

    项目源码:
    https://github.com/qingye/VirtualDOM-Study/tree/master/VirtualDOM-Study-02

    相关文章

      网友评论

          本文标题:手写虚拟DOM(二)—— VirtualDOM Diff

          本文链接:https://www.haomeiwen.com/subject/zyybnktx.html