Virtual Dom && Diff原理,极简版
前言
先介绍一个概念Virtual Dom,我猜大家或多或少都应该知道什么是Virtual Dom吧,简单来说就是用js来模拟DOM中的节点。
Virtual Dom
下面就是一个Virtual Dom的结构,包含了标签名,拥有的属性,孩子节点,render函数
class Element {
constructor(tagName, attrs, children) {
this.tagName = tagName;
this.attrs = attrs || {};
this.children = children || [];
}
render () {
//这个函数是用来生成真实DOM的,最后会把return的结果添加到页面中去
}
}
创建一棵个Virtual Dom Tree && 渲染
/**
<ul id="list">
<li class="a">txt_a</li>
<li class="a">txt_b</li>
</ul>
**/
//根据上面结构可以用一下方式创建一棵 Virtual Dom Tree
let ul = Element('ul', { id: 'list' }, [
Element('li', { class: 'a' }, ['txt_a']),
Element('li', { class: 'b' }, ['txt_b'])
]);//ul 就是一棵个Virtual Dom Tree
let ulDom = ul.render();//生成真实Dom
document.body.appendChild(ulDom);//添加到页面中
以上就是Virtual Dom Tree如何被转换成真实Dom并添加到网页中的过程,再这个过程中我把render函数给省略,只是为了让你们先了解原理,具体实现可以以后再深究。我学一个东西的时候,习惯是先把整体原理弄清楚,再去深入学习相关的知识。
Diff 算法
在介绍Diff算法之前,再次声明我只会列举Diff算法中会用到的函数,并串联它们之间的关系并不会给出具体实现的代码
介绍
diff算法是进行虚拟节点Element的对比,并返回一个patchs对象,用来存储两个节点不同的地方,最后用patchs记录的消息去局部更新Dom。
两个树如果完全比较的话需要时间复杂度为O(n^3),如果对O(n^3)不太清楚的话建议去网上搜索资料。而在Diff算法中因为考虑效率的问题,只会对同层级元素比较,时间复杂度则为O(n),说白了就是深度遍历,并比较同层级的节点。
Diff只需下面两句代码
- 判断两棵Virtual Dom Tree 差异
- 把差异更新到真实Dom中去
let patchs = diff(oldTree, newTree);//获取两棵Virtual Dom Tree 差异
patch(ulDom, patchs);//找到对应的真实dom,进行部分渲染
Diff中所用到的函数
//深度遍历树,将需要做变更操作的取出来
//局部更新 DOM
function patch(node,patchs){
//代码略
}
// diff 入口,比较新旧两棵树的差异
function diff (oldTree, newTree) {
let index = 0
let patches = {} // 记录每个节点差异的补丁
dfs(oldTree, newTree, index, patches)
return patches;
}
/**
* dfs 深度遍历查找节点差异
* @param oldNode - 旧虚拟Dom树
* @param newNode - 新虚拟Dom树
* @param index - 当前所在树的第几层
* @param patches - 记录节点差异
*/
function dfs (oldNode, newNode, index, patches){
let currentPatch = [];//当前层的差异对比
if (!newNode) {
//如果节点不存不用处理,listdiff函数会处理被删除的节点
}else if (isTxt(oldNode) && isTxt(newNode)) {//isTxt用来判断是否是文本,为了简便这边并没有声明
if (newNode !== oldNode)
currentPatch.push({ type: "text", content: newNode })
//如果发现文本不同,currentPatch会记录一个差异
}else if(oldNode.tagName === newNode.tagName && oldNode.key === newNode.key){
//如果发现两个节点一样 则去判断节点是属性是否一样,并记录下来
let attrsPatches = diffAttrs(oldNode, newNode)
if (attrsPatches) {//有属性差异则把差异记录下来
currentPatch.push({ type: "attrs", "attrs": attrsPatches })
}
// 递归遍历子节点,并对子节点进行diff比较
diffChildren(oldNode.children, newNode.children, index, patches)
}else{
//最后一种情况是,两个节点完全不一样,这样只需要把旧节点之间替换就行
//把当前差异记录下来
currentPatch.push({ type: "replace", node: newNode})
}
//如果有差异则记录到当前层去
if (currentPatch.length) {
if (patches[index]) {
patches[index] = patches[index].concat(currentPatch)
} else {
patches[index] = currentPatch
}
}
}
//判断两个节点的属性差异
function diffAttrs(oldNode, newNode){
let attrsPatches = {};//记录差异
let count = 0;//记录差异的条数
/**
代码略
判断两个节点的属性差异的代码就略了,
让你们知道这里的代码就是判断两个节点的属性有哪些差异,
如果有差异就记录在attrsPatches这个对象中,并把它返回
**/
if(0 == count){
return null;
}else {
return attrsPatches;
}
}
//判断孩子节点
function diffChildren(oldChild, newChild, index, patches){
let { changes, list } = listDiff(oldChild, newChild, index, patches);
if (changes.length) {//如果有差异则记录到当前层去
if (patches[index]) {
patches[index] = patches[index].concat(changes);
} else {
patches[index] = changes;
}
}
// 代码略
//遍历当前数组
oldChild && oldChild.forEach((item, i) => {
// 代码略
let node;// 经过判断后node节点是同时存在于oldChild 和 newChild中
//则对节点进行递归遍历 相当于 进入下一层 节点,
let curIndex;
dfs(item, node, curIndex, patches);
// 代码略
})
}
//判断oldNodeList, newNodeList 节点的位置差,主要是为了判断哪些节点被移动、删除、新增。
function listDiff(oldNodeList, newNodeList, index){
let changes = [];//记录 oldNodeList, newNodeList节点的位置差异,是被移动、删除、新增
let list = [];//记录 oldNodeList,newNodeList 同时存在的节点
/**
具体判断逻辑的代码就略了
**/
return {changes,list};
}
如果大家对函数之间的调用还不明白的话可以看下面的图
最后
Virtual Dom 算法的实现也就是以下三步
- 通过 JS 来模拟生成 Virtual Dom Tree
- 判断两个 Tree 的差异
- 渲染差异
上面省略了很多代码,主要是为了让大家快速了解Dom diff 的基本原理和流程,如果想更深入的了解,可以在网上查阅相关资料。
编辑于 2019-04-20 01:53