Diff算法

Diff

  • diff对比的就是vnode
  • 同时由于dom很少跨级移动,所以对比只在同层级中进行
  • vue和react的diff算法大体是一样的

VNode

  • vue的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
}
  • react的vnode如下
{
    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一样的问题

你可能感兴趣的:(javascript,vue.js,react.js)