上一篇描述了什么是虚拟DOM。
在React和Vue中,虚拟DOM的创建都是由模板或者JSX来完成的。但是由模板变成render或者JSX完成虚拟DOM的创建都是由webpack的loader来完成。
我们现在就用原生的方法去完成虚拟DOM是如何去新建和渲染的。
假设我们要生成下面这样一个虚拟DOM
<div id="test">
<p>节点1</p>
</div>
1.我们新建一个"vdom.js"文件,新建createElement函数,这个函数就是用来创建虚拟DOM。
思路:
1、DOM一般由三部分组成:1.标签 2.标签属性 3.子节点
2、我们创建一个函数,传入三个参数:tag,data,children
3.我们判断tag是什么类型的,并记录,可分为HTML,COMPONENT,TEXT等。我们这次只说HTML和TEXT
4.我们判断children是什么类型的,并记录,可分为EMPTY(无),SINGLE(单个),MULTIPE(多个)
5.如果children为文本,我们创建children为文本标签
6.以对象的形式返回这些数据
代码:
//虚拟DOM的类型
const vnodeType = {
HTML: 'HTML',
TEXT: 'TEXT',
COMPONENT: 'COMPONENT'
}
//子节点的类型
const childType = {
EMPTY: 'EMPTY',
SINGLE: 'SINGLE',
MULTIPLE: 'MULTIPLE'
}
// 创建虚拟DOM
// 三个参数 tag(标签名),data:属性值,children: 子节点,默认是null
function createElement(tag, data, children=null) {
// 记录vnode的类型
let flag
// 如果是string,如'div',我们就认为是普通节点HTML
if (typeof tag === 'string') {
flag = vnodeType.HTML
} else if (typeof tag === 'function') {
// 如果是function,我们就认为是组件
flag = vnodeType.COMPONENT
} else {
// 其他的默认是文本类型
flag = vnodeType.TEXT
}
// 记录字节点类型
let childrenFlag
// 如果为空,说明没有字节点
if (children === null) {
childrenFlag = childType.EMPTY
} else if (Array.isArray(children)) {
// 如果它是一个数组,根据长度判断,如果为0,认为没有子节点,否则认为有多个节点
const lenght = children.length
if (lenght === 0) {
childrenFlag = childType.EMPTY
} else {
childrenFlag = childType.MULTIPLE
}
} else {
// 其他情况,都默认是文本
childrenFlag = childType.SINGLE
children = createTextVnode(children + '')
}
// 返回虚拟DOM
return {
flag, //vnode的类型
tag, // 标签,div ,文本没有tag,组件就是一个函数
data, // 属性
children, // 子节的
childrenFlag,// 子节点类型
el: null
}
}
//新建文本类型的vnode
function createTextVnode (text) {
//文本节点的tag 为null,且它没有子节点
return {
flag: vnodeType.TEXT,
tag: null,
data: null,
children: text,
childrenFlag: childType.EMPTY
}
}
2.我们在"index.html"中调用以上函数
代码:
<body>
<script src="./vdom.js">script>
<script>
let div = createElement('div', {
id: 'test'}, [
createElement('p', {
}, '节点1')
])
console.log(JSON.stringify(div, null, 2))
script>
body>
3、我们控制台打印出来
结果以对象的形式展示出来了,最终完成了虚拟DOM的新建。
1.渲染分为首次渲染和非首次渲染
我们将上述虚拟节点变得复杂一些。我们新增了多个子节点,每个子节点含有key属性及其他属性。
let vnode = createElement('div', {
id: 'test'}, [
createElement('p', {
key: 'a', style: {
color: 'blue'}}, '节点1'),
createElement('p', {
key: 'b', '@click': () => {
alert('节点2')}}, '节点2'),
createElement('p', {
key: 'c', 'class':'item-header' }, '节点3'),
createElement('p', {
key: 'd'}, '节点4'),
])
// 执行渲染函数
render(vnode, document.getElementById('app'))
现在我们需要将它渲染到页面上。
思路:
1.我们在“vdom.js”中新建一个render函数,参数为要渲染的虚拟节点和要渲染到哪个节点中的元素(盒子)
2.判断它是首次渲染,还是非首次渲染。(我们现在只写首次渲染)
3.调用首次渲染函数mount函数,将要渲染的虚拟节点和盒子传入
4.新建mount函数,判断要渲染的vnode的类型,如果是节点类型,调用mountElement函数,如果是文本类型,调用mountText函数。
5.新建mountElement函数,根据vnode的tag新建一个虚拟dom,并将dom赋值给el
6.遍历data,渲染属性
7.判断vnode的子节点类型,如果是单个节点,直接调用mount函数递归,参数为子节点和dom,如果是多节点,遍历子节点递归
8.将dom添加盒子中
9.新建mountText函数,创建一个文本节点dom,并记录el
10.将dom添加盒子中
这样我们就完成了首次渲染
代码:
function render(vnode, container) {
// 首次渲染
mount(vnode, container)
}
// 首次渲染
function mount(vnode, container) {
// 得到vnode的类型
const {
flag } = vnode
// 如果是节点类型,执行mountElement
if (flag === vnodeType.HTML) {
mountElement(vnode, container)
} else if (flag === vnodeType.TEXT) {
// 如果是文本类型,执行mountText
mountText(vnode, container)
}
}
function mountElement(vnode, container) {
// 根据tag新建dom元素
const dom = document.createElement(vnode.tag)
// 赋值给el
vnode.el = dom
// 结构出 data, children, childrenFlag
const {
data, children, childrenFlag } = vnode
// 挂载属性
if (data) {
for (let key in data) {
// 传入4个参数,当前节点,属性名, 上个属性值,这此属性值,因为是首次渲染,所以perv为null
patchData(dom, key, null, data[key])
}
}
// 挂载子节点
if (childrenFlag !== childType.EMPTY) {
if (childrenFlag === childType.SINGLE) {
// 递归
mount(children, dom)
} else if (childrenFlag === childType.MULTIPLE) {
for(let i = 0; i < children.length; i++) {
// 递归
mount(children[i], dom)
}
}
}
// 添加到 container 中
container.appendChild(dom)
}
function patchData(el, key, perv, next) {
switch(key) {
case 'style':
for (let i in next) {
el.style[i] = next[i]
}
break
case 'class':
el.className = next
break
default:
if (key[0] === '@') {
el.addEventListener(key.slice(1), next)
} else {
el.setAttribute(key, next)
}
}
}
function mountText(vnode, container) {
// 创建文本节点
const dom = document.createTextNode(vnode.children)
// 记录el
vnode.el = dom
// 添加到 container 中
container.appendChild(dom)
}