面試官:你瞭解 vue 的diff演算法嗎?

duffy發表於2018-04-17

面試官:你瞭解 vue 的diff演算法嗎?

寫在前面

vue2.0加入了virtual dom,有點向react靠攏的意思。vue的diff位於patch.js檔案中,複雜度為O(n)。 聽大神說了解diff過程可以讓我們更高效的使用框架,工作和女朋友都更加好找了,我們趕快了解哈~。 瞭解diff過程,我們先從虛擬dom開始。

虛擬dom

所謂的virtual dom,也就是虛擬節點。它通過JS的Object物件模擬DOM中的節點,然後再通過特定的render方法將其渲染成真實的DOM節點 dom diff 則是通過JS層面的計算,返回一個patch物件,即補丁物件,在通過特定的操作解析patch物件,完成頁面的重新渲染, 上一張圖讓大家更加清晰點:

面試官:你瞭解 vue 的diff演算法嗎?
到這裡有童鞋可能會問,模擬DOM是幹嘛為什麼要這樣做?虛擬dom對應的是真實dom, 使用document.CreateElement 和 document.CreateTextNode建立的就是真實節點。

我們可以做個試驗。列印出一個空元素的第一層屬性,可以看到標準讓元素實現的東西太多了。如果每次都重新生成新的元素,對效能是巨大的浪費。

var odiv = document.createElement('div');
for(var k in odiv ){
  console.log(k)
}
複製程式碼

看看你的列印臺,有你想要的結果。

實現步驟

  • 用JavaScript物件模擬DOM
  • 把此虛擬DOM轉成真實DOM並插入頁面中
  • 如果有事件發生修改了虛擬DOM
  • 比較兩棵虛擬DOM樹的差異,得到差異物件
  • 把差異物件應用到真正的DOM樹上

程式碼實現

class crtateElement {
    constructor (el, attr, child) {
        this.el = el
        this.attrs = attr
        this.child = child || []
    }
    render () { 
        let virtualDOM =  document.createElement(this.el)
        // attr是個物件所以要遍歷渲染
        for (var attr in this.attrs) {
            virtualDOM.setAttribute(attr, this.attrs[attr])
        }

        // 深度遍歷child
        this.child.forEach(el => {
            console.log(el instanceof crtateElement)
            //如果子節點是一個元素的話,就呼叫它的render方法建立子節點的真實DOM,如果是一個字串的話,建立一個檔案節點就可以了
            // 判斷一個物件是否是某個物件的實力
            let childElement = (el instanceof crtateElement) ? el.render() : document.createTextNode(el);
            virtualDOM.appendChild(childElement);
        });
        return virtualDOM
    }
}
function element (el, attr, child) {
    return new crtateElement(el, attr, child)
}

module.exports = element
複製程式碼

用JavaScript物件結構表示DOM樹的結構;然後用這個樹構建一個真正的DOM樹,插到文件當中

let element = require('./element') 

let myobj = {
    "class": 'big_div'
}
let ul = element('div',myobj,[
    '我是文字',
    element('div',{'id': 'xiao'},['1']),
    element('div',{'id': 'xiao1'},['2']),
    element('div',{'id': 'xiao2'},['3']),
])
console.log(ul)
ul = ul.render()
document.body.appendChild(ul)
複製程式碼

DOM DIFF

比較兩棵DOM樹的差異是Virtual DOM演算法最核心的部分.簡單的說就是新舊虛擬dom 的比較,如果有差異就以新的為準,然後再插入的真實的dom中,重新渲染。、 借網路一張圖片說明:

面試官:你瞭解 vue 的diff演算法嗎?

比較只會在同層級進行, 不會跨層級比較。
比較後會出現四種情況:
1、此節點是否被移除 -> 新增新的節點
2、屬性是否被改變 -> 舊屬性改為新屬性
3、文字內容被改變-> 舊內容改為新內容
4、節點要被整個替換 -> 結構完全不相同 移除整個替換

看diff.js 的簡單程式碼實現,下面都有相應的解釋說明

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;
複製程式碼

其中有個需要注意的是新舊虛擬dom比較的時候,是先同層比較,同層比較完看看時候有兒子,有則需要繼續比較下去,直到沒有兒子。搞個簡單的圖來說明一下吧:

面試官:你瞭解 vue 的diff演算法嗎?
同層比較,比較順序是上面的數字來,把不同的打上標記,放到陣列裡面去,統一交給patch處理。

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;
複製程式碼

說明改天有空補上,歡迎留言。diff部分還有一個key的設定,它可以更高效的利用dom。這個也很重要。時間不早了,改天寫。你的點贊是我不斷的動力。感謝,歡迎拋磚~~

相關文章