手寫虛擬DOM(四)—— 進一步提升Diff效率之關鍵字Key

本文為系列文章:

手寫虛擬DOM(一)—— VirtualDOM介紹
手寫虛擬DOM(二)—— VirtualDOM Diff
手寫虛擬DOM(三)—— Diff算法優(yōu)化
手寫虛擬DOM(四)—— 進一步提升Diff效率之關鍵字Key
手寫虛擬DOM(五)—— 自定義組件
手寫虛擬DOM(六)—— 事件處理
手寫虛擬DOM(七)—— 異步更新

一、前言

本文繼續(xù)上一節(jié)的Virtual DOM Diff,來聊一聊:
在渲染數(shù)組元素的時候,編譯器會提醒加上key這個屬性,那么key是用來做什么的呢?

二、key的作用

在渲染數(shù)組元素時,它們一般都有相同的結(jié)構,只是內(nèi)容有些不同而已,比如:

<ul>
    <li>
        <span>商品:蘋果</span>
        <span>數(shù)量:1</span>
    </li>
    <li>
        <span>商品:香蕉</span>
        <span>數(shù)量:2</span>
    </li>
    <li>
        <span>商品:雪梨</span>
        <span>數(shù)量:3</span>
    </li>
</ul>

可以把這個例子想象成一個購物車。此時如果想往購物車里面添加一件商品,性能不會有任何問題,因為只是簡單的在ul的末尾追加元素,前面的元素都不需要更新:

<ul>
    <li>
        <span>商品:蘋果</span>
        <span>數(shù)量:1</span>
    </li>
    <li>
        <span>商品:香蕉</span>
        <span>數(shù)量:2</span>
    </li>
    <li>
        <span>商品:雪梨</span>
        <span>數(shù)量:3</span>
    </li>
     <li>
        <span>商品:橙子</span>
        <span>數(shù)量:2</span>
    </li>
</ul>

但是,如果我要刪除第一個元素,根據(jù)VD的比較邏輯,后面的元素全部都要進行更新的操作。dom結(jié)構簡單還好說,如果是一個復雜的結(jié)構,那頁面渲染的性能將會受到很大的影響。

<ul>
    <li>
        <span>商品:香蕉</span>
        <span>數(shù)量:2</span>
    </li>
    <li>
        <span>商品:雪梨</span>
        <span>數(shù)量:3</span>
    </li>
     <li>
        <span>商品:橙子</span>
        <span>數(shù)量:2</span>
    </li>
</ul>

最直觀的方法肯定是直接刪除第一個元素然后其它元素保持不變了。

但程序沒有這么智能,可以像我們一樣一眼就看出變化。程序能做到的是盡量少的修改元素,通過移動元素而不是修改元素來達到更新的目的。為了告訴程序要怎么移動元素,我們必須給每個元素加上一個唯一標識,也就是key。

<ul>
    <li key="apple">
        <span>商品:蘋果</span>
        <span>數(shù)量:1</span>
    </li>
    <li key="banana">
        <span>商品:香蕉</span>
        <span>數(shù)量:2</span>
    </li>
    <li key="pear">
        <span>商品:雪梨</span>
        <span>數(shù)量:3</span>
    </li>
    <li key="orange">
        <span>商品:橙子</span>
        <span>數(shù)量:2</span>
    </li>
</ul>

當把蘋果刪掉的時候,Virtual DOM里面第一個元素是香蕉,而實際dom里面第一個元素是蘋果。
當元素有key屬性的時候,框架就會嘗試根據(jù)這個key去找對應的元素,找到了就將這個元素移動到第一個位置,循環(huán)往復。
最后Virtual DOM里面沒有第四個元素了,才會把蘋果從dom移除。

三、代碼實現(xiàn)

在上一個版本代碼的基礎上,主要的改動點是diffChildren這個函數(shù)。原來的實現(xiàn)很簡單,遞歸的調(diào)用diff就可以了:

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

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

    // 根據(jù)是否有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中的節(jié)點
    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有,則表明是新增節(jié)點
    if (srcDOM === undefined) {
        parent.appendChild(createElement(destDOM));
        return false;
    }

    // 原dom有,新vdom沒有,則表明是移除節(jié)點
    if (destDOM === undefined) {
        parent.removeChild(srcDOM);
        return false;
    }

    // 新老節(jié)點(類型不同 or tag不同 or 內(nèi)容不同),則表明是替換節(jié)點
    if (!isEqual(destDOM, srcDOM)) {
        parent.replaceChild(createElement(destDOM), srcDOM);
        return false;
    }

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

為了看效果,view函數(shù)也要改造下:

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);
}

// 根據(jù) 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>數(shù)量:{item.number}</span>
            </li>
        )
    });

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

四、總結(jié)

本文基于上一個版本的代碼,加入了對唯一標識(key)的支持,很好的提高了更新數(shù)組元素的效率。

項目源碼:
https://github.com/qingye/VirtualDOM-Study/tree/master/VirtualDOM-Study-04

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容