前端前端学习虚拟DOM
Juns虚拟 DOM
前言
在上一篇文章回流与重绘里面我们就提到了,操作 DOM 元素是代价挺高的,也给出了一些优化方案,不过总体来说,直接操作 DOM 其实也是比较麻烦的,而且也有较高的心智负担,这篇文章就来介绍一下虚拟 DOM。
关键词:虚拟 DOM、diff 算法、跨平台、回流、重绘
什么是虚拟 DOM
先来说说为什么出现吧,在之前,基本都是使用jQuery手动操作 DOM,代码中有很大一部分都是在操作 DOM,随着应用的复杂化,这变得越来越烦琐,而且也有很大的性能开销。
所以说我们需要有一种方式来优化他,根据上一篇文章的优化方案,我们可以很容易的想到,在 js 里面模拟 DOM,然后操作完毕后,一次性渲染 DOM,这样可以减少很多重排次数。
那么把这个操作规范一下,我们只需要负责改变数据/状态,修改 DOM 的行为交给一个东西来做,可以称之为虚拟 DOM。
虚拟 DOM 可以简单理解为用 js 模拟 DOM 结构,然后在 js 中对比,之后再去渲染。
下面这个 DOM 就可以映射成一个虚拟 DOM
| <ul id="list"><li class="item">item1</li>
 </ul>
 
 | 
虚拟 DOM:
| const listVDom = {tag: 'ul',
 attrs: {
 id: 'list',
 },
 children: [
 {
 tag: 'li',
 attrs: {
 className: 'item',
 },
 children: ['item1'],
 },
 ],
 }
 
 | 
相较于真实 DOM 的优点
有一部分人的观点认为,虚拟 DOM 比真实 DOM 快,不过这个肯定是分情况的,无虚拟 DOM 的Svelte和Solid性能都不差。只能说是,比如在大量数据渲染,同时只改变一小部分数据的时候,虚拟 DOM 更具有优势。
实际上对于虚拟 DOM,下面才算是他更加优秀的地方:
- 打开了函数式的 UI 编程大门,UI = f(data)
- 使用 js 对象来描述 DOM,可以把这个对象渲染到除了浏览器 DOM 之外的地方,可以实现跨平台开发
简易实现
下面分别用 jQuery 和虚拟 DOM 来写一个例子
jQuery
| <div id="container"></div><button id="btn-change">改变</button>
 
 <script src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script>
 
 <script>
 const data = [
 {
 name: '张三',
 age: '20',
 address: '北京',
 },
 {
 name: '李四',
 age: '21',
 address: '武汉',
 },
 ]
 
 function render(data) {
 const $container = $('#container')
 $container.html('')
 const $table = $('<table>')
 
 $table.append($('<tr><td>name</td><td>age</td><td>address</td></tr>'))
 data.forEach(item => {
 
 $table.append(
 $(
 `<tr><td>${item.name}</td><td>${item.age}</td><td>${item.address}</td></tr>`
 )
 )
 })
 $container.append($table)
 }
 render(data)
 
 $('#btn-change').click(function () {
 data[1].age = 30
 data[0].address = '深圳'
 render(data)
 })
 </script>
 
 | 
可以轻松看出来,每次改动 data 之后,table 标签都会重新创建渲染,这会很消耗浏览器的性能
下面使用snabbdom来实现虚拟 DOM 版本
这是一个简易的实现虚拟 DOM 的库,有俩个核心 api:h 函数和 patch 函数,h 函数用于生成 vdom,patch 用于比对 vdom 以及挂载到真实 DOM 上。
| <div id="container"></div><button id="btn-change">改变</button>
 <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
 <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
 <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
 <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
 <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.min.js"></script>
 <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
 <script>
 let sn = window.snabbdom
 
 
 let patch = sn.init([
 snabbdom_class,
 snabbdom_props,
 snabbdom_style,
 snabbdom_eventlisteners,
 ])
 
 
 let h = sn.h
 
 const data = [
 { name: '姓名', age: '年龄', address: '地址' },
 { name: '张三', age: '20', address: '北京' },
 { name: '李四', age: '21', address: '武汉' },
 ]
 
 let container = document.getElementById('container')
 let vnode = null
 const render = data => {
 let newVnode = h(
 'table',
 {},
 data.map(item => {
 let tds = []
 for (let key in item) {
 if (item.hasOwnProperty(key)) {
 tds.push(h('td', {}, item[key] + ''))
 }
 }
 return h('tr', {}, tds)
 })
 )
 
 vnode ? patch(vnode, newVnode) : patch(container, newVnode)
 
 vnode = newVnode
 }
 
 render(data)
 
 let btn = document.getElementById('btn-change')
 btn.addEventListener('click', function () {
 data[1].age = 30
 data[1].address = '深圳'
 render(data)
 })
 </script>
 
 | 
可以发现,只有数据改变的地方 DOM 才重新渲染了
diff 算法
在上面也提到过了,新旧虚拟 DOM 之间需要比对,然后才渲染成真实 DOM,而不是直接整个的替换,这其中就涉及到了 diff 算法。
什么是 diff 算法?
在其他地方我们也都听到过 diff,比如文本 diff,git diff,意思就是对比算法
为什么需要 diff 算法?
在上面也说了,DOM 操作十分消耗性能,所以我们需要尽量减少 DOM 操作。所以说需要找出必须更行的 DOM 节点来选择性更新,其他的不更新,这其中的过程就需要应用到 diff 算法。
简易实现
| const createElement = vnode => {
 let tag = vnode.tag
 let attrs = vnode.attrs || {}
 let children = vnode.children || []
 if (!tag) {
 return null
 }
 
 let elem = document.createElement(tag)
 
 for (let attrName in attrs) {
 if (attrs.hasOwnProperty(attrName)) {
 elem.setAttribute(attrName, attrs[attrName])
 }
 }
 
 children.forEach(childVnode => {
 
 elem.appendChild(createElement(childVnode))
 })
 
 return elem
 }
 
 
 function updateChildren(vnode, newVnode) {
 let children = vnode.children || []
 let newChildren = newVnode.children || []
 
 children.forEach((childVnode, index) => {
 let newChildVNode = newChildren[index]
 if (childVnode.tag === newChildVNode.tag) {
 
 updateChildren(childVnode, newChildVNode)
 } else {
 
 replaceNode(childVnode, newChildVNode)
 }
 })
 }
 
 | 
参考文章