Diff
- diff对比的就是vnode
- 同时由于dom很少跨级移动,所以对比只在同层级中进行
- vue和react的diff算法大体是一样的
VNode
{
el: div //对真实的节点的引用,本例中就是document.querySelector('#id.classA')
tagName: 'DIV', //节点的标签
sel: 'div#v.classA' //节点的选择器
data: null, // 一个存储节点属性的对象,对应节点的el[prop]属性,例如onclick , style
children: [], //存储子节点的数组,每个子节点也是vnode结构
text: null, //如果是文本节点,对应文本节点的textContent,否则为null
}
{
type: 'div',
props: {
className: 'myDiv',
},
chidren: [
{ type: 'p', props: { value:'1' } },
{ type: 'div', props: { value:'2' } },
{ type: 'span', props: { value:'3' } }
]
}
Vue
- vue的diff是边比较边更新的过程
- 更改数据时,触发试图更新,会生成一棵新的vdom树
- 对比同级的vnode,仅当vue认为两个vnode值得比较时,才会继续对比其子节点,如果认为不值得比较,会直接删除旧节点,插入新节点
function patch (oldVnode, vnode) {
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el
let parentEle = api.parentNode(oEl)
createEle(vnode)
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
api.removeChild(parentEle, oldVnode.el)
oldVnode = null
}
}
return vnode
}
function sameVnode(oldVnode, vnode){
// 两节点key值相同,并且sel属性值相同,即认为两节点属同一类型,可进行下一步比较
return vnode.key === oldVnode.key && vnode.sel === oldVnode.sel
}
- 针对同类型的节点,进行patch操作,并比较他的子节点
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el //让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化。
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return //新旧节点引用一致,认为没有变化
//文本节点的比较
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
//对于拥有子节点(两者的子节点不同)的两个节点,调用updateChildren
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
} else if (ch){ //只有新节点有子节点,添加新的子节点
createEle(vnode) //create el's children dom
} else if (oldCh){ //只有旧节点内存在子节点,执行删除子节点操作
api.removeChildren(el)
}
}
}
- 针对双方都拥有子节点的情况,通过updateChildren来处理更新
updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0, newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx
let idxInOld
let elmToMove
let before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) { //对于vnode.key的比较,会把oldVnode = null
oldStartVnode = oldCh[++oldStartIdx]
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 使用key时的比较
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
}
idxInOld = oldKeyToIdx[newStartVnode.key]
if (!idxInOld) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
}
else {
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
}else {
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = null
api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
}
}
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
- 概况下,就是分别新建4个指针,指向新旧节点的头尾,然后通过两两比对,看是否是同一个类型的节点
- 然后分情况,如果新旧头节点是同一个类型节点,直接调用patchVnode即可,同理新旧尾节点是同一个类型节点,也是patchVnode处理
- 但如果判断新头和旧尾是同一个类型节点,除了要patchVnode处理外,还需要将尾部节点移到头部
- 同理,如果新尾和旧头是同一个类型节点,patchVnode处理后,将头节点移到尾部
- 如果四种比较都没有击中,这时判断新头的节点是否在仍未被对比的旧节点中,如果在,patchVnode处理,并在dom中将旧节点移到新头的位置,原本旧节点对应的Vnode置空,这样下次判断就会被跳过,而如果新节点不在旧节点列表里,则直接新建节点插入即可
- 最后,当头指针和尾指针相遇并错过后,判断当前四个指针的位置,如果旧的头尾指针中间还有节点,那这些节点都是多余的,需要被移除,如果新的头尾指针中间还有节点,那这些节点都是新增的,需要被插入
React
- react的diff是先比较,后更新的流程,对比阶段会先记录下差异
function diff (oldTree, newTree) {
// 当前节点的标志,以后每遍历到一个节点,加1
var index = 0
var patches = {} // 用来记录每个节点差异的对象
dfsWalk(oldTree, newTree, index, patches)
return patches
}
// 对两棵树进行深度优先遍历
function dfsWalk (oldNode, newNode, index, patches) {
// 对比oldNode和newNode的不同,记录下来,省略了代码...
patches[index] = [...]
diffChildren(oldNode.children, newNode.children, index, patches)
}
- 先对比当前节点,记录差异,然后对比子节点
- 子节点的对比直接从左节点开始,判断其是否在旧节点列表中,如果不在,则记录新建,如果在,则标记移动
- 最后根据差异统一更新dom
区别
- vue的diff是边比较边更新的,react则是先记录差异,再统一更新
- vue认为两个节点元素是同一个元素是根据key以及sel来判断的,如果元素类型相同,但classname不一致的话也会认为是不同的元素,而react是根据元素类型以及key来判断的,所以会认为是同一个元素类型,直接在这上面进行属性增减
- diff策略也不一样,vue是新旧头尾四个节点对比,react则是从左到右进行对比
- 另外react16提出了fiber这个概念,diff是基于fiber树进行的,且可以被打断,让浏览器能处理更高优先级的任务,而vue的diff和patch是不能被打断的
key的作用
- vue和react的key作用是一样的,就是告诉引擎当前对比的两个节点是否是同一个节点
- vue没有key的时候,两个都是undefined,则vue会认为其是相同节点,而就地复用这个元素,但很可能这是个input元素,你已经在里面输入了某些信息,而更新时不会移动这些信息导致最终结果可能不是你所期望的,所以需要指明key来防止元素的复用
- react则会默认赋值给key,所以子元素中,新旧节点的位置可能发生了移动,但因为react默认index作为key,还是会认为相同位置上的元素是同一个元素,而在其基础上修改属性,导致发生和vue一样的问题