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

2020-12-26  本文已影响0人  青叶小小

本文为系列文章:

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

一、前言

本文继续上一节的Virtual 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

上一篇下一篇

猜你喜欢

热点阅读