美文网首页
手写虚拟DOM(三)—— Diff算法优化

手写虚拟DOM(三)—— Diff算法优化

作者: 青叶小小 | 来源:发表于2020-12-26 13:30 被阅读0次

    本文为系列文章:

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

    一、前言

    本文继续上一节的Virtual DOM Diff,来聊一聊,有哪些可优化的点:

    • 我们通过diff获取了新、老 Virtual DOM的差异,然后再对现有DOM进行打补丁;
      既然,我们都找出了差异,何不直接修改DOM,还要再多此一举呢?
    • 我们每次都会保存上一次的Virtual DOM,与新的Virtual DOM比较,再转成真实的dom树;
      那我们何不将Virtual DOM与真实dom树直接关联,diff差异,然后直接更新呢?

    二、优化

    2.1、优化diff,去掉patch

    // 改造 render
    function render(container) {
        const vdom = view();
        if (!vdomPre) {
            container.appendChild(createElement(vdom));
        } else {
            diff(vdomPre, vdom, container);  <====== 直接传入 root container
        }
        vdomPre = vdom;
    
        setTimeout(() => {
            state.number += 1;
            render(container);
        }, 3000);
    }
    

    // 改造 diff
    function diff(pre, post, parent, cid = 0) {
        const len = parent.childNodes.length;
        const child = cid >= len ? undefined : parent.childNodes[cid];
    
        // 原vdom没有,新vdom有,则表明是新增节点
        if (pre === undefined) {
            parent.appendChild(createElement(post));
            return;
        }
    
        // 原vdom有,新vdom没有,则表明是移除节点
        if (post === undefined) {
            parent.removeChild(child);
            return
        }
    
        // 新老节点(类型不同 or tag不同 or 内容不同),则表明是替换节点
        if (typeof pre !== typeof post || pre.tag !== post.tag ||
            (typeof pre === 'string' || typeof pre === 'number') && pre !== post) {
            parent.replaceChild(createElement(post), child);
            return;
        }
    
        // 至此,只有可能是当前vdom的自身props变化 or 其children发生变化
        if (pre.tag) {
            diffProps(pre.props, post.props, child);
            diffChildren(pre, post, child);
        }
    }
    

    // 改造 diffProps
    function diffProps(preProps, postProps, element) {
        // 合并所有props的键值(后者替换前者)
        const all = {...preProps, ...postProps};
    
        // 遍历props的所有键值
        Object.keys(all).forEach(key => {
            const ov = preProps[key];
            const nv = postProps[key];
    
            // 新vdom没有该属性
            if (nv === undefined) {
                element.removeAttribute(key);
            }
    
            // 老vdom没有该属性,or 该属性值与新vdom的属性值不一致
            if (ov === undefined || ov !== nv) {
                element.setAttribute(key, nv);
            }
        });
    }
    

    // 改造 diffChildren
    function diffChildren(pre, post, element) {
        // 子元素最大长度
        const len = Math.max(pre.children.length, post.children.length);
    
        // 依次遍历并diff子元素
        for (let i = 0; i < len; i ++) {
            diff(pre.children[i], post.children[i], element, i);
        }
    }
    

    在diff、diffProps中,直接操作dom或者属性。

    2.2、关联真实dom树,直接更新

    // 修改 render
    function render(container) {
        diff(view(), container);
    
        setTimeout(() => {
            state.number += 1;
            render(container);
        }, 3000);
    }
    

    // 修改 setProps
    function setProps(element, props) {
        for (let k in props) {
            if (props.hasOwnProperty(k)) {
                element.setAttribute(k, props[k]);
            }
        }
    
        // 保存当前的属性,之后用于新VirtualDOM的属性比较
        element['props'] = props;    <===== 重点是这里
    }
    

    // 修改 diff
    function diff(vdom, parent, cid = 0) {
        const len = parent.childNodes.length;
    
        // child是当前真实dom!
        const child = cid >= len ? undefined : parent.childNodes[cid];
    
        // 原dom没有,新vdom有,则表明是新增节点
        if (child === undefined) {
            parent.appendChild(createElement(vdom));
            return;
        }
    
        // 原dom有,新vdom没有,则表明是移除节点
        if (vdom === undefined) {
            parent.removeChild(child);
            return
        }
    
        // 新老节点(类型不同 or tag不同 or 内容不同),则表明是替换节点
        if (!isEqual(vdom, child)) {
            parent.replaceChild(createElement(vdom), child);
            return;
        }
    
        // 至此,只有可能是当前vdom的自身props变化 or 其children发生变化
        if (child.nodeType === Node.ELEMENT_NODE) {
            diffProps(vdom.props, child);
            diffChildren(vdom, child);
        }
    }
    


    这里,用到了一个新的比较方法判断是否需要Replace节点:
    function isEqual(vdom, element) {
        const elType = element.nodeType;
        const vdomType = typeof vdom;
    
        // 检查dom元素是文本节点的情况
        if (elType === Node.TEXT_NODE &&
            (vdomType === 'string' || vdomType === 'number') &&
            element.nodeValue === vdom) {
            return true;
        }
    
        // 检查dom元素是普通节点的情况
        if (elType === Node.ELEMENT_NODE && element.tagName.toLowerCase() === vdom.tag.toLowerCase()) {
            return true;
        }
    
        return false;
    }
    

    // 修改 diffProps
    function diffProps(props, element) {
        // 合并所有props的键值(后者替换前者)
        const all = {...element['props'], ...props};   <===== 合并直接dom中的props与新的VirtualDOM的props
        const newProps = {};
    
        // 遍历props的所有键值
        Object.keys(all).forEach(key => {
            const ov = element['props'][key];
            const nv = props[key];
    
            // 新vdom没有该属性
            if (nv === undefined) {
                element.removeAttribute(key);
                return;
            }
    
            // 老vdom没有该属性,or 该属性值与新vdom的属性值不一致
            if (ov === undefined || ov !== nv) {
                element.setAttribute(key, nv);
            }
            newProps[key] = all[key];
        });
        element['props'] = newProps;  // 保存最新的属性
    }
    

    // 修改 diffChildren
    function diffChildren(vdom, element) {
        // 子元素最大长度
        const len = Math.max(element.childNodes.length, vdom.children.length);
    
        // 依次遍历并diff子元素
        for (let i = 0; i < len; i ++) {
            diff(vdom.children[i], element, i);
        }
    }
    

    三、总结

    本文基于上一个版本的代码,简化了页面渲染的过程(省略patch对象),
    同时提供了更灵活的VD比较方法(直接跟dom比较),可用性越来越强了。

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

    相关文章

      网友评论

          本文标题:手写虚拟DOM(三)—— Diff算法优化

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