古老的渲染方式(innerHTML)
在虚拟DOM出现之前,我们创建页面UI最常用的方式就是innerHTML,但是它有一个很大的问题,就是会导致很多不必要的性能开销。
看下面这段代码,这是个很经典的渲染服务器返回的列表数据到HTML中:
const dataList = [
{ label: 'Lorem, ipsum.', value: 112.7 },
{ label: 'Praesentium, facere.', value: 96.22 },
{ label: 'Rerum, repudiandae.', value: 144.13 },
]
for (let i = 0; i < dataList.length; i++) {
containerDiv.innerHTML += `
${ dataList[i].label }
${ dataList[i].value }
`
}
它会如何执行呢?首先将模板字符串解析成 DOM 树,这是一个 DOM 层面的计算。DOM运算是非常消耗性能的,相比于纯JavaScript层面的计算来说是非常低效的。
然后,在for循环中每一次的循环都重新设置了innerHTML,重新设置 innerHTML 属性就等价于销毁所有旧的 DOM 元素,然后再全量创建新的 DOM 元素。
上面的代码中只是一个数量少的,结构复杂度低的示例,假如你要处理的数据量多达几千条,结构复杂度稍微高一点,那么你就能很明显的感觉出性能的开销。
虚拟DOM的作用(VNode)
既然问题有了,那么就一定会出现对应的解决方案,而虚拟DOM就是其中之一。根据上一个代码示例可以看出,直接使用innerHTML的方式来渲染DOM结构最大的问题在于:重复(没有变化)的结构被重新渲染了N次。
那有没有一种方法可以让我们只重新渲染变化的部分呢?而没变化的部分就不要重新渲染了。当然有,虚拟DOM就是为了解决这个问题而出现的。
虚拟 DOM 创建页面UI的过程分为两步:
- 第一步是创建 JavaScript 对象,这个对象可以理解为真实 DOM 的描述(VNode)
- 第二步是递归地遍历虚拟 DOM 树并创建真实 DOM。
虚拟DOM重新创建 JavaScript 对象(虚拟 DOM 树),然后比较新旧虚拟 DOM,找到变化的元素并更新它,也就是说虚拟DOM只会重新渲染差异部分。
渲染器(renderer)
Vue组件都是依赖渲染器来工作的,渲染器的工作原理就是使用一些我们熟悉的 DOM 操作 API 来完成渲染工作。
假设有如下虚拟DOM结构:
const vnode = {
tag: 'button',
children: [
{
tag: 'b',
children: 'Submit'
}
]
}
然后编写一个渲染器,把上面这段虚拟 DOM 渲染为真实 DOM。
function renderer(vnode, container) {
// 提取顶层元素
const el = document.createElement(vnode.tag)
// 遍历props
for (const key in vnode.props) {
// 提取事件
if (/^on/.test(key)) {
el.addEventListener(
key.substr(2).toLowerCase(),
vnode.props[key]
)
}
// 提取属性
el[key] = vnode.props[key]
}
// 处理children
if (typeof vnode.children === 'string') {
el.appendChild(
document.createTextNode(vnode.children)
)
} else {
vnode.children.forEach(child => renderer(child, el))
}
// 挂载元素
container.appendChild(el)
}
上面只是一段我们自己DIY的渲染器,现在我们用vue的渲染器来实现真实DOM的渲染:
- h(tag, props, children)
import { h, createApp } from './vue.esm-browser.prod.js'
const app = createApp({
render() {
return h('button', null, [
h('b', null, 'Submit')
])
}
})
app.mount('#app')