patch流程和diff算法

86 阅读11分钟

1.  问题点知识点集合

1.  key的作用,为什么不能用index座位key值

普通绑定了值的元素的key为undefined,只有for循环指定了key才有key。

key的作用就是用来判断新老vnode是不是同一个vnode,这个key只能是用户手动去定义,不能系统生成(因为系统不知道怎么对应数据)

为什么不能用index作为可以。因为当数据有删改之后,index是不会有空缺的,只会因此替补。以前的数据和重新渲染后的数据随着 key 值的变化从而没法建立关联关系. 这就失去了 key 值存在的意义

2.  如何对比两个vnode是否是同一个(sameVnode)

sameVnode的意思是这两个新旧节点是同一个节点,并不是说这两个新旧节点毫无变化,他们是同一个节点,但是里面的内容可能变化了,比如属性变化了,子元素变化了。

sameVnode比较:

1.  key相同普通非for循环的vnode的key为undefined,也算是相同的

2.  相同的tag标签

3.  isDef(a.data) === isDef(b.data),同样定义了data,或者同样没定义data

4.  如果是input元素,则要input的type也相同

function sameVnode (ab) {
  return (    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        !childrenIgnored(a) && !childrenIgnored(b) &&
        sameInputType(ab)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

普通元素的key为undefined,只有for循环有key,普通元素一个元素对应一个值,也用不上key,如果有人做如下骚操作,那么就认为这两个元素是同一个的,就直接更新。

<div id="demo" >
  <div>{{arr[0]}}</div>
  <div>{{arr[1]}}</div>
</div>

data: {
  arr: ['11''22'],
},
mounted() {     
    this.arr.reverse()
},

3.  为什么return ptach,这里用一个工厂函数

答:为了跨平台,因为patch所做的操作是平台相关的,而patch方法是通用的,里面操作节点的方法是外部传进去的,不同的平台传不同的操作节点的方法

4.  更新过程是操作dom,不会改变vnode数据

不会去改变新老虚拟dom的数据结构,也就是新老vnode是什么样就是什么样,只会去直接操作真实的dom,例如算出来是老的第一个和新的最后一个是相同元素,那就操作真实的dom,把列表中的第一个元素放到最后面去(对应是通过vnode里面的elm指针去对应的,不论如真实的dom如何移动,指针的指向都没有改变)。当所有的vnode比较完以后,直接把新的vnode赋值给老的vnode。全程不改变新老vnode的任何结构和数据

5.  dom操作是微任务

dom操作是微任务,等所有的微任务执行完以后,浏览器才会刷新,也就是下一个宏任务之始。

const main = document.getElementById('main');
const frg = document.createDocumentFragment();

for(let i = 0; i < 10; i++) {
  const li = document.createElement('li');
  li.innerHTML = i;
  frg.appendChild(li);
}
main.appendChild(frg);

new Promise((resolve) => {
  resolve();
}).then(() => {
  console.log('微任务已经执行');
  alert('dom 还未插入')
});

setTimeout(() => {
  console.log('宏任务执行');
  alert('dom 已经插入')
});

6.  真实的元素是怎么被对应到每个vnode的elm上的

首先根元素会指定一个div,这个能对应到。

然后每一次创建元素,创建完以后,立刻就会被挂载当前vnode的elm上。createElm函数内:

vnode.elm = vnode.ns
            ? nodeOps.createElementNS(vnode.ns, tag)
            : nodeOps.createElement(tag, vnode);

从此以后,删除,修改,就都能对应得到真实的元素了。至于新增,新增是插入操作,能定位到对应的父元素以及新增元素的index索引,所以也能定位到新增到什么位置、

7.  各种操作dom是什么流程(删除,新增,追加,文本替换),代码体现在哪里,真实的dom怎么找到的

已找到,在patchVnode里面有一部分:新增,删除,文本替换

addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
nodeOps.setTextContent(elm, '')

在updateChildren里面有一部分:移动,创建,删除

canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// ...

最终调用的都是src\platforms\weex\runtime\node-ops.js文件里面的封装过的操作dom的api,为什么要把这些操作dom的js 写到另一个文件夹里面,为了跨平台。

至于怎么就虚拟dom和真是的dom对应起来了,是因为传参,在生成vnode对象里面就有对真实dom的映射了,然后记录传参到patch里面。就是vnode的Elm属性,记录的就是真实的dom。每一个元素都有一个对应的vnode,就都有Elm属性。创建vnode是在render函数里面执行的,render函数是编译生成的,编译那一块再去做响应了解。

import { createTextVNode, createEmptyVNode } from 'core/vdom/vnode'

target._v = createTextVNode

8.  属性更新流程,数据是vnode的那个数据,怎么更新的反馈到dom上的

2.  patch流程

1.  patch(判断vnode是否存在和同一个)

createPatchFunction返回patch函数,采用工厂函数的形式。patch主要判断新老vnode是否存在,一方存在一方不存在的情况,直接更新就完了,都存在判断是否同一个vnode,不同还是直接更新就完了,同一个就进行下一步比较(patchVnode)。最终返回的是真实的dom。patch只会在组件的根元素比较一次,里面所有子元素都会走进patchvnode里面二次比较。

1.  新节点不存在:直接删除老节点对应的dom

2.  老节点不存在:直接创建新的节点

3.  新的老的节点都存在:

1)  oldVnode传的是真实的dom(进行初始化操作)

a.  创建空节点,替换为oldVnode

b.  找到当前oldVnode对应的真实的dom的父元素

c.  根据newVnode创建真实的dom(当前vnode所有节点创建完一次性追加)

d.  把真实的dom追加到父元素上

e.  如果有老的dom,则删掉(就是删掉模板)

2)  oldVnode是虚拟dom(执行diff操作,patchVnode函数)

  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // 新的虚拟dom不存在,直接删除操作
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
    let isInitialPatch = false
    const insertedVnodeQueue = []
    // 老节点不存在:直接创建新的节点
    if (isUndef(oldVnode)) {
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) { // 比对新旧家电是否一样
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } else {
        // 如果新旧节点不同,则做一下三个步骤
        // 1. 创建新的节点
        // 2. 更新父的占位符节点
        // 3. 删除旧的节点
        if (isRealElement) { // 真实的dom,初始化oldVnode
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            // todo
          }
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }
        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes(parentElm, [oldVnode], 00)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

