一、virtual dom是什么?它为什么会出现?
1、是什么?
virtual dom即 虚拟dom
用js模拟DOM结构
DOM变化的对比,放在JS层来做
提高重绘性能
// 真实的HTML DOM结构<ul id='list'> <li class='item'>Item 1</li> <li class='item'>Item 2</li> </ul> // 用JS模拟这个DOM{ tag: 'ul', attrs: { id: 'list' }, children: [ { tag: 'li', attrs: {className: 'item'}, children: ['Item 1'] }, { tag: 'li', attrs: {className: 'item'}, children: ['Item 2'] } ] }
DOM操作是非常‘昂贵’的,看似更复杂JS的virtual dom实则效率更高
// 用jquery实现修改DOM<div id="container"></div><button id="change-btn">CHANGE</button><script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script><script> const data = [ { name: '彭一', age: '18', height: '180cm', gender: '男', }, { name: '彭二', age: '19', height: '185cm', gender: '男', }, { name: '彭三', age: '20', height: '168cm', gender: '女', }, ] // 渲染函数 let render = data => { const $container = $('#container') // 清空现有内容 $container.html('') // 字符串拼接 const $table = $('<table>') $table.append($('<tr><td>姓名</td><td>年龄</td><td>身高</td><td>性别</td></tr>')) data.forEach(element => { $table.append($(`<tr><td>${element.name}</td><td>${element.age}</td> <td>${element.height}</td><td>${element.gender}</td></tr>`)) }); // 渲染到页面 $container.append($table) } // 修改信息 $('#change-btn').on('click', () => { data[1].age = 30 data[2].height = '178cm' render(data) }) // 初始化渲染 render(data)</script>
当点击change按钮后,看似只有彭二的age和彭三的height发生改变,但实则整个table表单又重新渲染了一次
2、遇到的问题
DOM操作是‘昂贵’的,JS运行效率高
尽量减少DOM操作,而不是‘推到重来’
项目越复杂,影响越严重
Virtual DOM即可解决这个问题
3、virtual dom存在的必要
用JS模拟DOM结构,效率更高
DOM操作‘昂贵’
将DOM对比操作放在JS层,提高效率
二、virtual dom如何应用,核心API是什么?
1、如何用?
能实现virtual dom的库很多,如: snabbdom
var container = document.getElementById('container');var vnode = h('div#container.two.classes', {on: {click: someFn}}, [ h('span', {style: {fontWeight: 'bold'}}, 'This is bold'), ' and this is just normal text', h('a', {props: {href: '/foo'}}, 'I\'ll take you places!') ]);// Patch into empty DOM element – this modifies the DOM as a side effectpatch(container, vnode);var newVnode = h('div#container.two.classes', {on: {click: anotherEventHandler}}, [ h('span', {style: {fontWeight: 'normal', fontStyle: 'italic'}}, 'This is now italic type'), ' and this is still just normal text', h('a', {props: {href: '/bar'}}, 'I\'ll take you places!') ]);// Second `patch` invocationpatch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
上面一段是snabbdom给出的一个示例,其中 h方法 是创建一个vnode,即虚拟节点,定义一个div,有一个id名container
,两个类名two
和classes
,绑定一个click事件someFn方法,后面跟着一个数组,数组中有3个元素:
第一个用h方法返回的,一个span,有
font-weight
样式,和文本内容This is bold
第二个元素就是一个文本字符串:
and this is just normal text
第三个元素是一个a元素,有一个属性href链接,后面是a标签文本
第一个patch
方法是将vnode放入到空的container中
newVnode
是返回一个新的node,然后第二个patch
是将前后两个node进行一个对比,找出区别,只更新需要改动的内容,其他不更新的内容不更新,这样做到尽可能少的操作DOM。
h方法抽离出来如下:
用h方法去具体实现文章开头的那个简单dom节点
// 真实的HTML DOM结构<ul id='list'> <li class='item'>Item 1</li> <li class='item'>Item 2</li> </ul> // JS模拟这个DOM{ tag: 'ul', attrs: { id: 'list' }, children: [ { tag: 'li', attrs: {className: 'item'}, children: ['Item 1'] }, { tag: 'li', attrs: {className: 'item'}, children: ['Item 2'] } ] }// 用h方法表示这个dom节点:var vnode = h('ul#list', {}, [ h('li.item', {}, 'Item 1'), h('li.item', {}, 'Item 2'), ])
用virtual dom写法改写jquery的那个demo:
<div id="container"></div><button id="change-btn">CHANGE</button><script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom.min.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-class.min.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-props.min.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-style.min.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-eventlisteners.min.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.1/h.min.js"></script><script> const snabbdom = window.snabbdom // 定义关键函数 patch const patch = snabbdom.init([ snabbdom_class, snabbdom_props, snabbdom_style, snabbdom_eventlisteners ]) // 定义关键函数 h const h = snabbdom.h const data = [ { name: '彭一', age: '18', height: '180cm', gender: '男', }, { name: '彭二', age: '19', height: '185cm', gender: '男', }, { name: '彭三', age: '20', height: '168cm', gender: '女', }, ] // 把表头放入data数组的第一项 data.unshift({ name: '姓名', age: '年龄', height: '身高', gender: '性别' }) const container = document.getElementById('container') const changeBtn = document.getElementById('change-btn') let vnode // 渲染函数 let render = (data) => { const newVnode = h('table', {}, data.map((item) => { let tds = [] let i for (i in item) { // hasOwnProperty检测某个对象是否拥有某个属性,可以有效避免扩展本地原型而引起的错误 if (item.hasOwnProperty(i)) { tds.push(h('td', {}, item[i] + '')) } } return h('tr', {}, tds) })) if (vnode) { // 修改后再次渲染 patch(vnode, newVnode) } else { // 初次渲染 patch(container, newVnode) } // 存储当前vnode赋给newVnode vnode = newVnode } // 初次渲染 render(data) changeBtn.addEventListener('click', () => { data[1].age = 30 data[2].height = '178cm' // 再次渲染data render(data) }) </script>
2、核心API
h('<标签名>', {属性}, [子元素])
h('<标签名>', {属性}, '文本字符串')
初次渲染:patch(container, vnode)
再次修改后DOM渲染:patch(vnode, newVnode)
三、diff算法
1、什么是diff算法
日常开发中都会用到diff,最普通的linux基础命令
diff
两个文件,找出不同,还有就是git命令比对前后修改内容
// 两个对象分别放在两个json中// data1.json{ "name": "pengxiaohua", "age": 18, "height": 184}// data2.json{ "name": "xiaohua", "age": 18, "height": 183}// 控制台输入 diff data1.json data2.json,得出:2c2 < "name": "pengxiaohua", --- > "name": "xiaohua",4c4 < "height": 184--- > "height": 183
同时在git命令中的git diff XXXX
也可以用来比对文件修改前后的差别
virtual dom为何用diff算法?
DOM操作是昂贵的,应该尽可能减少DOM操作
找出本次必须更新的节点,其他的不用更新
这个“找出”的过程,就需要diff算法
一句话,virtual dom中应用diff算法是为了找出需要更新的节点
2、diff算法实现流程
diff的实现过程就是 patch(container, vnode)
和 - patch(vnode, newVnode)
diff的实现的核心就是 createElement
和 updateChildren
patch(container, vnode)
初始化加载,直接将vnode
节点打包渲染到一个空的容器container
中
文章开头可以看到,用JS去模拟一个简单的DOM节点
// 真实的HTML DOM结构<ul id='list'> <li class='item'>Item 1</li> <li class='item'>Item 2</li> </ul> // JS模拟这个DOM{ tag: 'ul', attrs: { id: 'list' }, children: [ { tag: 'li', attrs: {className: 'item'}, children: ['Item 1'] }, { tag: 'li', attrs: {className: 'item'}, children: ['Item 2'] } ] }
那么模拟完了之后,怎么将模拟的JS进行转化为真实的DOM的呢?这个转化过程可以用这样一个 createElement
函数来描述:
function createElement (vnode) { var tag = vnode.tag var attrs = vnode.attrs || {} var children = vnode.children || [] if(!tag) { return null } // 创建真实的 DOM 元素 var ele = document.createElement(tag) // 属性 var attrName for (attrName in attrs) { if (attrs.hasOwnProperty(attrName)) { elem.setAttribute(attrName, attrs[attrName]) } } // 子元素 children.forEach(function (childVnode) { // 递归调用 createElement 创建子元素 elem.appendChild(createElement(childVnode)) }) // 返回真实的 DOM 元素 return elem }
当我们修改子节点,如下操作后,就要用到 patch(vnode, newVnode)
patch(vnode, newVnode)
数据改变后,patch对比老数据vnode
和新数据newVnode
// 真实的HTML DOM结构 <ul id='list'> <li class='item'>Item 1</li> <li class='item'>Item 22</li> <li class='item'>Item 3</li> </ul> // JS模拟这个DOM { tag: 'ul', attrs: { id: 'list' }, children: [ { tag: 'li', attrs: {className: 'item'}, children: ['Item 1'] }, { tag: 'li', attrs: {className: 'item'}, children: ['Item 22'] }, { tag: 'li', attrs: {className: 'item'}, children: ['Item 3'] } ] }
这个转化过程,其实就是遍历子节点,然后找出区别,如下面的方法 updateChildren
:
function updateChildren (vnode, newVnode) { var children = vnode.children || [] var newChildren = newVnode.children || [] // 遍历现有的 children children.forEach(function (child, index) { var newChild = newChildren[index] if (newChild == null) { return } if (child.tag === newChildren.tag) { // 两者 tag 一样 updateChildren(child, newChild) } else { // 两者 tag 不一样 replaceNode(child, newChild) } }) }function replaceNode (vnode, newVnode) { // 真实的DOM节点 var elem = vnode.elem var newElem = createElement(newVnode) // 替换(此处代码太过复杂,略省无数字) ... ... }
3、diff算法做了哪些事:
节点的新增和删除
节点重新排序
节点属性、样式、事件绑定
如何极致压榨性能
... ...
作者:JokerPeng
链接:https://www.jianshu.com/p/6ca5c22439f5