你不知道的Virtual DOM(三):Virtual Dom更新優化

歡迎關注個人公衆號睿Talk,獲取我最新的文章:
clipboard.pngjavascript

1、前言

目前最流行的兩大前端框架,React和Vue,都不約而同的藉助Virtual DOM技術提升頁面的渲染效率。那麼,什麼是Virtual DOM?它是經過什麼方式去提高頁面渲染效率的呢?本系列文章會詳細講解Virtual DOM的建立過程,並實現一個簡單的Diff算法來更新頁面。本文的內容脫離於任何的前端框架,只講最純粹的Virtual DOM。敲單詞太累了,下文Virtual DOM一概用VD表示。前端

這是VD系列文章的第三篇,如下是本系列其它文章的傳送門:
你不知道的Virtual DOM(一):Virtual Dom介紹
你不知道的Virtual DOM(二):Virtual Dom的更新
你不知道的Virtual DOM(三):Virtual Dom更新優化
你不知道的Virtual DOM(四):key的做用
你不知道的Virtual DOM(五):自定義組件
你不知道的Virtual DOM(六):事件處理&異步更新java

本文基於本系列文章的第二篇,對VD的比較過程進行優化。node

2、優化一:省略patch對象,直接更新dom

在上一個版本的代碼裏,咱們是經過在diff過程當中生成patch對象,而後在利用這個對象更新dom。git

function tick(element) {
    if (state.num > 20) {
        clearTimeout(timer);
        return;
    }

    const newVDom = view();

    // 生成差別對象
    const patchObj = diff(preVDom, newVDom);

    preVDom = newVDom;

    // 給dom打個補丁
    patch(element, patchObj);
}

實際上這步是多餘的。既然在diff的時候就已經知道要如何操做dom了,那爲何不直接在diff裏面更新呢?先來回顧下以前的diff代碼:github

function diff(oldVDom, newVDom) {
    // 新建node
    if (oldVDom == undefined) {
        return {
            type: nodePatchTypes.CREATE,
            vdom: newVDom
        }
    }

    // 刪除node
    if (newVDom == undefined) {
        return {
            type: nodePatchTypes.REMOVE
        }
    }

    // 替換node
    if (
        typeof oldVDom !== typeof newVDom ||
        ((typeof oldVDom === 'string' || typeof oldVDom === 'number') && oldVDom !== newVDom) ||
        oldVDom.tag !== newVDom.tag
    ) {
       return {
           type: nodePatchTypes.REPLACE,
           vdom: newVDom
       } 
    }

    // 更新node
    if (oldVDom.tag) {
        // 比較props的變化
        const propsDiff = diffProps(oldVDom, newVDom);

        // 比較children的變化
        const childrenDiff = diffChildren(oldVDom, newVDom);

        // 若是props或者children有變化,才須要更新
        if (propsDiff.length > 0 || childrenDiff.some( patchObj => (patchObj !== undefined) )) {
            return {
                type: nodePatchTypes.UPDATE,
                props: propsDiff,
                children: childrenDiff
            }   
        }
        
    }
}

diff最終返回的對象是這個數據結構:算法

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

如今,咱們把生成對象的步驟省略掉,直接操做dom。這時候咱們須要將父元素,還有子元素的索引傳進來(原patch的邏輯):segmentfault

function diff(oldVDom, newVDom, parent, index=0) {
    // 新建node
    if (oldVDom == undefined) {
        parent.appendChild(createElement(newVDom));
    }

    const element = parent.childNodes[index];

    // 刪除node
    if (newVDom == undefined) {
        parent.removeChild(element);
    }

    // 替換node
    if (
        typeof oldVDom !== typeof newVDom ||
        ((typeof oldVDom === 'string' || typeof oldVDom === 'number') && oldVDom !== newVDom) ||
        oldVDom.tag !== newVDom.tag
    ) {
        parent.replaceChild(createElement(newVDom), element);
    }

    // 更新node
    if (oldVDom.tag) {
        // 比較props的變化
        diffProps(oldVDom, newVDom, element);

        // 比較children的變化
        diffChildren(oldVDom, newVDom, element);
    }
}

function diffProps(oldVDom, newVDom) {
    const allProps = {...oldVDom.props, ...newVDom.props};

    // 獲取新舊全部屬性名後,再逐一判斷新舊屬性值
    Object.keys(allProps).forEach((key) => {
            const oldValue = oldVDom.props[key];
            const newValue = newVDom.props[key];

            // 刪除屬性
            if (newValue == undefined) {
                element.removeAttribute(key);
            } 
            // 更新屬性
            else if (oldValue == undefined || oldValue !== newValue) {
                element.setAttribute(key, newValue);
            }
        }
    )
}

