/* 實現 */ 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"]) ]);
做用:將虛擬dom轉化爲真實Domjavascript
/* 將虛擬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; }
做用:將真實dom渲染到對應位置css
/* 將真實Dom放到target容器 */ function renderDom(el, target) { target.appendChild(el); }
經過js層的計算,返回一個patch對象,解析patch從新渲染更改部分java
{type:'ATTRS',attrs:{class:'list2'}}
{type:'REMOVE',index:xxx}
{type:'REPLACE',newNode:newNode}
{type:'TEXT',text:"aaa"}
let allPatches;/* 補丁 */ let Index;/* 樹的結點編號 */ /* 頂層 */ function diff(oldTree, newTree) { /* 初始化 */ allPatches = {}; Index = 0; /* 遍歷結點 */ walk(oldTree, newTree, Index); /* 返回補丁 */ return allPatches; }
/* 判斷是否爲文本 */ 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; }
/* 比對屬性 */ 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; }
/* 比對子元素 */ function diffChildren(oldChild, newChild) { oldChild.forEach((child, idx) => { /* 遞歸比對 Index是子元素的索引 */ walk(child, newChild[idx], ++Index) }) }
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); } }
/* 將補丁應用到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; } }) }