先介绍一个概念Virtual Dom,我猜你们或多或少都应该知道什么是Virtual Dom吧,简单来讲就是用js来模拟DOM中的结点。node
下面就是一个Virtual Dom的结构,包含了标签名,拥有的属性,孩子结点,render函数算法
class Element {
constructor(tagName, attrs, children) {
this.tagName = tagName;
this.attrs = attrs || {};
this.children = children || [];
}
render () {
//这个函数是用来生成真实DOM的,最后会把return的结果添加到页面中去
}
}
复制代码
/**
<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算法中会用到的函数,并串联它们之间的关系并不会给出具体实现的代码bash
diff算法是进行虚拟节点Element的对比,并返回一个patchs对象,用来存储两个节点不一样的地方,最后用patchs记录的消息去局部更新Dom。app
两个树若是彻底比较的话须要时间复杂度为O(n^3),若是对O(n^3)不太清楚的话建议去网上搜索资料。而在Diff算法中由于考虑效率的问题,只会对同层级元素比较,时间复杂度则为O(n),说白了就是深度遍历,并比较同层级的节点。dom
let patchs = diff(oldTree, newTree);//获取两棵Virtual Dom Tree 差别
patch(ulDom, patchs);//找到对应的真实dom,进行部分渲染
复制代码
//深度遍历树,将须要作变动操做的取出来
//局部更新 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 算法的实现也就是如下三步学习
上面省略了不少代码,主要是为了让你们快速了解Dom diff 的基本原理和流程,若是想更深刻的了解,能够在网上查阅相关资料。ui