文章目录
真实dom结构
在学习虚拟DOM之前,先来了解一下真实的DOM结构,这里不得不提的是关于浏览器渲染方面的知识。
当浏览器拿到一个HTML文件,首先会根据HTML文件构建出一个DOM树来,并行加载CSS文件,图片,JS脚本,值得注意的是DOM树的渲染和CSSOM渲染是并行执行的,而不是串行进行的。(JS脚本需要在html尾部加载,或是写入window.onload方法里,让DOM加载完成后再去加载JS脚本,防止进程阻塞,JS脚本是同步加载的)。
在构建完DOM树和CSSOM树之后,即可开始Render Tree即渲染树的构建,进行布局,绘制。
关于真实dom结构的详情,可以看我的另一篇博文,相信看完之后,对virtualdom会有一个更清晰的认知。
《关于实现一个函数把真实dom转换成虚拟dom原来是这么一回事》
创建虚拟dom的思路
vue中,虚拟dom创建时,使用的相关操作如下,这里用的是网上的图。
将虚拟DOM映射为真实DOM
确定VNode的结构
class Element {
constructor(tagName, props, children) {
this.tagName = tagName
this.props = props
this.children = children
}
}
创建一个结点的方法
/**
* 创建一个结点
* @param {*} type
* @param {*} props
* @param {*} children
*/
function createElement(type, props, children) {
return new Element(type, props, children);
}
给结点设置properties的方法
/**
* 给结点设置properties
* @param {*} node
* @param {*} prop
* @param {*} value
*/
function setAttrs(node, prop, value) {
switch (prop) {
case 'value':
if (node.tagName == 'INPUT' || node.tagName === 'TEXTAREA') {
node.value = value;
}
else {
node.setAttribute(prop, value);
}
break;
case 'style':
node.style.cssText = value;
break;
default:
node.setAttribute(prop, value);
break;
}
}
将一个virtualdom转换成真实dom对象
/**
* 将一个virtualdom转换成真实dom对象 这里使用了深度优先DFS算法进行节点的遍历。
* @param {*} vDom
*/
function render(vDom) {
const { tagName, props, children } = vDom
const el = document.createElement(tagName);
for (let key in props) {
setAttrs(el, key, props[key]);
}
children.map((c)=>{
if(c instanceof Element){
c = render(c);
}else{
c = document.createTextNode(c);
}
el.appendChild(c)
})
return el;
}
将一个节点添加到root元素上
/**
* 将一个节点添加到root元素上
* @param {*} rDom
* @param {*} rootEl
*/
function renderDom(rDom, rootEl){
rootEl.appendChild(rDom);
}
测试
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
</div>
<script>
class Element {
constructor(tagName, props, children) {
this.tagName = tagName
this.props = props
this.children = children
}
}
/**
* 创建一个结点
* @param {*} type
* @param {*} props
* @param {*} children
*/
function createElement(type, props, children) {
return new Element(type, props, children);
}
/**
* 给结点设置properties
* @param {*} node
* @param {*} prop
* @param {*} value
*/
function setAttrs(node, prop, value) {
switch (prop) {
case 'value':
if (node.tagName == 'INPUT' || node.tagName === 'TEXTAREA') {
node.value = value;
} else {
node.setAttribute(prop, value);
}
break;
case 'style':
node.style.cssText = value;
break;
default:
node.setAttribute(prop, value);
break;
}
}
/**
* 将一个virtualdom转换成真实dom对象
* @param {*} vDom
*/
function render(vDom) {
const {
tagName,
props,
children
} = vDom
const el = document.createElement(tagName);
for (let key in props) {
setAttrs(el, key, props[key]);
}
children.map((c) => {
if (c instanceof Element) {
c = render(c);
} else {
c = document.createTextNode(c);
}
el.appendChild(c)
})
return el;
}
/**
* 将一个节点添加到root元素上
* @param {*} rDom
* @param {*} rootEl
*/
function renderDom(rDom, rootEl) {
rootEl.appendChild(rDom);
}
// 开始测试
const rootEl = document.getElementById("app")
const vDom = createElement(
'ul', {
class: 'list',
style: 'width:300px;height:300px;background-color:orange'
},
[
createElement(
'li', {
class: 'item',
data_id: 0,
},
[
createElement(
'p', {
class: 'text',
},
["第一个列表项"]
)
]
),
createElement(
'li', {
class: 'item',
data_id: 1,
},
[
createElement(
'p', {
class: 'text',
},
[
createElement(
'span', {
class: "title"
},
[
'第二个列表项'
]
)
]
)
]
),
createElement(
'li', {
class: "item",
data_id: 2
},
[
'第三个列表项'
]
)
]
)
trueDom = render(vDom)
renderDom(trueDom, rootEl)
</script>
</body>
</html>