虛擬DOM的實現
使用虛擬DOM的原因: 減少回流與重繪
將DOM結構轉換成對象保存到內存中
<img /> => { tag: 'img'}
文本節點 => { tag: undefined, value: '文本節點' }
<img title="1" class="c" /> => { tag: 'img', data: { title = "1", class="c" } }
<div><img /></div> => { tag: 'div', children: [{ tag: 'div' }]}
根據上面可以寫出虛擬DOM的數據結構
class VNode {constructor(tag, data, value, type) {this.tag = tag && tag.toLowerCase()this.data = datathis.value = valuethis.type = typethis.children = []}appendChild(vnode){this.children.push(vnode)}
}
可能用到的基礎知識
- 判斷元素的節點類型:
node.nodeType
let nodeType = node.nodeType
if(nodeType == 1) {// 元素類型
} else if (nodeType == 3) {// 節點類型
}
- 獲取元素類型的標簽名和屬性 && 屬性中具體的鍵值對,保存在一個對象中
let nodeName = node.nodeName // 標簽名
let attrs = node.attributes // 屬性
let _attrObj = {} // 保存各個具體的屬性的鍵值對,相當于虛擬DOM中的data屬性
for(let i =0, len = attrs.length; i< len; i++){_attrObj[attrs[i].nodeName] = attrs[i].nodeValue
}
- 獲取當前節點的子節點
let childNodes = node.childNodes
for(let i = 0, len = childNodes.length; i < len; i++){console.log(childNodes[i])
}
算法思路
- 使用
document.querySelector
獲取要轉換成虛擬DOM的模板 - 使用
nodeType
方法來獲取是元素類型還是文本類型 - 若是元素類型
- 使用
nodeName
獲取標簽名 - 使用
attributes
獲取屬性名,并將具體的屬性保存到一個對象_attrObj
中 - 創建虛擬DOM節點
- 考慮元素類型是否有子節點,使用遞歸,將子節點的虛擬DOM存入其中
- 使用
- 若是文本類型
- 直接創建虛擬DOM,不需要考慮子節點的問題
// 虛擬DOM的數據結構
class VNode{constrctor(tag, data, value, type){this.tag = tag && tag.toLowerCase()this.data = datathis.value = valuethis.type = typethis.children = []}appendChild(vnode) {this.children.push(vnode)}
}// 獲取要轉換的DOM結構
let root = document.querySelector('#root')
// 使用getVNode方法將 真實的DOM結構轉換成虛擬DOM
let vroot = getVNode(root)
以上寫了虛擬DOM的數據結構,以及使用getVNode
方法將真實DOM結構轉換成虛擬DOM,下面開始逐步實現getVNode方法
- 判斷節點類型,并返回虛擬DOM
function getVNode(node){// 獲取節點類型let nodeType = node.nodeType;if(nodeType == 1){// 元素類型: 獲取其屬性,判斷子元素,創建虛擬DOM} else if(nodeType == 3) {// 文本類型: 直接創建虛擬DOM}let _vnode = null;return _vnode
}
- 下面根據元素類型和文本類型分別創建虛擬DOM
if(nodeType == 1){// 標簽名let tag = node.nodeName// 屬性let attrs = node.attributes/*屬性轉換成對象形式: <div title ="marron" class="1"></div>{ tag: 'div', data: { title: 'marron', class: '1' }}*/let _data = {}; // 這個_data就是虛擬DOM中的data屬性for(let i =0, len = attrs.length; i< attrs.len; i++){_data[attrs[i].nodeName] = attrs[i].nodeValue}// 創建元素類型的虛擬DOM_vnode = new VNode(tag, _data, undefined, nodeType)// 考慮node的子元素let childNodes = node.childNodesfor(let i =0, len = childNodes.length; i < len; i++){_vnode.appendChild(getVNode(childNodes[i]))}
}
// 接下來考慮文本類型
else if(nodeType == 3){_vnode = new VNode(undefined, undefined, node.nodeValue, nodeType)
}
總體代碼
class VNode {constructor(tag, data, value, type) {this.tag = tag && tag.toLowerCase()this.data = datathis.value = valuethis.type = typethis.children = []}appendChild(vnode){this.children.push(vnode)}
}function getVNode(node) {let nodeType = node.nodeTypelet _vnode = nullif (nodeType == 1) {let tag = node.nodeNamelet attrs = node.attributeslet _data = {}for (let i = 0, len = attrs.length; i < len; i++) {_data[attrs[i].nodeName] = attrs[i].nodeValue}_vnode = new VNode(tag, _data, undefined, nodeType)let childNodes = node.childNodesfor (let i = 0, len = childNodes.length; i < len; i++) {_vnode.appendChild(getVNode(childNodes[i]))}} else if (nodeType == 3) {_vnode = new VNode(undefined, undefined, node.nodeValue, nodeType)}return _vnode
}let root = document.querySelector('#root')
let vroot = getVNode(root)
console.log(vroot)
將虛擬DOM轉換成真實的DOM結構
此過程就是上面的反過程
可能用到的知識點
- 創建文本節點
document.createTextNode(value)
- 創建元素節點
document.createElement(tag)
- 給元素節點添加屬性
node.setAttribute(attrName, attrValue)
- 給元素節點添加子節點
node.appendChild(node)
算法思路
- 虛擬DOM的結構中,元素的節點類型存儲在type中,根據type可以判斷出是文本節點還是元素節點
- 若為文本節點,直接返回一個文本節點
return document.createTextNode(value)
- 若為元素節點
- 創建一個node節點:
_node = document.createElement(tag)
- 遍歷虛擬DOM中的data屬性,將其中的值賦給node節點
- 給當前節點添加子節點
- 創建一個node節點:
具體實現
function parseVNode(vnode){let type = vnode.typelet _node = nullif(type == 3){return document.createTextNode(vnode.value)} else if (type == 1){_node = document.createElement(vnode.tag)let data = vnode.datalet attrName,attrValueObject.keys(data).forEach(key=>{attrName = keyattrValue = data[key]_node.setAttribute(attrName, attrValue)})// 考慮子元素let children = vnode.childrenchildren.forEach( subvnode =>{_node.appendChild(parseVNode(subvnode))})}return _node
}
驗證:
let root = querySelector('#root')
let vroot = getVNode(root)
console.log(vroot)
let root1 = parseVNode(vroot)
console.log(root1)