實現一個簡單的DOM diff算法

上一篇文章,我說到了如何實現一個簡單的虛擬DOM,這一篇文章是接着上一篇文章的知識點的。node

咱們都知道虛擬DOM其實就是JS對象,咱們用JS來操做對象比操做DOM性能要好得多算法

咱們爲何還須要diff算法?

  • 由於若是咱們有一個很龐大的DOM Tree,咱們要對它進行更新操做,若是咱們只是更新了它很小的一部分,咱們就須要更新整個DOM Tree。這也是很浪費性能和資源的。由於有好多無用的更新。因此咱們才須要diff算法來剔除掉無用的更新

我先來簡單的歸納一下diff算法。

  1. 它遵循先序深度優先遍歷的規則。
  2. 同級比較。
  3. diff只是找到差別,找到了差別咱們還有修補差別,就是打補丁(patch)。
  4. 咱們diff的過程其實就是對比兩個虛擬DOM的過程。經過對比找到patch(差別)。
  5. 而後再把patch打到真實的DOM上(打補丁)。
  • 咱們先來講一下先序深度優先遍歷,我以爲學過數據結構的人對它都不是很陌生,這是一種對樹的遍歷方法。(先序、中序、後序,廣度優先、深度優先)。 先序遍歷就是先遍歷根節點在遍歷左邊的孩子節點而後是右邊的孩子節點(根→左→右)。

diff的實現過程

  • 首先準備好咱們須要的變量: 咱們須要一個補丁對象和一個全局的位置索引(遍歷的順序)
let patches = {};
let index = 0;
複製代碼
  • 兩個虛擬DOM的不一樣主要有
    • 文本的不一樣("a" → "bb")
    • 屬性的不一樣(class: .a → .b)
    • 刪除
    • 替換

因此咱們須要創建四個標識符。bash

const ATTR = 0; // 屬性
const TEXT = 1; // 文本
const REMOVE = 2; // 刪除
const REPLACE = 3; // 替換
複製代碼
  1. 先建立兩個不一樣的虛擬DOM。
let vDom1 = createElement("div", {class: "div"}, [
            	createElement("div", {class: "div"}, ["a"])
            ]);
let vDom2 = createElement("div", {class: "div1"}, [
                createElement("div", {class: "div2"}, ["b"])
            ]);

複製代碼

2. 而後比較兩個虛擬DOM的差別(diff過程) diff(vDom1, vDom2)

function diff(oldTree, newTree){
    walk(oldTree, newTree, index); // 遍歷兩個虛擬DOM樹
}
複製代碼
  1. 遍歷的過程
  • 文本的不一樣
// 我這裏使用了不是很準確的比較(可使用toString.call)
// 若是都是文本
// patch是補丁對象
// 數據描述: {type: 不一樣的類型,不一樣的地方}
if((typeof oldNode === "string") && (typeof newNode === "string")){
	// 若是文本內容不同
	if(newNode !== oldNode){
		patch.push({type: TEXT, text: newNode});
	}
}
複製代碼
  • 屬性的不一樣
// 若是類型相同就比較屬性, 類型不相同默認換掉了整個元素
if(oldNode.type === newNode.type){
    // 遍歷新老節點屬性的不一樣
    let attr = diffAttr(oldNode.props, newNode.props);
    // 若是有不一樣, 就加入patch中
    if (Object.keys(attr).length > 0) {
        patch.push({ type: ATTR, attr });
    }
    // 遍歷子節點
    diffChildren(oldNode.children, newNode.children);
}
複製代碼

遍歷屬性數據結構

function diffAttr(oldAttr, newAttr){
    let attr = {};
    // 看兩個屬性是否不一樣(修改)
    for (key in oldAttr) {
        	if(oldAttr[key] != newAttr[key]){
        	    attr[key] = newAttr[key];
        	}
	}
	// 是否新增
	for (key in newAttr) {
        	if(!oldAttr.hasOwnProperty(key)){
        	    attr[key] = newAttr[key];
        	}
	}
    return attr;
}
複製代碼

遍歷子節點的屬性dom

function diffChildren(oldChildren, newChildren){
    oldChildren.forEach(function(child, i){
        // 子節點遞歸遍歷屬性
    	walk(child, newChildren[i], ++ index);
    });
}
複製代碼
  • 刪除
// 若是沒有新節點,說明刪除了,標記處刪除的索引
if(!newNode){
    patch.push({type: REMOVE, index});
}
複製代碼
  • 替換
// 其他狀況爲替換
patch.push({type: REPLACE, newNode});
複製代碼
  • 總體代碼
let patches = {};
let index = 0;

const ATTR = 0;
const TEXT = 1;
const REMOVE = 2;
const REPLACE = 3;

function diff(oldTree, newTree){
    walk(oldTree, newTree, index);
}

