一、什么是虚拟DOM
简单来说,虚拟DOM是对真实DOM的一层抽象,本质是Javascript的对象,他用对象的属性描述节点,最终通过一系列操作转换为真实DOM。
一般来说,虚拟DOM包含标签名 (tag)、属性 (data) 、文本(text)、子元素对象 (children)以及对应的真实DOM(elm)。
export default function (sel, data, children, text, elm) {
return {
sel,
data,
children,
text,
elm
}
}
二、为什么要使用虚拟DOM
因为直接操作真实DOM的代价是昂贵的,会导致页面的重绘和汇流。比如,当你在一次操作时,需要更新10个DOM节点,浏览器没这么智能,收到第一个更新DOM请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程。
通过VNode,同样更新10个DOM节点,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地的一个js对象中,最终将这个js对象一次性attach到DOM树上,避免大量的无谓计算。总结:
- 通过diff算法,减少JS操作真实DOM带来的性能消耗;
- 抽象了原本的渲染过程,带来了跨平台能力,因此不只浏览器原来的DOM,安卓、 IOS 的原生组件甚至小程序,各种GUI都可以用来渲染。
三、如何实现虚拟DOM
首先创建一个vnode函数vnode.js:
export default function (sel, data, children, text, elm) {
return {
sel, //TAG名
data, //属性
children, //子DOM
text, //文本
elm //真实DOM
}
}
h函数h.js,传入三个参数:
import vnode from './vnode.js'
export default function (sel, data, params) {
if (typeof params == 'string') { //文本节点
return vnode(sel, data, undefined, params, undefined)
} else if (Array.isArray(params)) { //含有子节点
let children = []
for (let item of params) {
children.push(item)
}
return vnode(sel, data, children, undefined, undefined)
}
}
创建真实DOM的函数createElement.js:
export default function createElement(vnode) {
let domNode = document.createElement(vnode.sel) //获取真实DOM
if (vnode.children === undefined) {
domNode.innerText = vnode.text //无子节点,直接赋值
} else if (Array.isArray(vnode.children)) {
for (let child of vnode.children) {
let childDom = createElement(child) // 递归获取子节点的真实DOM
domNode.appendChild(childDom) //添加子节点
}
}
vnode.elm = domNode //重新赋值真实DOM
return domNode
}
四、patch函数
patch函数主要用于判断两个节点是否为同一节点:
import vnode from "./vnode.js"
import createElement from "./createElement.js"
import patchVnode from './patchVnode.js'
export default function (oldvNode, newvNode) {
// 把真实dom变成虚拟dom
if (oldvNode.sel == undefined) {
oldvNode = vnode(
oldvNode.tagName.toLowerCase(), //sel
{}, //data
[], //children
oldvNode.innerText, // text
oldvNode // 真实节点
)
}
// 判断新旧节点是否为同一个节点
if (oldvNode.sel === newvNode.sel) {
// 如果是同一节点,则要比较子节点,用到diff算法
patchVnode(oldvNode, newvNode)
} else {
// 不是同一节点,则暴力删除、创建
let newvNodeElm = createElement(newvNode) //将虚拟DOM转换为真实DOM
let oldvNodeElm = oldvNode.elm //获取节点
// 创建新节点,并插入页面
if (newvNodeElm) {
oldvNodeElm.parentNode.insertBefore(newvNodeElm, oldvNodeElm)
}
// 删除旧节点
oldvNodeElm.parentNode.removeChild(oldvNodeElm)
}
}
patchVnode.js:
import createElement from "./createElement.js"
export default function patchVnode(oldVnode, newVnode) {
// 情况1:新节点没有childrem ==> 直接替换文本
if (newVnode.children === undefined) {
if (newVnode.text !== oldVnode.text) {
oldVnode.elm.innerText = newVnode.text
}
} else {
// console.log(oldVnode.children.length)
// 情况2:新节点、旧节点都有children ==> diff算法
if (oldVnode.children.length > 0) {
}
// 情况3:新节点有children、旧节点没有children ==> 清空旧节点、添加新节点
else if (oldVnode.children.length == 0) {
// 删除原来的文本
oldVnode.elm.innerHTML = ''
// 添加child节点
for (let child of newVnode.children) {
let childDom = createElement(child)
oldVnode.elm.appendChild(childDom)
}
}
}
}