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