function walk(oldNode, newNode, index){
    let patch = [];
    // 刪除
    if(!newNode){
    	patch.push({type: REMOVE, index});
    // 文本
    }else if((typeof oldNode === "string") && (typeof newNode === "string")){
    	if(newNode !== oldNode){
    	    patch.push({type: TEXT, text: newNode});
    	}
    }else if(oldNode.type === newNode.type){
        // 屬性
    	let attr = diffAttr(oldNode.props, newNode.props);
    	 if (Object.keys(attr).length > 0) {
            patch.push({ type: ATTR, attr });
        }
    	diffChildren(oldNode.children, newNode.children);
    }else {
        // 替換
    	patch.push({type: REPLACE, newNode});
    }
    if(patch.length > 0){
    	patches[index] = patch;
    }
}
// 比較屬性的不一樣
function diffAttr(oldAttr, newAttr){
    let attr = {};
    // 看兩個屬性是否不一樣(修改)
    for (key in oldAttr) {
        	if(oldAttr[key] != newAttr[key]){
        	    attr[key] = newAttr[key];
        	}
	}
	// 是否新增
	for (key in newAttr) {
        	if(!oldAttr.hasOwnProperty(key)){
        	    attr[key] = newAttr[key];
        	}
	}
    return attr;
}
// 比較子節點的屬性
function diffChildren(oldChildren, newChildren){
    oldChildren.forEach(function(child, i){
        walk(child, newChildren[i], ++ index);
    });
}
複製代碼

咱們查看一下補丁對象 post

打補丁(patch)

首先創建一個索引對象let patchIndex = 0;性能

  1. 將補丁對象和真實的DOM做比較patch(dom, patches)
function patch(dom, patches){
    walkPatch(dom);
}
複製代碼

遍歷補丁的實現ui

function walkPatch(dom){
    // 獲取當前節點的補丁
    let patch = patches[patchIndex ++];
    // 獲取子節點
    let children = dom.childNodes;
    // 遍歷子節點
    // 遍歷到最後一個元素,從後往前打補丁
    children.forEach((child)=>walkPatch(child));
    // 若是有補丁,就打補丁
    if(patch){
    	doPatch(dom, patch);
    }
}
複製代碼
  • 打補丁的實現過程
    • 屬性
    // 遍歷屬性
    // key 就是 class或者value(這個value是屬性)
    // value 就是 類名或者是值
    for (key in p.attr) {
        let value = p.attr[key];
        // 若是有值(其實就是上一篇虛擬DOM中的設置屬性)
        if(value){
            if(key === "value"){
    	    if(node.type.toUpperCase() === "INPUT" || node.type.toUpperCase() === "TEXTAREA"){
    	        node.value = value;
    	    }
    	}else {
    		node.setAttribute(key, value);
    	}
    	// 沒有值,就是刪除屬性
        }else {
            node.removeAttribute(key);
        }
    }
    複製代碼
    • 文本
    // 替換文本節點
    node.textContent = p.text;
    複製代碼
    • 刪除
    // 刪除本身
    node.parentNode.removeChild(node);
    複製代碼
    • 替換
    let { newNode } = p;
    // 若是是元素就建立元素不然就是文本
    newNode = (newNode instanceof Element) ?  createDom(newNode): document.createTextNode(newNode);
    // 用新節點替換舊結點
    newNode.parentNode.replaceChild(newNode, node);
    複製代碼

總體代碼spa

function doPatch(node, patch){
    patch.forEach((p)=>{
        switch (p.type) {
            case ATTR:
                // 遍歷屬性
            	for (key in p.attr) {
                    let value = p.attr[key];
                    // 若是有值(其實就是上一篇虛擬DOM中的設置屬性)
                    if(value){
                        if(key === "value"){
                	    if(node.type.toUpperCase() === "INPUT" || node.type.toUpperCase() === "TEXTAREA"){
                	        node.value = value;
                	    }
                	}else {
                		node.setAttribute(key, value);
                	}
                	// 沒有值,就是刪除屬性
                    }else {
                        node.removeAttribute(key);
                    }
            	}
            	break;
        	case TEXT:
        	    // 替換文本節點
        	    node.textContent = p.text;
        	    break;
        	case REMOVE:
        	    // 刪除本身
        	    node.parentNode.removeChild(node);
        	    break;
        	case REPLACE:
        	    let { newNode } = p;
        	    // 若是是元素就建立元素不然就是文本
        	    newNode = (newNode instanceof Element) ?  createDom(newNode): document.createTextNode(newNode);
        	    // 用新節點替換舊結點
        	    newNode.parentNode.replaceChild(newNode, node);
        	    break;
        	default:
        	    break;
        }
    })
}
複製代碼

未打補丁的DOM樹 3d

打完補丁的DOM樹
咱們用一個相對複雜一點的例子來驗證

let vDom1 = createElement("div", {class: "div"}, [
            	createElement("div", {class: "div"}, ["a"]),
            	createElement("div", {}, ["b"]),
            	createElement("div", {class: "div"}, [
        		    createElement("div", {class: "div"}, ["c"]),
        		    createElement("div", {class: "div"}, ["d"])
            	])
            ]);
let vDom2 = createElement("div", {class: "div1"}, [
    	        createElement("div", {class: "div2"}, ["1"]),
    	        createElement("div", {class: "div3"}, ["2"]),
    	        createElement("div", {}, [
    	            createElement("div", {class: "div5"}, ["3"]),
    		        createElement("div", {class: "div6"}, ["4"])
    	        ])
            ]);
複製代碼

打補丁之前的DOM樹

打補丁以後的DOM樹

  • 總結
    • 其實我只是實現了一個很簡單的diff算法,還有好多狀況沒有考慮和實現。好比新增還有兩個節點交換了位置,以及不是同級的比較。
    • 其實我以爲這就是一種思想,重點在於咱們不只會使用它還有學會了解他並慢慢的掌握它。
    • 上邊的屬性,其實沒有包含style的實現。
相關文章
相關標籤/搜索