虚拟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)