什么是虚拟Dom

我们知道我们平时的页面都是有很多Dom组成,那虚拟Dom(virtual dom)到底是什么,简单来讲,就是将真实的dom节点用JavaScript来模拟出来,而Dom变化的对比,放到 Js 层来做。

下面是一个传统的dom节点,大家肯定都不陌生。

而这个dom对应的虚拟dom,可以表示成下面的样子很简单,大家都能看懂,tag表示标签名,attrs就是dom的属性,每个dom如果有children的话,就会在children中以数组的形式展示,数组的每一项就又是一个虚拟dom结构。

这里使用 Js 来实现虚拟dom的原因是 Js 在前端领域,是唯一一门图灵完备的语言;所谓图灵完备语言,就是指可以进行复杂逻辑操作,实现各种逻辑算法语言。

为何使用虚拟Dom

有人会问,dom挺好啊,我们刚学前端的时候肯定会接触JQuery,JQuery就是典型的操作dom的一个框架工具库,我们拿JQuery来设计一个场景,来解释一下虚拟dom的用处及价值。

这有一个需求场景

var data = [{name: '张三',age: '20',address: '杭州'},{name: '李四',age: '22',address: '北京'},{name: '隔壁老王',age: '24',address: "西溪水岸"}]
复制代码

我们现在想要将这个数据渲染成一个表格,并点击页面上的按钮更换我们的部分数据,我们使用Jquery来做。

  <div id="container"></div><button id="btn-change">change</button><script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script><script>var data = [{name: '张三',age: '20',address: '杭州'},{name: '李四',age: '22',address: '北京'},{name: '隔壁老王',age: '24',address: "西溪水岸"}]function render(data) {var $container = $('#container')//清空现有内容$container.html('')// 拼接 tablevar $table = $('<table>')$table.append($('<tr><td>name</td><td>age</td><td>address</td></tr>'))data.forEach(function (item) {$table.append($('<tr><td>'+ item.name +'</td><td>'+item.age+'</td><td>'+item.address+'</td></tr>'))})// 渲染到页面$container.append($table)}$('#btn-change').click(function () {data[1].age = 30data[2].address = '上海'render(data)})// 初始化时候渲染render(data)
复制代码

可以看到,我们将data的第2项的age和 第3项的address数据更换了,点击change按钮:

vdom解决的问题

我们可以从图中看到,我们只是更改了表格的部分数据,但是整个tabel节点就全部闪烁,说明整个table都被替换了一遍。

这个合乎常理的JQuery操,及时是web页面性能的巨大杀手。因为它更改了不需要更改的dom节点,如果你还不能事情的严重性,可以继续往下看。

下面的代码的操作很简单,创建一个空的div标签,循环遍历其中的属性并将其拼打印出来

    var div = document.createElement('div')var item ,result = ''for (item in div) {result += ' | ' + item}console.log(result)
复制代码

密密麻麻的属性,更何况这还只是一级属性,可想而知直接操作dom的方式是有多么费时,dom操作是费时的,但是Js作为一门语言,运行速度是非常快的,我们如果在Js层做dom对比,尽量减少不必要的dom操作,而不是每一次都全部翻修,我们的效率就会大大增加。而vdom就可以完美解决这个问题。

如何使用虚拟dom

说了这么多虚拟dom的好,有同学会问,如何使用虚拟dom呢?

要了解如何使用vdom,我们可以借助现有的vdom实现库,来了解其API,进而了解如何将vdom运用于开发中。

这里我们选择一个Vue2中使用的虚拟dom库 snabbdom,下面图是截得它github主页的示范案例:

仔细观察后我们可以发现,这个snabbdom官方案例中,核心内容就是两个函数 -- h函数patch函数

h函数

可以看到 h 函数,有三个参数

  • 标签选择器
  • 属性
  • 子节点

比如说第一个h函数生成的vnode,就是一个div标签,绑定了click事件为someFn,第一个child为带有style的spansapn里是一个文本节点This is bold,第二个child就直接是一个文本节点,第三个child就是一个带有herfa链接

patch函数

patch 分为两种情况

  • 第一种是第一次渲染的时候 patch将vnode丢到container空容器中

       var vnode = h('ul#list',{},[h('li.item',{},'大冰哥'),h('li.item',{},'伦哥'),h('li.item',{},'阿孔')])patch(container, vnode) // vnode 将 container 节点替换
    复制代码

第一次patch渲染的时候,是将生成的vnode往空容器里丢 可以对比之前的Jquery第一次渲染表格的时候,将table html append到容器中去

  • 第二种是更新节点的时候,newVnodeoldVnode替换

    btn.addEventListener('click',function() {var newVnode = h('ul#list',{},[h('li.item',{},'大冰哥'),h('li.item',{},'伦哥'),h('li.item',{},'孔祥宇'),h('li.item',{},'小老弟'),])patch(vnode, newVnode)
    })
    复制代码

这里的patch就会将的vonde和之前的vnode进行比对,只修改改动的地方,没动的地方保持不变,这里的核心就是涉及的diff算法

我们可以清楚的看到,相对于之前的JQuery整个页面dom全部替换的情况,用vdom的pathc函数只修改了我们相对老的vnode变动的地方,没改动的地方就没用动(从页面的闪烁可以看出来)

使用vdom重做之前Jq案例

vdom核心的api h函数和patch函数我们已经有个基本的了解了,为了巩固对其的认识,我们接下来用snabbdom重做我们之前的JQuery案例

直接先上代码

<div id="container"></div><button id="btn-change">change</button><script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.2/snabbdom-class.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.2/snabbdom-props.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.2/snabbdom-style.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.2/snabbdom-eventlisteners.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script><script>let container = document.getElementById('container')let btn = document.getElementById('btn-change')let snabbdom = window.snabbdomlet patch = snabbdom.init([snabbdom_class,snabbdom_props,snabbdom_style,snabbdom_eventlisteners])let h = snabbdom.hlet data = [{name: '张三',age: '20',address: '杭州'},{name: '李四',age: '22',address: '北京'},{name: '隔壁老王',age: '24',address: "西溪水岸"}]data.unshift({name: '姓名',age: '年龄',address: '地址'})let vnodefunction render(data) {// 创建虚拟table节点 第三个参数,也就是虚拟table的孩子 应该是虚拟的 行节点let newVnode = h('table', {}, data.map(item => {let tds = [] // 列,作为虚拟行的子项let ifor(i in item) {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}btn.addEventListener('click', function(){data[1].age = 30,data[3].name = '一个女孩',render(data)})// 初始化时候渲染render(data)</script>
</body>
复制代码

代码有点长,其实内容还是我们之前讲的,代码主要干了下面的事情

  • 引入snabbdom核心文件,初始化h函数和patch函数
  • 第一次加载的时候render 其实本质就是patch(container,newVnode)
  • 之后点击change的时候,生成新的vnode,再patch(vnode,newVnode)

这里的render函数重点讲解一下

  • newVnode生成的时候,第三个参数是childs
  • table的childs是行节点
  • tr行节点也是vnode,它再生成的时候也要使用h函数,第三个参数是td列vnode
  • td列vnode的第三个参数,就直接是文本节点啦,遍历item的每一项push到tds数组中就可以了

到了这里,你对vdom应该有个大体的认识了,其实,与其说vdom快,更准确的说是相比于Jquer这种推翻dom的方式等,保证不慢而已。

总结

vdom的核心api

  • h('标签名', '属性', [子元素])
  • h('标签名', '属性', '文本')
  • patch(container, vnode)
  • patch(oldVnode,newVnode)

简单介绍diff算法

什么是diff算法

我们在平时工作中,其实很多时候都会使用到diff算法

比如你在git提交代码的时候使用的 git diff 命令,再或者是网上的一些代码比对工具,而我们的虚拟dom,核心就是diff算法,我们前面讲过,找出有必要更新的节点更新,没有更新的节点就不要动。这其中的核心就是如何找出哪些更新哪些不更新,这个过程就需要diff算法来完成

通过patch简单讲diff

我们趁热打铁,还是使用之前的snabbdom库来简单的讲下diff算法的大体思路,在snabbdom中diff主要体现在patch中,我们接下来看一下patch的两种情况 patch(container, vnode)patch(vnode, newVnode)

篇幅有限,(其实是能力有限), 这里就简单的讲解,因为涉及到完成的diff算法的话东西实在是太多太多,有兴趣的可以去看一下snabbdom的源码

patch(container, vnode)

我们知道这个patch的过程是将一个vnode(vdom)添加到空容器生成真实dom的过程,主要的代码流程如下:

function creatElement(vnode) {let tag = vnode.taglet 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(arrtName, arrts[attrName])}}// 递归创建子元素children.forEach((childVnode) => {elem.appendChild(createElement(childVnode))})return elem
}
复制代码

