1, 为什么需要虚拟 DOM
前面我们从零开始写了一个简单的类 vue 框架 (文章链接), 其中的模板解析和渲染是通过 Compile 函数来完成的, 采用了文档碎片代替了直接对页面中 DOM 元素的操作, 在完成数据的更改后通过 appendChild 函数将真实的 DOM 插入到页面.
虽然采用的是文档碎片, 但是操作的还是真实的 DOM.
而我们知道操作 DOM 的代价是昂贵的, 所以 vue2.0 采用了虚拟 DOM 来代替对真实 DOM 的操作, 最后通过某种机制来完成对真实 DOM 的更新, 渲染视图.
所谓的虚拟 DOM, 其实就是用 JS 来模拟 DOM 结构, 把 DOM 的变化操作放在 JS 层来做, 尽量减少对 DOM 的操作 (个人认为主要是因为操作 JS 比操作 DOM 快了不知道多少倍, JS 运行效率高). 然后对比前后两次的虚拟 DOM 的变化, 只重新渲染变化了的部分, 而没有变化的部分则不会重新渲染.
比如我们有如下的 DOM 结构.
<ul id="list">
<li class="item1">Item 1</li>
<li class="item2">Item 2</li>
</ul>
我们完全可以用 JS 对象模拟上面的 DOM 结构, 模拟后就会变成下面的这种结构.
- var vdom = {
- tag: 'ul',
- attr: {
- id: 'list',
- },
- children: [
- {
- tag: 'li',
- attrs: {
- className: 'item',
- children: ['Item 1']
- },
- },
- {
- tag: 'li',
- attrs: {
- className: 'item',
- children: ['Item 2']
- }
- }
- ]
- }
必须要注意一点的是: JS 模拟的 DOM 结构并没有模拟所有 DOM 节点上的属性, 方法 (因为 DOM 节点本身的属性非常多, 这也是 DOM 操作耗性能的一个点), 而是只模拟了一部分和数据操作相关的属性和方法.
2, 怎么使用虚拟 DOM
Vue 在 2.0 版本引入了 vdom. 其 vdom 是基于 https://github.com/snabbdom/snabbdom 库所做的修改. snabbdom 是一个开源的 vdom 库.
snabbdom 的主要作用就是将传入的 JS 模拟的 DOM 结构转换成虚拟的 DOM 节点.
先通过其中的 h 函数将 JS 模拟的 DOM 结构转换成虚拟 DOM 之后, 再通过其中的 patch 函数将虚拟 DOM 转换成真实的 DOM 渲染到页面中.
为了保证页面的最小化渲染, snabbdom 引入了 Diff 算法, 通过 Diff 算法找出前后两个虚拟 DOM 之间的差异, 只更新改变了的 DOM 节点, 而不重新渲染为改变的 DOM 节点.
在这里我不打算分析 snabbdom 的源码来解释到底 snabbdom 是怎么干成这件事的 (主要是现阶段没到那个水平, 哈哈. 再者已经有很多同学做过类似的分析, 相关链接附在文章末尾).
我会从 snabbdom 的使用角度来看 Vue 中的虚拟 DOM 是如何完成视图渲染的.
我们先看一下 snabbdom 中两个核心 API 的功能.
h() 函数: 将传入的 JS 模拟的 DOM 结构模板转换成 vnode.(vnode 是一个纯 JS 对象)
patch() 函数: 将虚拟的 DOM 节点渲染到页面中.
我们提供一个实例来看一下 snabbdom 的实际作用.
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta http-equiv="X-UA-Compatible" content="ie=edge">
- <title>Document</title>
- </head>
- <body>
- <div id="container"></div>
- <button id="btn-change">change</button>
- <!-- 引入 snabbdom 库, 先不必纠结为什么这样引入, 以及每个文件的作用. 本篇文章只是介绍一下虚拟 DOM 的工作方式, 并不涉及原理解析
- 主要是因为博主目前功力尚浅, 有兴趣的小伙伴可以另行研究 -->
- <script src="https://cdn.bootCSS.com/snabbdom/0.7.1/snabbdom.js"></script>
- <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-class.js"></script>
- <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-props.js"></script>
- <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-style.js"></script>
- <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-eventlisteners.js"></script>
- <script src="https://cdn.bootcss.com/snabbdom/0.7.1/h.js"></script>
- <script>
- // 定义 patch 函数
- var patch = snabbdom.init([
- snabbdom_class,
- snabbdom_props,
- snabbdom_style,
- snabbdom_eventlisteners
- ])
- // 定义 h 函数
- var h = snabbdom.h;
- // 生成一个 vnode
- var vnode = h('ul#list',{},[
- h('li.item',{},['Item 1']),
- h('li.item',{},['Item 2']),
- ])
- //console.log(vnode);
- // 获取 container
- var container = document.getElementById('container');
- patch(container,vnode);// 初次渲染
- var btn = document.getElementById('btn-change');
- btn.onclick = function() {
- var newVnode = h('ul#list',{},[
- h('li.item',{},['Item 1']),
- h('li.item',{},['Item B']),
- h('li.item',{},['Item 3']),
- ])
- patch(vnode,newVnode);// 再次渲染
- vnode = newVnode;// 将修改后的 newVnode 赋值给 vnode
- }
- </script>
- </body>
- </html>
思路分析:
我们先通过 h 函数创建一个虚拟 DOM 节点, 通过 patch 函数将虚拟 DOM 渲染到页面.
点击 btn 按钮时, 更新 ul#list 列表的数据, 改变了第二个 li 元素的值并且新增了一个 li 元素, 第一个 li 元素的值并没有改变. 我们再次通过 patch 函数将更新后的数据渲染到页面上. 可以看到只有第二个和第三个 li 发生了更新, 而第一个 li 由于没有改变, 并没有重新渲染.
vue 中的模板解析和渲染的核心就是: 通过类似 snabbdom 的 h() 和 patch() 的函数, 先将模板解析成 vnode, 如果是初次渲染, 则通过 patch(container,vnode) 将 vnode 渲染至页面, 如果是二次渲染, 则通过 patch(vnode,newVnode), 先通过 Diff 算法比较原 vnode 和 newVnode 的差异, 以最小的代价重新渲染页面.
3, 参考文章
vue 的 Virtual Dom 实现 - snabbdom 解密 https://juejin.im/entry/591a5f14128fe1005cdad9b5
一起理解 Virtual DOM https://segmentfault.com/a/1190000007694388
virtual-dom(Vue 实现) 简析 https://segmentfault.com/a/1190000010090659
解析 vue2.0 的 diff 算法 https://segmentfault.com/a/1190000008782928
Vue 为什么要用 VDOM? https://segmentfault.com/q/1010000010520929
来源: https://www.cnblogs.com/yuliangbin/p/9392624.html