美文网首页
手写虚拟DOM(四)—— 进一步提升Diff效率之关键字Key

手写虚拟DOM(四)—— 进一步提升Diff效率之关键字Key

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

本文为系列文章:

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

一、前言

本文继续上一节的Virtual DOM Diff,来聊一聊:
在渲染数组元素的时候,编译器会提醒加上key这个属性,那么key是用来做什么的呢?

二、key的作用

在渲染数组元素时,它们一般都有相同的结构,只是内容有些不同而已,比如:

<ul>
    <li>
        <span>商品:苹果</span>
        <span>数量:1</span>
    </li>
    <li>
        <span>商品:香蕉</span>
        <span>数量:2</span>
    </li>
    <li>
        <span>商品:雪梨</span>
        <span>数量:3</span>
    </li>
</ul>

可以把这个例子想象成一个购物车。此时如果想往购物车里面添加一件商品,性能不会有任何问题,因为只是简单的在ul的末尾追加元素,前面的元素都不需要更新:

<ul>
    <li>
        <span>商品:苹果</span>
        <span>数量:1</span>
    </li>
    <li>
        <span>商品:香蕉</span>
        <span>数量:2</span>
    </li>
    <li>
        <span>商品:雪梨</span>
        <span>数量:3</span>
    </li>
     <li>
        <span>商品:橙子</span>
        <span>数量:2</span>
    </li>
</ul>

但是,如果我要删除第一个元素,根据VD的比较逻辑,后面的元素全部都要进行更新的操作。dom结构简单还好说,如果是一个复杂的结构,那页面渲染的性能将会受到很大的影响。

<ul>
    <li>
        <span>商品:香蕉</span>
        <span>数量:2</span>
    </li>
    <li>
        <span>商品:雪梨</span>
        <span>数量:3</span>
    </li>
     <li>
        <span>商品:橙子</span>
        <span>数量:2</span>
    </li>
</ul>

最直观的方法肯定是直接删除第一个元素然后其它元素保持不变了。

但程序没有这么智能,可以像我们一样一眼就看出变化。程序能做到的是尽量少的修改元素,通过移动元素而不是修改元素来达到更新的目的。为了告诉程序要怎么移动元素,我们必须给每个元素加上一个唯一标识,也就是key。

<ul>
    <li key="apple">
        <span>商品:苹果</span>
        <span>数量:1</span>
    </li>
    <li key="banana">
        <span>商品:香蕉</span>
        <span>数量:2</span>
    </li>
    <li key="pear">
        <span>商品:雪梨</span>
        <span>数量:3</span>
    </li>
    <li key="orange">
        <span>商品:橙子</span>
        <span>数量:2</span>
    </li>
</ul>

当把苹果删掉的时候,Virtual DOM里面第一个元素是香蕉,而实际dom里面第一个元素是苹果。
当元素有key属性的时候,框架就会尝试根据这个key去找对应的元素,找到了就将这个元素移动到第一个位置,循环往复。
最后Virtual DOM里面没有第四个元素了,才会把苹果从dom移除。

三、代码实现

在上一个版本代码的基础上,主要的改动点是diffChildren这个函数。原来的实现很简单,递归的调用diff就可以了:

function diffChildren(vdom, element) {
    const nodes = element.childNodes || [];
    const children = vdom.children || [];

    const hasKeys = {};
    let noKeys = [];

    // 根据是否有key先进行分组
    nodes.forEach(node => {
        const props = node['props'];
        if (props && props.key !== undefined) {
            hasKeys[props.key] = node;
        } else {
            noKeys.push(node);
        }
    });

    // 遍历vdom
    children.forEach((child, index) => {
        let dom;
        const key = child.props && child.props.key !== undefined ? child.props.key : undefined;
        if (key != null && hasKeys[key]) {
            dom = hasKeys[key];
            delete hasKeys[key];
        } else {
            for (let i = 0; i < noKeys.length; i ++) {
                const node = noKeys[i];
                if (isEqual(child, node)) {
                    dom = node;
                    noKeys.splice(i, 1);
                    break;
                }
            }
        }

        const isUpdate = diff(dom, child, element);
        if (isUpdate) {
            // 更新(移动),则移到当前元素的位置,当前元素向后延一位
            const origin = nodes[index];
            if (origin !== child) {
                element.insertBefore(dom, origin);
            }
        }
    });

    // 移除不在新的vdom中的节点
    const list = Object.keys(hasKeys);
    if (list.length > 0) {
        list.forEach(key => {
            element.removeChild(hasKeys[key]);
        });
    }
    if (noKeys.length > 0) {
        noKeys.forEach(node => {
            element.removeChild(node);
        });
    }
}

