淺學virtualDom和diff算法

淺學virtualDom和diff算法


virtual Dom

創建virtual Dom

/* 實現 */
class Element {
    constructor(type, props, children) {
        this.type = type;
        this.props = props;
        this.children = children;
    }
}
function createElement(type, props, children) {
    return new Element(type, props, children)
}
let virtualDom = createElement("ul", { class: "list" }, [
  createElement("li", { class: "item" }, ["a"]),
  createElement("li", { class: "item" }, ["b"]),
  createElement("li", { class: "item" }, ["c"])
]);

render函數

作用:將虛擬dom轉化爲真實Dom

/* 將虛擬Dom屬性應用到真實Dom */
function setAttr(node, key, value) {
    switch (key) {
        case "value":
            if (node.tagName.toUpperCase() === "INPUT" ||
                node.tagName.toUpperCase() === "TEXTAREA") {
                node.value = value;
            }
            break;
        case "style":
            node.style.cssText = value;
            break;
        default:
            node.setAttribute(key, value);
    }
}
/* 渲染虛擬dom成真實的dom */
function render(vDom) {/* "li", { class: "item" }, ["1"] */
    /* 創建結點 */
    let el = document.createElement(vDom.type);
    /* 增加屬性 */
    for (let key in vDom.props) {
        setAttr(el, key, vDom.props[key]);
    }
    /* 插入子元素 */
    vDom.children.forEach(child => {
        /* 若是Element繼續遞歸render */
        child = (child instanceof Element) ? render(child)
            : document.createTextNode(child);
        el.appendChild(child);
    });
    return el;
}

renderDom

作用:將真實dom渲染到對應位置

/* 將真實Dom放到target容器 */
function renderDom(el, target) {
    target.appendChild(el);
}

diff算法

通過js層的計算,返回一個patch對象,解析patch重新渲染更改部分

差異計算(深度優先遍歷)

  • 用javascript模擬Dom
  • 把虛擬Dom轉成真實Dom插入頁面
  • 如果發生事件更改,比較兩棵樹變化,得到差異對象
  • 把差異應用到真實Dom

比對規則

  • 結點類型相同,比對屬性,產生一個屬性的補丁包{type:'ATTRS',attrs:{class:'list2'}}
  • 結點不存在,補丁包增加{type:'REMOVE',index:xxx}
  • 結點類型不同,直接替換{type:'REPLACE',newNode:newNode}
  • 文本變化,{type:'TEXT',text:"aaa"}

補丁生成代碼實現

  • diff函數初始化補丁和編號
let allPatches;/* 補丁 */
let Index;/* 樹的結點編號 */
/* 頂層 */
function diff(oldTree, newTree) {
    /* 初始化 */
    allPatches = {};
    Index = 0;
    /* 遍歷結點 */
    walk(oldTree, newTree, Index);
    /* 返回補丁 */
    return allPatches;
}
  • walk比對新舊節點,將補丁合併到總補丁
/* 判斷是否爲文本 */
function isString(node) {
    return Object.prototype.toString.call(node) === "[object String]";
}
/* 比對兩個結點 */
function walk(oldNode, newNode, currentIndex) {
    /* 當前補丁 */
    let currentPatches = [];
    /* 結點被刪除 */
    if (!newNode) {
        currentPatches.push({ type: "REMOVE", index: currentIndex })
    }
    /* 文本結點 */
    else if (isString(oldNode) && isString(newNode)) {
        if (oldNode !== newNode)
            currentPatches.push({ type: "TEXT", text: newNode })
    }
    /* 屬性改變 */
    else if (oldNode.type === newNode.type) {
        /* 找出差異屬性 */
        let attrs = diffAttr(oldNode.props, newNode.props);
        if (Object.keys(attrs).length) {
            currentPatches.push({ type: "ATTR", attrs });
        }
        /* 遍歷子元素 */
        diffChildren(oldNode.children, newNode.children);
    }
    /* 結點被替換 */
    else {
        currentPatches.push({ type: "REPLACE", node: newNode })
    }
    /* 將補丁合併到總補丁 */
    if (currentPatches.length)
        allPatches[currentIndex] = currentPatches;
}
  • diffAttr比對結點屬性差異
/* 比對屬性 */
function diffAttr(oldAttr, newAttr) {
    let attrs = {};
    /* 比對變化 */
    for (let key in oldAttr) {
        if (oldAttr[key] !== newAttr[key]) {
            attrs[key] = newAttr[key];
        }
    }
    /* 比對新增屬性 */
    for (let key in newAttr) {
        if (!oldAttr.hasOwnProperty(key)) {
            attrs[key] = newAttr[key];
        }
    }
    return attrs;
}
  • diffChildren比對子元素,設置索引並遞歸調用walk
/* 比對子元素 */
function diffChildren(oldChild, newChild) {
    oldChild.forEach((child, idx) => {
        /* 遞歸比對 Index是子元素的索引 */
        walk(child, newChild[idx], ++Index)
    })
}

應用補丁到真實Dom

  • patch爲入口,將傳入的補丁包放到全局並進入walk函數
let allPatches = {};
let Index = 0;
function patch(el, patches) {
  allPatches = patches;
  Index = 0;
  walk(el);
}
/* 按照索引的順序遍歷孩子 */
function walk(el) {
  let currentPatches = allPatches[Index++];
  let childNodes = el.childNodes;
  /* 深度優先遍歷 */
  childNodes.forEach(child => walk(child));
  /* 自下而上更新 */
  if (currentPatches) {
    doPatch(el, currentPatches);
  }
}
  • doPatch針對不同類型更新結點
/* 將補丁應用到el上 */
function doPatch(el, patches) {
  patches.forEach(patch => {
    switch (patch.type) {/* {type:'TEXT',text:"aaa"} */
      case "TEXT":
        el.textContent = patch.text;
        break;
      case "ATTR":/* {type:'ATTRS',attrs:{class:'list2'}} */
        for (let key in patch.attrs) {
          let value = patch.attrs[key];
          /* 若值爲undefine直接刪除屬性 */
          if (value) setAttr(el, key, value);
          else el.removeAttribute(key);
        }
        break;
      case "REPLACE":/* {type:'REPLACE',newNode:newNode} */
        /* 文本結點特殊處理 */
        let newNode = (patch.newNode instanceof Element) ?
          render(patch.newNode) : document.createTextNode(patch.newNode);
        el.parentNode.replaceChild(newNode, el);
        break;
      case "REMOVE":/* {type:'REMOVE',index:xxx} */
        el.parentNode.removeChild(el);
        break;
    }
  })
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章