2.  patchVnode(已确定新老是同一个vnode的前提)

patchVnode是已经确定新老vnode是同一个的前提下,做进一步判断和更新。

三种操作:属性更新,文本更新,子节点更新。

注意:patch外层的函数vm._update是异步的,在微任务中执行,因为nextTick。但此时已经是在nextTick里面,此时执行的操作真实dom都是同步的。但是最终在显示器上看到效果需要等待此次js事件循环结束(因为js线程和渲染线程互斥,js线程一个事件循环结束后,渲染dom的权利才会交给GUI线程)

1.  如果是静态节点,占位符等,直接return,不进行diff比较和更新。

2.  执行组件的钩子(不是生命周期的钩子)

3.  属性更新(isPtachable)

4.  判断新的vnode子节点是否是元素(不是文本就都算是元素)。具体规则如下:

1.  新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren(深度优先的体现)

2.  老节点没有子节点而新节点有子节点,先清空老节点文本(也许以前人家是文本节点),然后为其新增子节点

3.  新节点没有子节点,而老节点有子节点,则移除该节点下的所有子节点

4.  新老节点都没有子节点,只是文本替换

  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { 
    if (oldVnode === vnode) {
      return
    }
    const elm = vnode.elm = oldVnode.elm
    // 占位符,直接return
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }
    // 静态节点,直接return
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    // 属性更新
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // 以下节点操作
    if (isUndef(vnode.text)) { // 只要没有文本,就都默认是元素节点
      // 新的老的都有孩子
      if (isDef(oldCh) && isDef(ch)) {
        // 比孩子,diff算法发生的地方
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) { // 老节点没孩子(可能有文本),新节点有孩子
        // 清空老节点文本
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        // 创建孩子并追加
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 老节点有孩子新节点没有,删除
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // 老节点存在文本,删除即可
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // 双方都是文本节点且长的不一致,替换文本即可
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

3. updateChildren(已知父vnode相同且有子vnode比较子vnode)

updateChildren是比孩子,已知父级vnode是同一个vnode的前提下,且双方都有子元素的前提下,比较子元素的异同。采用while循环,优先比较首尾元素的策略。

注意:updateChildren是比较同级一排元素的有哪些相同,以及移动此排元素,子元素的比较和更新又递归在patchVnode里面去做。

updateChildren里面又会再次调用patchVnode,只有有孩子就一直这样递归调用,patchVnode是在已经确定当前新旧两个vnode是同一个的情况下做的操作,而updateChildren是在同一个父节点下比较所有所有子节点的情况,子节点如果有任何不同,则又会走patchVnode,如果双方都没有子元素,且双反的文本是相同,那就什么都不做,略过,在patchVnode函数里面体现。

1.  while循环比较

情况1:新老开始节点是同一个节点(例如在结尾追加元素的情况)

 继续深度这两个节点的子节点,递归到patchVnode方法

指针++oldStartIdx,++newStartIdx

情况2:新老结尾节点是同一个(例如在开头追加元素的情况)

继续深度这两个节点的子节点,递归到patchVnode方法

指针--oldEndIdx,--newEndIdx

情况3:老的开头和新的结尾是同一个元素(例如排序反了)

继续深度这两个节点的子节点,递归到patchVnode方法

把老了开头对应的那个真实的dom移动到最末尾

指针++oldStartIdx,--newEndIdx

情况4:老的结尾和新的开头是同一个元素(排序反了,还在新的里面追加了元素)

继续深度这两个节点的子节点,递归到patchVnode方法

把老了结尾对应的那个真实的dom移动到最开头

指针--oldE ndIdx,++newStartIdx

情况5:以上猜测都没猜对,老老实实走双循环遍历。在oldVnode中找到与newStartVnode满足sameVnode的vnode,若存在则执行patchVnode,同时将vnodeToMove对应的dom移动到oldStartVnode对应的dom的前面。当然也有可能newStartVnode在oldVnode节点中找不到一致的key,或者即便key相同却不是sameVnode,这个时候会调用createEle创建一个新的dom节点。

2.  while循环比较完以后

while比较完后,(index走完了)两组vnode可能长度不一致(元素增加或者删除的情况)

新节点多出来则增加,新节点少了则删除

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0
    let 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, idxInOld, vnodeToMove, refElm

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          // 没有找到则创建新元素
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // 找到了,除了打补丁,还要移动到队首
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

3.  diff算法用文字描述(面试介绍)

1.  vue和react的diff算法区别

2.  流程

其实patch流程里面都有介绍,但是这里更加侧重面试总结,以像别人介绍的角度

wwwh答法

w是什么

w为什么用他,好处(性能,跨平台等)

w在什么地方使用(patch方法里,两份虚拟dom比较)

h怎么比的,怎么执行的(重点)

深度优先同级比较

重排算法(假设首尾相同)