主要是以下几个步骤:

  1. 将所有dom子元素分为有key和没key两组
  2. 遍历VD子元素:
    • 如果VD子元素有key,则去查找有key的分组;
    • 如果没key,则去没key的分组找一个类型相同的元素出来;
  3. diff一下,得出是否更新元素的类型
  4. 如果是更新元素且子元素不是原来的,则移动元素
  5. 最后清理删除没用上的dom子元素

diff也要改造一下,如果是新建、删除或者替换元素,返回false。更新元素则返回true:

function diff(srcDOM, destDOM, parent) {
    // 原dom没有,新vdom有,则表明是新增节点
    if (srcDOM === undefined) {
        parent.appendChild(createElement(destDOM));
        return false;
    }

    // 原dom有,新vdom没有,则表明是移除节点
    if (destDOM === undefined) {
        parent.removeChild(srcDOM);
        return false;
    }

    // 新老节点(类型不同 or tag不同 or 内容不同),则表明是替换节点
    if (!isEqual(destDOM, srcDOM)) {
        parent.replaceChild(createElement(destDOM), srcDOM);
        return false;
    }

    // 至此,只有可能是当前vdom的自身props变化 or 其children发生变化
    if (srcDOM.nodeType === Node.ELEMENT_NODE) {
        diffProps(destDOM.props, srcDOM);
        diffChildren(destDOM, srcDOM);
    }
    return true;
}

为了看效果,view函数也要改造下:

const state = {
    list: []
};

function add(key) {
    let index = -1;
    for (let i = 0; i < state.list.length; i ++) {
        const param = state.list[i];
        if (param.id === key) {
            index = i;
            break;
        }
    }

    if (index === -1) {
        state.list.push({
            id: key,
            number: 1
        });
    } else {
        state.list[index].number ++;
    }
    render(root);
}

function del(key) {
    for (let i = 0; i < state.list.length; i ++) {
        const param = state.list[i];
        if (param.id === key) {
            state.list.splice(i, 1);
            break;
        }
    }
    render(root);
}

// 根据 state.number 来计算有多少个 div
function view() {
    const goods = [];
    for (let i = 0; i < 5; i ++) {
        goods.push(
            <span class="goods" onClick={"add(" + i + ")"} key={i}>商品{i+1}</span>
        )
    }

    const car = [];
    state.list.forEach(item => {
        car.push(
            <li key={item.id} onClick={"del(" + item.id + ")"}>
                <span>商品:{item.id}....</span>
                <span>数量:{item.number}</span>
            </li>
        )
    });

    return (
        <div data-list={state.list.length}>
            <div>{goods}</div>
            <ul>{car}</ul>
        </div>
    );
}

四、总结

本文基于上一个版本的代码,加入了对唯一标识(key)的支持,很好的提高了更新数组元素的效率。

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

相关文章

  • 手写虚拟DOM(四)—— 进一步提升Diff效率之关键字Key

    本文为系列文章: 手写虚拟DOM(一)—— VirtualDOM介绍[https://www.jianshu.co...

  • 虚拟dom和diff算法

    虚拟DOM和diff算法 diff:精细化比对最小量更新 真实DOM和虚拟DOM 虚拟DOM:用JavaScrip...

  • 1,v-for:key

    1,为什么v-for需要添加key,优缺点和场景。 前置知识: 虚拟Dom和Diff算法 KeyWords: 虚拟...

  • Diff 算法、key

    概念 DOM diff 就是对比两棵虚拟 DOM 树的算法。当组件变化时,会得到一个新的虚拟 DOM,diff 算...

  • react VS Vue diff算法

    react diff diff算法的作用:数据更改,生成相应的虚拟DOM,与真实DOM作对比,通过diff算法,对...

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

    本文为系列文章: 手写虚拟DOM(一)—— VirtualDOM介绍[https://www.jianshu.co...

  • 深入理解react中的虚拟DOM、diff算法

    文章结构: React中的虚拟DOM是什么? 虚拟DOM的简单实现(diff算法) 虚拟DOM的内部工作原理 Re...

  • 第十七天

    1.你怎么理解vue中的diff算法? diff算法是虚拟DOM技术的必然产物:通过新旧虚拟DOM作对比(即dif...

  • 初识React

    1、虚拟DOM:降低了算法复杂度,提升速度,主要是diff computation 2、组件(快速复用) 3、学会...

  • vue系列---vue-diff

    1.vue-diff 是什么? 提到vue的diff算法就不得不提一个名词 虚拟dom(Virtual DOM) ...

网友评论

      本文标题:手写虚拟DOM(四)—— 进一步提升Diff效率之关键字Key

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