查看本文Dom Diff源码demo地址
一、前言
1、什么是Dom Diff( 本质 )?
比对(diff)渲染更新前后产生的两个虚拟dom对象的差异,并产出差异补丁对象,再将差异补丁对象应用到真实dom节点上
2、为什么需要Dom Diff?
大家应该都知道操作Dom代价是昂贵的,因为操作Dom其本质是两个线程(JS引擎和GUI渲染引擎)间发送指令(通信)的过程,并且浏览器在创建初始化一个元素时,会为其创建很多属性,因此,在大量操作Dom的场景下,通过一些计算来尽可能少地操作Dom,保证了性能的下限。当然Dom Diff不一定更快!
3、Diff算法思想的现状?
在React、Vue等js库中,甚至Flutter的视图更新中都有应用
二、Virtual Dom
一句话概述:用JavaScript的树形结构对象来描述真实dom结构
1、@babel/preset-react
在我们日常开发中,我们写的react代码会经过@babel/preset-react(babel7)编译成如下图1
从上图可以看出整个React代码编译到生成Virtual-Dom的过程,相信你已经明白什么是虚拟Dom了
三、VirtualDom从初次渲染到更新
1、过程分析(图2)
(1) 用JS对象模拟DOM(虚拟DOM1)
(2) 将此虚拟DOM1转成真实DOM并插入页面中(render)
(3) 如果有事件发生(用户操作更新数据)修改了虚拟DOM产生虚拟DOM2,比较两棵虚拟 DOM树的差异,得到差异对象(diff)
(4) 把差异对象应用到真正的DOM树上(patch)
下面具体分析每一步:
2、生成VirtualDom1
从图一我们可以看出通过babel编译生成的代码,最终是通过createElement函数去构造每一个节点的:
(1) 通过构造函数Element构造虚拟dom节点
(2) 通过createElement来生成Element构造函数的实力对象(虚拟dom对象)
至此,我们就得到了VirtualDom1对象
3、 将VirtualDom1转化为真实dom(render)
思路:
(1) 根据虚拟dom对象中的type属性,使用document.createElement创建对应元素A
(2) 遍历虚拟dom对象中的props属性,先判断props是否null,再使用for in遍历props对象并 设置属性到元素A( setAttr方法 )
(3) 遍历虚拟dom对象中的children属性,判断当前遍历项是否继承Element构造函数,是的 话,递归当前遍历项,否则创建文本节点,并通过appenChild添加到元素A的子节点上
至此我们成功通过递归VirtualDom1创建出对应的真实dom
4、将真实dom挂载到指定根节点(renderDom)
5、Dom-Diff
由于外部用户操作导致生成了VirtualDom2,然后比对Virtual-dom1和Virtual-dom2的差异
分析:接收两个虚拟Dom对象,通过深度优先遍历来进行比对(只比较同级节点,不跨级比较),每次遍历到一个节点,就记录一个索引值(从0递增),如果比对发现有差异,就把索引值对应需要执行的操作存起来
比对规则:
(1) 节点类型不同,就执行节点替换模式{ type: REPLACE, newNode }
(2) 节点相同,就比较属性是否相同,不相同则{ type: ATTR, attrs: { class: 'list-group' } }
(3) 新的节点不存在,则{ type: REMOVE }
(4) 如果当前节点都是字符串且不相等,则是文本的变化{ type: TEXT, text: 'xxxx' }
过程分析(图3)
由上图我们可以看出,标红0,2,4,6的节点发生了改变,那么产出的pathes补丁对象是:
图4
6、打补丁(将补丁对象应用到真实dom上)
分析:我们通过VirtualDom1生成真实dom,所以VirtualDom1和真实dom的结构是一一对应的,然后又因为补丁对象是通过对VirtualDom1进行深度优先遍历生成的,那么我们只要对真实dom进行深度优先遍历,那补丁对象中记录的索引(表示节点位置)就能和真实dom对应上了,然后就能取出对应需要执行的dom操作
图5
至此就完成了DOM的更新操作
四、简易源码实现
Dom-Diff的简易实现源码: :
https://github.com/DBCdouble/Dom-Diff
附:后期补录关于key的Diff规则