前言
众所周知,直接操作DOM是一个昂贵的操作,会销毁所有DOM元素,再全量创建新的DOM元素。
而虚拟DOM的意义就是找出差异的性能消耗最小化,通过减少直接的DOM操作来提高性能,而不是重新渲染整个页面。
渲染器的作用就是递归地遍历虚拟DOM对象,并调用原生DOM API来完成真实DOM的创建,将虚拟DOM转化为真实DOM。
介绍
假设我们有如下虚拟DOM:
const vnode = {
tag: 'div',
props: {
onClick: () => console.log('hello!')
},
children: 'click me'
}
tag
用来描述标签名称,所以tag: 'div'
描述的就是一个<div>
标签。props
是一个对象,用来描述<div>
标签的属性、事件等内容。可以看到,我们希望给div
绑定一个点击事件。children
用来描述标签的子节点。在上面的代码中,children
是一个字符串值,意思是div
标签有一个文本子节点:<div>click me</div>
。
接下来,我们需要编写一个渲染器,把上面这段虚拟DOM渲染为真实DOM。
代码实现
function renderer(vnode, container) {
// 使用 vnode.tag 作为标签名称创建 DOM 元素
const el = document.createElement(vnode.tag);
// 遍历 vnode.props,将属性、事件添加到 DOM 元素
for (const key in vnode.props) {
if (/^on/.test(key)) {
// 如果 key 以 on 开头,说明它是事件
el.addEventListener(
key.substr(2).toLowerCase(), // 事件名称 onClick --> click
vnode.props[key] // 事件处理函数
);
}
}
// 处理 children
if (typeof vnode.children === 'string') {
// 如果 children 是字符串,说明它是元素的文本子节点
el.appendChild(document.createTextNode(vnode.children));
} else if (Array.isArray(vnode.children)) {
// 递归地调用 renderer 函数渲染子节点,使用当前元素 el作为挂载点
vnode.children.forEach(child => renderer(child, el));
}
// 将元素添加到挂载点下
container.appendChild(el);
}
这里的 renderer
函数接收如下两个参数:
vnode
: 虚拟DOM对象。container
: 一个真实DOM元素,作为挂载点,渲染器会把虚拟DOM挂载到这个点下。
代码说明
总体来说,实现一个简单的渲染器分为三步:
- 创建元素:把
vnode.tag
作为标签名称来创建DOM元素。 - 为元素添加属性和事件:遍历
vnode.props
对象,如果key
以on
字符开头,说明它是一个事件,把字符on
截取掉后再调用toLowerCase
函数将事件名称小写化,最终得到合法的事件名称,例如onclick
会变成click
,最后调用addEventListener
绑定事件处理函数。 - 处理children:如果
children
是一个数组,就递归地调用renderer
继续渲染,注意,此时我们要把刚刚创建的元素作为挂载点(父节点);如果children
是字符串,则使用createTextNode
函数创建一个文本节点,并将其添加到新创建的元素内。
总结
当然,vnode.tag
也可以是组件,组件的本质就是一组DOM元素的封装。
在生成的虚拟DOM对象中多出了一个 patchFlags
属性,我们假设数字1代表“class是动态的”,这样渲染器看到这个标志时就知道:“哦,原来只有class属性会发生改变。”
它会通过Diff算法找出变更点,并且只会更新需要更新的内容。
就相当于省去了寻找变更点的工作量,性能自然就提升了。通过这篇文章,我们了解了虚拟DOM和渲染器的基本概念,以及如何实现一个简单的渲染器。
– 欢迎点赞、关注、转发、收藏【我码玄黄】,各大平台同名。