虚拟DOM在当下流行的前端框架中是非常重要的概念,相信所有前端人都会有所耳闻。下面我们来研究一下 vue 中的虚拟 DOM 是怎样的。
什么是虚拟 DOM
我们知道 DOM 对象被称作文档对象模型,是用来操作 html 元素的。将文档作为一个树形结构,树的每个结点表示了一个HTML标签或标签内的文本项。
而虚拟 DOM 简单来说就是用一个 JS 对象来描述一个 DOM 节点,如下示例:
// 定义虚拟DOM的类
class Element {
constructor(type, props, children) {
this.type = type
this.props = props
this.children = children
}
}
// 创建虚拟DOM
function createElement(type, props, children) {
return new Element(type, props, children)
}
let virtualDom = createElement('ul', {class: 'list'}, [
createElement('li', {class: 'item'}, ['周杰伦']),
createElement('li', {class: 'item'}, ['林俊杰']),
createElement('li', {class: 'item'}, ['王力宏'])
])
通过 js 对象模拟一个 DOM 节点所需的所有内容,这个 js 对象就称为 虚拟DOM。
如何把虚拟DOM 渲染为真实DOM?
主要思路就是:创建节点→设置节点属性→添加子节点。
// element.js
...
// 将虚拟DOM转换为真实DOM
function render(vDom) {
// 创建DOM元素
let el = document.createElement(vDom.type)
// 遍历虚拟DOM的props, 给真实DOM设置属性
for (let key in vDom.props) {
setAttr(el, key, vDom.props[key])
}
// 遍历子节点
// 如果是虚拟DOM,则递归渲染
// 否则则代表是文本节点,直接创建
vDom.children.forEach(item => {
let node = item instanceof Element ? render(item) : document.createTextNode(item)
el.appendChild(node)
})
return el
}
// 将真实DOM添加到页面中
function renderDom(el, target) {
target.appendChild(el)
}
// 设置DOM的属性
function setAttr(el, key, value) {
switch(key) {
case 'value':
// 如果 el 是 input或者textarea 就直接设置其value
if (el.tagName.toLowerCase() === 'input' || el.tagName.toLowerCase() === 'textarea') {
el.value = value
}
else {
el.setAttribute(key, value)
}
break
case 'style':
el.style.cssText = value
default:
el.setAttribute(key, value)
}
}
渲染虚拟DOM
// 创建虚拟DOM
let virtualDom = createElement('ul', {class: 'list'}, [
createElement('li', {class: 'item'}, ['周杰伦']),
createElement('li', {class: 'item'}, ['林俊杰']),
createElement('li', {class: 'item'}, ['王力宏']),
])
console.log(virtualDom)
// 将虚拟DOM渲染成真实DOM并添加到页面中
var el = render(virtualDom)
console.log(el)
renderDom(el, document.getElementById("app"))
到这里我们就实现了把一个 虚拟DOM 渲染为真实DOM并添加到页面里。
这里只模拟了基本的思路,vue中实现的方法要比这个复杂一些。
DOM-Diff算法
因为 vue 要实现数据的双向绑定,在更新视图的时候操作 DOM 是必须的,但是真实的 DOM 中有大量的 API,操作真实 DOM 势必是非常耗性能的。因此我们需要一个虚拟DOM来代替真实DOM,尽量减少对真实DOM的操作,从而提升性能。
当数据发生变化的时候,我们通过对比变化前后的虚拟节点,再通过DOM-diff算法计算出需要更新的部分,只更新有差异的DOM节点,从而实现局部更新的效果。
DOM-Diff的基本思想就是比较数据变化前后的两个虚拟DOM(vue中把虚拟DOM称为Vnode):
- 创建节点:对比新旧Vnode,如果在新的Vnode上新增节点,那么就在旧的Vnode 上加上;
- 删除节点:如果在新Vnode上删除节点,那么就在旧的Vnode 上去掉;
- 更新节点:如果新旧Vnode上都有的节点,那就以新Vnode为准,更新旧的Vnode,从而让新旧Vnode相同。
创建与删除节点比较好理解,但是更新节点就稍微复杂一些,因为我们要细致分析找出不一样的地方进行更新。这里我们可以细分为3部分:
(1)新旧 Vnode 都为静态节点
静态节点就是指这个节点只包含纯文字,没有可变的变量,无论数据如何变化,这个节点都不会改变。
这种情况下,会直接跳过,不做处理。
(2)新 Vnode 为文本节点
- 旧 Vnode 也是文本节点,比较两个文本是否相同,不同则更改旧 Vnode 的文本;
- 旧 Vnode 不是文本节点,直接改成新 Vnode 的文本节点
(3)新 VNode 是元素节点
- 该节点不包含子节点,又不是文本节点,说明是个空节点,直接清空即可。
- 该节点包含子节点,判断旧VNode是否有子节点,有则递归对比更新,没有则创建子节点。