- diff算法是vdom中最核心、最关键的部分
- diff算法能在日常使用vue react体现出来(如key)
概述
- diff即对比,是一个广泛的概念,如Linux diff命令、git diff等
- 两个js对象也可以做diff,如https://github.com/cujojs/jiff
- 两颗树做diff,如这里的vdom diff
树 diff 的时间复杂度 O(n^3)
snabbdom 原理解读
来感受 虚拟dom 对比的过程
// 只传递sel参数
export function h (sel: string): VNode
...
// 常用的是sel data children都传递
export function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNode
// 这个h函数返回的是 一个vnode 虚拟节点
return vnode(sel, data, children, text, undefined)
// 让我们再打开vnode函数
export function vnode (sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined): VNode {
const key = data === undefined ? undefined : data.key
// 只看返回的是什么,是一个对象
/*
其中children和text不共存,要不子元素是标签,要不没有子元素,里面是文本
elm 是这个虚拟dom要渲染到哪个元素下去的
key
*/
return { sel, data, children, text, elm, key }
}
/* 再看patch */
return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
.
.
.
// 执行pre hook,生命周期的钩子函数
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
// 第一个参数不是vnode,就是初次渲染,要渲染到dom元素下
if (!isVnode(oldVnode)) {
// 创建一个空的vnode 并且关联到这个dom元素下
oldVnode = emptyNodeAt(oldVnode)
}
// 相同的vnode
/*
sameVnode的内部是
return vnode1.key === vnode2.key && vnode1.sel === vode2.sel
就是之前说的虚拟dom的diff算法如果一个节点的tag(sel)和key相等,就认为他是相同的vnode
假如 key 都不传, undefined === undefined 是true
*/
if (sameVnode(oldVnode, vnode)) {
// vode对比
patchVnode(oldVnode, vnode, insertedVnodeQueue)
// 不同的vnode,直接删除重建
} else {
elm = oldVnode.elm!
parent = api.parentNode(elm) as Node
// 用vnode重建
createElm(vnode, insertedVnodeQueue)
。
。
。
。
return vnode
}
/* 打开patchVode */
function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue){
// 类似于vue的生命周期的钩子
const hook = vnode.data?.hook
hook?.prepatch?.(oldVnode, vnode)
// 新的来了,就把就得elm存起来
const elm = vnode.elm = oldVnode.elm!
// 新旧children
const oldCh = oldVnode.children as VNode[]
const ch = vnode.children as VNode[]
// 如果新的vnode的text是undefined,则意味着children是有值的
if (isUndef(vnode.text)) {
// 新旧都有children
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
// 新children有,旧children无,
} else if (isDef(ch)) {
// 如果旧text有值就情况
if (isDef(oldVnode.text)) api.setTextContent(elm, '')
// 然后把新的Children添加,addVnode就是dom操作
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
// 旧的有children,新的没有,那就移除旧的children
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, '')
}
// 有文本值,children是无值的
} else if (oldVnode.text !== vnode.text) {
if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
}
// 新旧text不一样,那就把旧的children移除,再设置新的文本
api.setTextContent(elm, vnode.text!)
}
hook?.postpatch?.(oldVnode, vnode)
}
// 对应使用方式
var vnode = h('div#container.two.classes', { on: { click: someFn } }, [
h('span', { style: { fontWeight: 'bold' } }, 'This is bold'),
' and this is just normal text',
h('a', { props: { href: '/foo' } }, 'I\'ll take you places!')
])
总的来说
h函数,用来根据传入的参数,生成对应的dom节点
patch函数,用来把新旧vnode对比完成,生成且去渲染计算好的dom
patchVode函数,两个vnode节点的一些判断。
比如先判断两个vode的sel和key是否一致
一致:那就继续判断children
不一致:删除重建dom元素
两个nvode的sel和key一样了,去看看children是否一样
新children有值,旧children无值,清空旧的文本内容,插入新的内容
新children无值,旧children有值,移除旧的节点,插入新的文本
新旧Children都有值,那就是要进行对比了。就是接下来对比的函数
updateChildren(elm, oldchildren, newchildren)
定义一个循环,当两边指针从外到内,碰到之后,循环结束。
进行特殊命中判断 (对比用的是 sel和key)
oldStart 和 newStart 对比一次
或者 oldend和newend
或者 oldstart和newend
或者 oldend和newstart对比
这四个都是尝试对比,命中就抛给patchvnode去处理
如果以上四个都没命中,这个方式是snabbdom的写法,在各种框架中对比方式各有不同
拿新节点的key能否对应上old节点中的key
就是对其它所有节点做对比,看是否能对应上,如果新的一圈下来,发现没有old对应上,那这个就是全新的节点,插入就行
如果对应上了,那就判断sel是否一致,不一致还是新建节点
一致,那就是一样的节点,交给patchVnode去处理。