简化后的代码很简单,大家也都能够理解,其中的一个重要的点就是 自递归调用生成孩子节点,终止条件就是tagnull的情况

patch(vnode, newVnode)

这个patch过程就是比较差异的过程,我们这里就只模拟最简单的场景

第三个item改变,又新增第四个item

// 简化流程 假设跟标签相同的两个虚拟dom
function updateChildren (vnode, newVnode) {let children = vnode.children || []let newChildren = newVnode.children || []// 遍历现有的孩子children.forEach((oldChild, index) => {let newChild = newChildren[index]if (newChild === null) {return}// 两者tag一样,值得比较if (oldChild.tag === newChild.tag) {// 递归继续比较子项updateChildren(oldchild, newChild)} else {// 两者tag不一样replaceNode(oldChild, newChild)}})
}
复制代码

这里面的点就也递归,这里只是简单的拿tag来判断更新条件,其实实际的比这复杂很多很多; 而replace函数实际的操作就是将newVnode新生成的真实dom将老的dom替换掉,这里涉及更多的是原生dom操作,就不在赘述了。

到这里,基本的diff概念应该大家有个认识了,再次强调,这里为了便于理解,将diff算法的流程简化了很多,实际的diff算法的复杂程度远远高于以上这些,比如说

  • 节点的新增和删除
  • 重新排序时以及这个过程的优化
  • 节点属性样式事件等的变化
  • 还有怎么将算法优化到极致等等。。

