虛擬DOM和diff算法


參考:

https://juejin.im/post/5a3200fe51882554bd5111a0

https://www.cnblogs.com/zhuzhenwei918/p/7271305.html

https://juejin.im/post/5ad6182df265da23906c8627

虛擬DOM

React將DOM抽象為虛擬DOM, 然后通過新舊虛擬DOM 這兩個對象的差異(Diff算法),最終只把變化的部分重新渲染,提高渲染效率的過程; diff 是通過JS層面的計算,返回一個patch對象,即補丁對象,在通過特定的操作解析patch對象,完成頁面的重新渲染

img

img

表示虛擬DOM的JS對象上面有如下三個屬性:

  1. tagName: 用來表示這個元素的標簽名。
  2. props: 用來表示這元素所包含的屬性。
  3. children: 用來表示這元素的children(數組)。

虛擬DOM快取決於兩個前提:1. JavaScript在瀏覽器上運行很快 2.DOM的渲染過程很慢,性能消耗高

用算法實現虛擬DOM:

function VDOM(tagName, props, children) {
    this.tagName = tagName;
    this.props = props;
    this.children = children;
}
VDOM.prototype.render = function() {
    // 建立一個真實元素
    var el = document.createElement(this.tagName);
    
    // 往該元素上添加屬性
    for (var name in this.props) {
        el.setAttribute(name, this.props[name]);
    }
    
    // children是一個數組
    var children = this.children || [];
    for (var child of children) {
        var childEl = (child instanceof VDOM) ? child.render() : document.createTextNode(child);
        // 無論childEl是元素還是文字節點,都需要添加到這個元素中。
        el.appendChild(childEl);
    }
    return el;
}

Diff算法

比較兩顆DOM數的差異是Virtual DOM算法中最為核心的部分,這也就是所謂的Virtual DOM的diff算法。 兩個樹的完全的diff算法是一個時間復雜度為 O(n3) 的問題。 但是在前端中,你會很少跨層地移動DOM元素,所以真實的DOM算法會對同一個層級的元素進行對比。

img

上圖中,div只會和同一層級的div對比,第二層級的只會和第二層級對比。 這樣算法復雜度就可以達到O(n)。

img

上面的這個遍歷過程就是深度優先,即深度完全完成之后,再轉移位置。 在深度優先遍歷的時候,每遍歷到一個節點就把該節點和新的樹進行對比,如果有差異的話就記錄到一個對象里面。

diff.js 的簡單代碼實現:

在同層進行比較時候會出現四種情況:

1、此節點是否被移除 -> 添加新的節點
2、屬性是否被改變 -> 舊屬性改為新屬性
3、文本內容被改變-> 舊內容改為新內容
4、節點要被整個替換 -> 結構完全不相同 移除整個替換

最后返回一個patch對象用來應用到實際的DOM tree更新,它的結構是這樣的:

// index記錄是哪一層的改變,type表示是哪種變化,第二個屬性對應着變化存儲相應的內容
patches = {index:[{type: utils.REMOVE/utils.TEXT/utils.ATTRS/utils.REPLACE, index/content/attrs/node: }, ...], ...}
let utils = require('./utils');

let keyIndex = 0;
function diff(oldTree, newTree) {
    //記錄差異的空對象。key就是老節點在原來虛擬DOM樹中的序號,值就是一個差異對象數組
    let patches = {};
    keyIndex = 0; // 兒子要起另外一個標識
    let index = 0; // 父親的表示 1 兒子的標識就是1.1 1.2
    walk(oldTree, newTree, index, patches);
    return patches;
}
//遍歷
function walk(oldNode, newNode, index, patches) {
    let currentPatches = []; //這個數組里記錄了所有的oldNode的變化
    if (!newNode) { //如果新節點沒有了,則認為此節點被刪除了
        currentPatches.push({type: utils.REMOVE, index});
        //如果說老節點的新的節點都是文本節點的話
    } else if (utils.isString(oldNode) && utils.isString(newNode)) {
        //如果新的字符符值和舊的不一樣
        if (oldNode != newNode) {
            ///文本改變
            currentPatches.push({type: utils.TEXT, content: newNode});
        }
    } else if (oldNode.tagName == newNode.tagName) {
        //比較新舊元素的屬性對象
        let attrsPatch = diffAttr(oldNode.attrs, newNode.attrs);
        //如果新舊元素有差異 的屬性的話
        if (Object.keys(attrsPatch).length > 0) {
            //添加到差異數組中去
            currentPatches.push({type: utils.ATTRS, attrs: attrsPatch});
        }
        //自己比完后再比自己的兒子們
        diffChildren(oldNode.children, newNode.children, index, patches, currentPatches);
    } else {
        currentPatches.push({type: utils.REPLACE, node: newNode});
    }
    if (currentPatches.length > 0) {
        patches[index] = currentPatches;
    }
}
//老的節點的兒子們 新節點的兒子們 父節點的序號 完整補丁對象 當前舊節點的補丁對象
function diffChildren(oldChildren, newChildren, index, patches, currentPatches) {
    oldChildren.forEach((child, idx) => {
        walk(child, newChildren[idx], ++keyIndex, patches);
    });
}
function diffAttr(oldAttrs, newAttrs) {
    let attrsPatch = {};
    for (let attr in oldAttrs) {
        //如果說老的屬性和新屬性不一樣。一種是值改變 ,一種是屬性被刪除 了
        if (oldAttrs[attr] != newAttrs[attr]) {
            attrsPatch[attr] = newAttrs[attr];
        }
    }

    // 對比舊節點新增的屬性
    for (let attr in newAttrs) {
        if (!oldAttrs.hasOwnProperty(attr)) {
            attrsPatch[attr] = newAttrs[attr];
        }
    }
    return attrsPatch;
}
module.exports = diff;

patch.js的簡單實現

let keyIndex = 0;
let utils = require('./utils');
let allPatches;//這里就是完整的補丁包
function patch(root, patches) {
    allPatches = patches;
    walk(root);
}
function walk(node) {
    let currentPatches = allPatches[keyIndex++];
    (node.childNodes || []).forEach(child => walk(child));
    if (currentPatches) {
        doPatch(node, currentPatches);
    }
}
function doPatch(node, currentPatches) {
    currentPatches.forEach(patch => {
        switch (patch.type) {
            case utils.ATTRS:
                for (let attr in patch.attrs) {
                    let value = patch.attrs[attr];
                    if (value) {
                        utils.setAttr(node, attr, value);
                    } else {
                        node.removeAttribute(attr);
                    }
                }
                break;
            case utils.TEXT:
                node.textContent = patch.content;
                break;
            case utils.REPLACE:
                let newNode = (patch.node instanceof Element) ? path.node.render() : document.createTextNode(path.node);
                node.parentNode.replaceChild(newNode, node);
                break;
            case utils.REMOVE:
                node.parentNode.removeChild(node);
                break;
        }
    });
}
module.exports = patch;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM