当听说或学习diff算法时第一印象就是它是一个比较虚拟dom的算法。欸,这个时候又不得不提虚拟dom这个小子是甚么。
虚拟dom是一个模拟真实dom的js对象,因为真实dom这小子身上的方法很多操作其不便,其次是操作js比操作dom性能更高更快。
虚拟dom的创建
js
<ul id="list"> //非常真实的dom
<li class="item">哈哈</li>
<li class="item">呵呵</li>
<li class="item">han</li>
</ul>
转化后->
js
let oldVDOM = { // 旧虚拟DOM
tagName: 'ul', // 标签名
props: { // 标签属性
id: 'list'
},
children: [ // 标签子节点
{
tagName: 'li', props: { class: 'item' }, children: ['哈哈']
},
{
tagName: 'li', props: { class: 'item' }, children: ['呵呵']
},
{
tagName: 'li', props: { class: 'item' }, children: ['han']
},
]
}
这个时候又很疑惑了,dom如何转化为虚拟dom。之前一些框架是使用h函数 我们来简单实现一下加深下印象:(代码可直接运行,大家不想看文本格式直接放到vscode兄弟身上运行)
js
/**
*
* @param {string} a tagName 选择器
* @param {object} b data
* @param {any} c 是子节点 可以是文本,数组
*/
export default function h(a, b, c) {
if (arguments.length < 3) throw new Error('请检查参数个数');
if (typeof c === 'string' || typeof c === 'number') {
return vnode(a, b, undefined, c, undefined)
}
else if (Array.isArray(c)) {
let children = []
for (let i = 0; i < c.length; i++) {
if (!(typeof c[i] === 'object' && c[i].sel))
throw new Error('第三个参数为数组时只能传递h()函数')
children.push(c[i])
}
return vnode(a, b, children, undefined, undefined)
}
else if (typeof c === 'object' && c.sel) {
let child = [c]
return vnode(a, b, child, undefined, undefined)
}
}
/**
* 把传入的 参数 作为 对象返回
* @param {string} tagName 选择器
* @param {object} data 数据
* @param {array} children 子节点
* @param {string} text 文本
* @param {dom} elm DOM
* @returns object
*/
export default function (sel, data, children, text, elm) {
return { sel, data, children, text, elm }
}
//简单使用
vnode = h('ul', {}, [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
h('li', { key: 'E' }, 'E'),
])
这里大家也可以清楚的看到h函数转化主要是对子节点进行添加。将js对象中的对应属性名添加上。
由上图,一看就能看到第二种方式更加快,因为第一种方式中间还加着虚拟dom的步骤。所以虚拟dom比真实dom快是不够严谨的。那该怎么说嘞,虚拟dom算法操作真实dom,性能高于直接操作真实dom,虚拟dom和虚拟dom算法是两个概念。
比较流程
虚拟dom算法=虚拟dom+diff算法(终于来了) 为了让刚学习的童鞋理解大概的diff算法,这简单介绍一下流程:
js
patch(vnode, vnodeList)//刚开始使用patch方法比较新旧虚拟dom pacth(oldvdom,newvdom)
//patch函数流程
if (!oldnode.tagname) { //先判断old的是不是虚拟dom,不是则进行转化格式
oldnode = changeVirtualnode(oldnode)
}
if (samenode(oldnode, newnode)) { //如果他们是同一节点则比较他们的子节点
patchnode(oldnode, newnode)
} else { //如果不是着直接将新的给强行插到老的里面就行
let newNode = createlm(newnode)
if (oldnode.elm.parentNode) {
let parent = oldnode.elm.parentNode
parent.insertBefore(newNode, oldnode.elm)
parent.removeChild(oldnode.elm)
}
}
//patchnode函数
if (oldnode === newnode) return //如果完全一样就return
if (newnode.text && !newnode.children) { //如果新dom节点没有子节点
if (newnode.text !== oldnode.text) {
oldnode.elm.textContent = newnode.text
}
} else {
if (oldnode.children) { //如果双方都有子节点开始细节比较,updateChildren里面还会使用patchnode递归调用直到没有子节点比较为主
updateChildren(oldnode.elm, oldnode.children, newnode.children)
} else { //不用多说旧dom没有子节点新dom由子节点
oldnode.elm.innerHTML = ''
for (let i = 0; i < newnode.children.length; i++) {
let node = createlm(newnode.children[i])
console.log('awdawd', node)
oldnode.elm.appendChild(node)
}
}
}
现在童鞋们知道了比较虚拟dom中diff算法出现的位置,现在介绍下具体的diff算法。
diff算法原理
众所周知diff算法是同级比较
这里主要将一下vue的diff(代码展示)
Vue2.X Diff ------ 双端比较
所谓双端比较
就是新列表 和旧列表两个列表的头与尾互相对比,,在对比的过程中指针会逐渐向内靠拢,直到某一个列表的节点全部遍历过,对比停止。

js
function vue2Diff(prevChildren, nextChildren, parent) {
//...声明属性大家应该都会滴
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
//... 不做操作 指针变化
} else if (oldEndNode.key === newEndNode.key) {
//... 不做操作指针变化
} else if (oldStartNode.key === newEndNode.key) {
//**原本旧列表中是头节点,然后在新列表中是尾节点。那么只要在旧列表中把当前的节点移动到原本尾节点的后面,就可以了**
} else if (oldEndNode.key === newStartNode.key) {
//**原本旧列表中是尾节点,然后在新列表中是头节点。那么只要在旧列表中把当前的节点移动到原本头节点点的前面,就可以了**
} else {
// 在旧列表中找到 和新列表头节点key 相同的节点
let newKey = newStartNode.key,
oldIndex = prevChildren.findIndex(child => child.key === newKey);
if (oldIndex > -1) { //如果找到了,直接塞到旧头节点前面
let oldNode = prevChildren[oldIndex];
patch(oldNode, newStartNode, parent)
parent.insertBefore(oldNode.el, oldStartNode.el)//将该节点插入到oldStartIndex节点前
prevChildren[oldIndex] = undefined
}else {
mount(newStartNode, parent, oldStartNode.el) //如果没找到就创建一个节点塞到前面
}
newStartNode = nextChildren[++newStartIndex]
}
}
if (oldEndIndex < oldStartIndex) { //需要添加节点时
for (let i = newStartIndex; i <= newEndIndex; i++) {
mount(nextChildren[i], parent, prevStartNode.el) 应判断插入到末尾还是开图位置
}
} else if (newEndIndex < newStartIndex) { 如果需要删除节点时
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
if (prevChildren[i]) { 判断是否为undefinded
partent.removeChild(prevChildren[i].el)
}
}
}
}
到此完成。 vue3
的diff
借鉴于inferno (opens new window),该算法其中有两个理念。第一个是相同的前置与后置元素的预处理;第二个则是最长递增子序列,此思想与React
的diff
类似又不尽相同。下面我来一一介绍
js
function vue3Diff(prevChildren, nextChildren, parent) {
let j = 0,
prevEnd = prevChildren.length - 1,
nextEnd = nextChildren.length - 1,
prevNode = prevChildren[j],
nextNode = nextChildren[j];
// label语法
outer: {
while (prevNode.key === nextNode.key) {
patch(prevNode, nextNode, parent)
j++
// 循环中如果触发边界情况,直接break,执行outer之后的判断
if (j > prevEnd || j > nextEnd) break outer
prevNode = prevChildren[j]
nextNode = nextChildren[j]
}
prevNode = prevChildren[prevEnd]
nextNode = prevChildren[nextEnd]
while (prevNode.key === nextNode.key) {
patch(prevNode, nextNode, parent)
prevEnd--
nextEnd--
// 循环中如果触发边界情况,直接break,执行outer之后的判断
if (j > prevEnd || j > nextEnd) break outer
prevNode = prevChildren[prevEnd]
nextNode = prevChildren[nextEnd]
}
} //双端比较
// 边界情况的判断 如果j<=nextend则添加上节点,j>nextend则
if (j > prevEnd && j <= nextEnd) {
let nextpos = nextEnd + 1,
refNode = nextpos >= nextChildren.length
? null
: nextChildren[nextpos].el;
while(j <= nextEnd) mount(nextChildren[j++], parent, refNode)
} else if (j > nextEnd && j <= prevEnd) {
while(j <= prevEnd) parent.removeChild(prevChildren[j++].el)
}
}
它会先比较两个节点前置和后值相同的地方直接复用到dom。
然后处理中间的元素先回创建一个和剩余新child长度一样的列表,然后会根据旧列表映射到新列表元素的值的下标改变列表中的值,之后再去寻找最长字符串,从后先向遍历,如果不是最长最序列中的值就会需要移动,如果是-1则直接插入到队尾。
感谢阅读文章由问题请指正