大家感兴趣可以去深入了解。

总结

本文知识抛砖引玉,通过阅读本文,让不了解虚拟dom的同学对虚拟dom有一个很好的认知,对diff算法有一个大体的认识。能达到这个效果,我觉得这篇文章就很有价值了。想要深入了解虚拟dom或者diff算法的同学可以翻阅snabbdom的 patch.js的源码,加深学习。

番外 Vue的key

写文章的时候碰到有vue key绑定的问题,这里就借助这股热劲,结合虚拟dom和diff算法,来了解一下Vue中的key

Vue 中的 key

首先Vue官网的解释:

当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。

这里的就地复用的策略复用的是没有发生改变的元素,其他的还要依次重排。

为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key 属性。理想的 key 值是每项都有的唯一 id。

我们在使用的使用经常会使用index(即数组的下标)来作为key,但其实这是不推荐的一种使用方法

如何理解,我们看下面一个例子:

这里有一个数组数据

const list = [{id: 1,name: 'test1',},{id: 2,name: 'test2',},{id: 3,name: 'test3',},
]
复制代码

我们现在想要在其后面追加一条数据

const list = [{id: 1,name: 'test1',},{id: 2,name: 'test2',},{id: 3,name: 'test3',},{id: 4,name: '添加到最后的一条数据',},
]
复制代码

这个时候用 index 作为 key, 是没有问题的,因为index在后面累加了1

但是如果插入的数据是插在中间而不是最后,

const list = [{id: 1,name: 'test1',},{id: 4,name: '不甘落后跑到第二的的一条数据',}{id: 2,name: 'test2',},{id: 3,name: 'test3',},
]
复制代码

这个时候就会会出现一个情况:

之前的数据                         之后的数据key: 0  index: 0 name: test1     key: 0  index: 0 name: test1
key: 1  index: 1 name: test2     key: 1  index: 1 name: 不甘落后跑到第二的的一条数据
key: 2  index: 2 name: test3     key: 2  index: 2 name: test2key: 3  index: 3 name: test3
复制代码

这样一来,追加数据以后,除了第一条数据能够就地复用,后三条都要重新渲染,这显然不是我们想要的结果。

唯一key来改善:

这次我们把每一项的key 绑定成唯一标示id

之前的数据                         之后的数据key: 1  id: 1 index: 0 name: test1   key: 1  id: 1 index: 0  name: test1
key: 2  id: 2 index: 1 name: test2   key: 4  id: 4 index: 1  name: 不甘落后的一条数据
key: 3  id: 3 index: 2 name: test3   key: 2  id: 2 index: 2  name: test2key: 3  id: 3 index: 3  name: test3
复制代码

现在除了新增了id为4的不甘落后的数据是新加入的,其他的都复用了之前的dom,因为这里通过唯一key来进行关联,不会随着顺序的改变而重新渲染。

所以我们需要使用key来给每个节点做一个唯一标识,Vue的Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点,所以一句话,key的作用主要是为了高效的更新虚拟DOM

灵魂画手上线:

可以看到,当我们老的数据转为新的数据时 [a,b,c,d] --> [a,e,b,c,d]

如果我们没有使用一个正确的key,可能除了a数据可以复用以外,后面的四个数据都要重新渲染

而如果使用了一个正确的key的时候,就可以实现要更改的只有一处,也就是新增数据 e,其他的就会如箭头所示,继续对应复用。