function diffChildren(oldVDom, newVDom, parent) {
    // 獲取子元素最大長度
    const childLength = Math.max(oldVDom.children.length, newVDom.children.length);

    // 遍歷並diff子元素
    for (let i = 0; i < childLength; i++) {
        diff(oldVDom.children[i], newVDom.children[i], parent, i);
    }
}

本質上來講,此次的優化是將patch的邏輯整合進diff的過程當中了。通過此次優化,JS計算的時間快了那麼幾毫秒。雖然性能的提高不大,但代碼比原來的少了80多行,下降了邏輯複雜度,優化的效果仍是不錯的。前端框架

clipboard.png

3、優化二:VD與真實dom融合

在以前的版本里面,diff操做針對的是新舊2個VD。既然真實的dom已經根據以前的VD渲染出來了,有沒辦法用當前的dom跟新的VD作比較呢?數據結構

答案是確定的,只須要按需獲取dom中不一樣的屬性就能夠了。好比,當比較tag的時候,使用的是nodeType和tagName,比較文本的時候用的是nodeValue。

function tick(element) {
    if (state.num > 20) {
        clearTimeout(timer);
        return;
    }

    const newVDom = view();

    // 比較並更新節點
    diff(newVDom, element);
    // diff(preVDom, newVDom, element);

    // preVDom = newVDom;
}

function diff(newVDom, parent, index=0) {
    
    const element = parent.childNodes[index];

    // 新建node
    if (element == undefined) {
        parent.appendChild(createElement(newVDom));
        return;
    }

    // 刪除node
    if (newVDom == undefined) {
        parent.removeChild(element);
        return;
    }

    // 替換node
    if (!isSameType(element, newVDom)) {
        parent.replaceChild(createElement(newVDom), element);
        return;
    }

    // 更新node
    if (element.nodeType === Node.ELEMENT_NODE) {
        // 比較props的變化
        diffProps(newVDom, element);

        // 比較children的變化
        diffChildren(newVDom, element);
    }
}

// 比較元素類型是否相同
function isSameType(element, newVDom) {
    const elmType = element.nodeType;
    const vdomType = typeof newVDom;

    // 當dom元素是文本節點的狀況
    if (elmType === Node.TEXT_NODE && 
        (vdomType === 'string' || vdomType === 'number') &&
        element.nodeValue == newVDom
    ) {
       return true; 
    }

    // 當dom元素是普通節點的狀況
    if (elmType === Node.ELEMENT_NODE && element.tagName.toLowerCase() == newVDom.tag) {
        return true;
    }

    return false;
}

爲了方便屬性的比較,提升效率,咱們將VD的props存在dom元素的__preprops_字段中:

const ATTR_KEY = '__preprops_';

// 建立dom元素
function createElement(vdom) {
    // 若是vdom是字符串或者數字類型,則建立文本節點,好比「Hello World」
    if (typeof vdom === 'string' || typeof vdom === 'number') {
        return doc.createTextNode(vdom);
    }

    const {tag, props, children} = vdom;

    // 1. 建立元素
    const element = doc.createElement(tag);

    // 2. 屬性賦值
    setProps(element, props);

    // 3. 建立子元素
    children.map(createElement)
            .forEach(element.appendChild.bind(element));

    return element;
}

// 屬性賦值
function setProps(element, props) {
     // 屬性賦值
    element[ATTR_KEY] = props;

    for (let key in props) {
        element.setAttribute(key, props[key]);
    }
}

進行屬性比較的時候再取出來:

// 比較props的變化
function diffProps(newVDom, element) {
    let newProps = {...element[ATTR_KEY]};
    const allProps = {...newProps, ...newVDom.props};

    // 獲取新舊全部屬性名後,再逐一判斷新舊屬性值
    Object.keys(allProps).forEach((key) => {
            const oldValue = newProps[key];
            const newValue = newVDom.props[key];

            // 刪除屬性
            if (newValue == undefined) {
                element.removeAttribute(key);
                delete newProps[key];
            } 
            // 更新屬性
            else if (oldValue == undefined || oldValue !== newValue) {
                element.setAttribute(key, newValue);
                newProps[key] = newValue;
            }
        }
    )

    // 屬性從新賦值
    element[ATTR_KEY] = newProps;
}

經過這種方式,咱們再也不須要用變量preVDom將上一次生成的VD存下來,而是直接跟真實的dom進行比較,靈活性更強。

4、總結

本文基於上一個版本的代碼,簡化了頁面渲染的過程(省略patch對象),同時提供了更靈活的VD比較方法(直接跟dom比較),可用性愈來愈強了。基於當前這個版本的代碼還能作怎樣的優化呢,請看下一篇的內容:你不知道的Virtual DOM(四):key的做用

P.S.: 想看完整代碼見這裏,若是有必要建一個倉庫的話請留言給我:代碼

相關文章
相關標籤/搜索