虚拟DOM如何进化为真实DOM
第一时间关注技术干货!
前言
Vue
和React
的Render
函数中都涉及到了Virtual DOM
的概念,Virtual DOM
也是性能优化上的重要一环,同时突破了直接操作真实DOM
的瓶颈,本文带着以下几个问题来阐述Virtual DOM
。
-
1.为什么要操作虚拟 DOM? -
2.什么是虚拟 DOM? -
3.手把手教你实现虚拟 DOM 渲染真实 DOM
希望阅读本文之后,能够让你深入的了解虚拟 DOM
并且在开发和面试中收益。
为什么要操作虚拟 DOM
为了帮助我们更好的理解为什么要操作虚拟 DOM,我们先从浏览器渲染[1]一个 HTML 文件需要做哪些事情说起:
浏览器渲染机制大致可以分为以下 5 步走:
-
1.创建 DOM tree -
2.创建 Style Rules -
3.构建 Render tree -
4.布局 Layout -
5.绘制 Painting
我们过去使用原生JavaScript
和jquery
去操作真实DOM
的时候,浏览器会从构建 DOM🌲 开始从头到尾的执行一遍渲染的流程。
在一次开发中,假如产品告诉你一个需求,你需要在一次操作中更新10个DOM
节点,理想状态是浏览器一次性构建完DOM树
,再执行后续操作。但浏览器没这么智能,收到第一个更新 DOM 请求后,并不知道后续还有9次更新操作
,因此会马上执行流程,最终执行 10 次流程。
过了一会产品经理把你叫过去和你说把需求改一下,此时你又需要操作一次 DOM 的更新,那么这个时候之前做的 10 次 DOM 操作就是白白浪费性能
,浪费感情。
即使计算机硬件一直在更新迭代,但是操作DOM
的代价仍旧是昂贵的,频繁操作 DOM 还是会出现页面卡顿
,影响用户的体验。真实的 DOM 节点,哪怕一个最简单的 div 也包含着很多属性,可以打印出来直观感受一下:
如此多的属性,如果每次对 DOM 结构都进行更新,一次,两次,三次...一百次....一千次...,可想而知,是多么庞大的数据量。
因此虚拟DOM
就是为了解决这个浏览器性能问题而被设计出来的。例如前面的例子,假如一次操作中有 10 次更新 DOM 的动作,虚拟DOM不会立即操作DOM
,而是将这 10 次更新 DOM 的动作通过Diff算法
最终生成一个js对象
,然后通知浏览器去执行一次绘制工作,这样可以避免大量的无谓的计算量
。
什么是虚拟 DOM
虚拟 DOM[2]就是我们上面所说的js对象
。
其本质上就是在JS
和DOM
之间做了一个缓存。可以类比 CPU
和硬盘
,既然硬盘
这么慢,我们就在它们之间加个缓存
:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存
。CPU(JS)
只操作内存(Virtual DOM)
,最后的时候再把变更写入硬盘(DOM),直接操作内存中的 JS 对象的速度显然要更快。
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
}
}
举个栗子:
假如我们有这样的一个 DOM 树
<ul class="list">
<li class="item">前端简报</li>
<li>vue</li>
</ul>
那么,我们怎么用 js 的对象来对应到这个树呢?
{
tag: 'ul', // 元素标签
data: { // 属性
class: 'list'
},
key: '',
text: '', // 文本内容
children: [
{
tag: "li",
data: {
class: "item"
},
key: '',
text: '',
children: [
{
tag: undefined,
data: undefined,
key: undefined,
text: '前端简报',
children: []
}
]
},
{
tag: "li",
data: "",
key: '',
text: '',
children: [
{
tag: undefined,
data: undefined,
key: undefined,
text: 'vue',
children: []
}
]
}
] // 子元素
}
由此可知:DOM tree
的信息都可以用JavaScript对象
来表示,反过来,我们也可以用 JavaScript对象
表示的树结构来构建一棵真正的DOM树
。
实现虚拟 DOM 渲染真实 DOM
有了JavaScript对象
之后如何转化为真实的 DOM 树结构呢?
ul 和 li 在 js 对象中,页面上并没有此结构,所以我们需要把ul
和li
转化为<ul>
和<li>
标签
而文本标签我们定义 Vnode 为:
{
tag: undefined,
data: undefined,
key: undefined,
text: 'vue',
children: []
}
故可以判断tag
的类型来确定创建元素
的类型.
function createElm(vnode) {
let { tag, data, children, key, text } = vnode;
if (typeof tag == "string") {
vnode.el = document.createElement(tag); //创建元素放到vnode.el上
children.forEach(child => {
vnode.el.appendChild(createElm(child))
})
} else {
vnode.el = document.createTextNode(text); //创建文本
}
return vnode.el
}
如果子节点存在并且也是虚拟DOM
的话,我们通过递归
调用创建子节点。
创建 DOM 树结构之后我们需要设置节点的属性,即处理虚拟 DOM 中的data
属性。
function updateProperties(vnode) {
let el = vnode.el;
let newProps = vnode.data || {};
for (let key in newProps) {
if (key == "style") {
for (let styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName];
}
} else if (key == "class") {
el.className = newProps.class;
} else {
el.setAttribute(key, newProps[key]);
}
}
}
在我们创建元素标签
之后调用updateProperties
方法即可
把上面创建出来的真实 DOM 结构 vnode.el 添加到文档当中即可呈现出我们需要的真实 DOM 结构
let parentElm = document.getElementById("app").parentNode; 获取之前app的父亲body
parentElm.insertBefore(createElm(vnode), document.getElementById("app").nextSibling); //body里在老的app后面插入真实dom
parentElm.removeChild(document.getElementById("app")); //删除老的节点
总结
以上就是本文的全部内容,我想我们现在应该了解什么是虚拟DOM的概念
了以及虚拟DOM是如何实现真实DOM渲染的
。其中用到了主要用到了子节点的递归
,下篇文章将讲解虚拟节点的 diff 算法,敬请期待。
参考资料
虚拟DOM介绍: https://www.jianshu.com/p/616999666920
[2]如何实现一个 Virtual DOM 算法: 'https://github.com/livoras/blog/issues/13'