虚拟DOM和Diff算法 - 入门级相关推荐

  1. vue--mixin混入以及虚拟DOM和diff算法

    mixin混入 使用它的好处: 将 options 中的配置项可以单独抽离出来,单独管理,这样方便维护 使用: 新建一个对象用来保存 options 中某一个配置项,比如: methods 接下来要将 ...

  2. 【Vue源码解析】Vue虚拟dom和diff算法

    Vue虚拟dom和diff算法 1. 简介 2. 搭建环境 1. 安装snabbdom 2. 安装webpack5并配置 3.函数 3.1 虚拟节点vnode的属性 3.2 使用h函数 创建虚拟节点 ...

  3. vue 虚拟dom和diff算法详解

    虚拟dom是当前前端最流行的两个框架(vue和react)都用到的一种技术,都说他能帮助vue和react提升渲染性能,提升用户体验.那么今天我们来详细看看虚拟dom到底是个什么鬼 虚拟dom的定义与 ...

  4. vue之购物车案例升级版、v-model之lazy、number、trim的使用、fetch和axios、计算属性、Mixins、虚拟dom与diff算法 key的作用及组件化开发

    文章目录 1.购物车案例升级版(含价格统计.全选/反选.商品增加减少) 2.v-model之lazy.number.trim的使用 3.fetch和axios 3.1.通过jquery+ajax实现v ...

  5. 探秘vue核心之虚拟DOM与diff算法

    探秘vue核心之虚拟DOM与diff 一.真实DOM和其解析流程 所有的浏览器渲染引擎工作流程大致分为5步: 创建 DOM 树 -> 创建 Style Rules -> 构建 Render ...

  6. 【总结】1135- 图解虚拟 DOM 之 DIff 算法

    原文: https://juejin.cn/post/7000266544181674014 1. 目录 1. 相关知识点: 2. 虚拟DOM(Virtual DOM) 2.1. 什么是虚拟DOM 2 ...

  7. 浏览器性能优化(2)React 虚拟 dom与diff算法

    随着前端技术快速发展,现在的mvvm几大框架遍布前端行业,那么它们对浏览器的性能到底影响多大?与传统的jq相比做了哪些优化呢? 文章目录: React中的虚拟DOM是什么? 虚拟DOM的简单实现(di ...

  8. 虚拟dom与diff算法 分析

    好文集合: 深入浅出React(四):虚拟DOM Diff算法解析 全面理解虚拟DOM,实现虚拟DOM 转载于:https://www.cnblogs.com/zqzjs/p/5798107.html

  9. React简介、虚拟DOM、Diff算法、创建React项目、JSX语法、组件、组件声明方式、组件传值props和state、组件的生命周期

    React简介: 前面只是简单介绍移动APP开发,后面还会继续深入介绍移动app开发:其中想要用ReactNative开发出更出色的应用,那么就得学好React,下面将介绍React: React 是 ...

最新文章

  1. 我!90后!重庆女孩!在淘宝给别人改简历,年入百万!
  2. MySQL 中的myisam内部临时表
  3. C语言之随机数和字符串输入输出
  4. 动态规划---实现输出最大公共子序列的长度以及输出最大子字符串(java语言描述)
  5. 【ZJOI2015】幻想乡 Wi-Fi 搭建计划【几何】【贪心】【dp】
  6. Svcutil.exe详解
  7. Java定时器的cron设置详解Quartz
  8. Win10 64位系统下PCL + Visual Studio + cmake + (Qt) 安装调试
  9. Hover属性的充分利用
  10. mysql如何上传音频文件_关于音频文件的上传
  11. 回归预测 | MATLAB实现NCA(近邻成分分析)多输入单输出
  12. 飞思卡尔智能车一:山外鹰眼摄像头使用原理
  13. 荧光染料Alexa Fluor 647 alkyne/炔基炔烃
  14. python结合正则表达式及校验码生成算法校验:电话号码、营业执照、组织机构代码证、税务登记证、统一社会信用代码证、非盈利性企业登记证号码的函数
  15. 高防IP是如何来防御DDoS攻击的呢?
  16. ajax 模糊查询,ajax模糊查询api
  17. crash工具分析sysdump使用
  18. mos管的rc吸收电路计算_RC吸收电路
  19. Keras-gpu版本安装教程(亲测有效)
  20. linux 启动 参数,Linux启动参数

热门文章

  1. 中国第三代半导体行业应用动态与十四五发展格局展望报告2022版
  2. API Hook完全手册
  3. Go 变量及基本数据类型2
  4. 分享一个JDK1.8丢失数字精度的案例
  5. 软件设计师笔记---流水线
  6. class类文件结构
  7. 怎样将python的文件转化为windows的可执行程序
  8. 【最小割】HDU 3987 Harry Potter and the Forbidden Forest
  9. ExtJS之 标准布局类(针对于panel)
  10. 筷子兄弟--11度青春系列电影之《老男孩》