虛擬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 = data
this.value = value
this.type = type
this.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 = data
this.value = value
this.type = type
this.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.childNodes
for(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 = data
this.value = value
this.type = type
this.children = []
}
appendChild(vnode){
this.children.push(vnode)
}
}
function getVNode(node) {
let nodeType = node.nodeType
let _vnode = null
if (nodeType == 1) {
let tag = node.nodeName
let attrs = node.attributes
let _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.childNodes
for (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.type
let _node = null
if(type == 3){
return document.createTextNode(vnode.value)
} else if (type == 1){
_node = document.createElement(vnode.tag)
let data = vnode.data
let attrName,attrValue
Object.keys(data).forEach(key=>{
attrName = key
attrValue = data[key]
_node.setAttribute(attrName, attrValue)
})
// 考慮子元素
let children = vnode.children
children.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)