Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

patch过程中的diff规则梳理 #73

Open
adodo0829 opened this issue May 22, 2020 · 0 comments
Open

patch过程中的diff规则梳理 #73

adodo0829 opened this issue May 22, 2020 · 0 comments

Comments

@adodo0829
Copy link
Owner

patch 过程中的 diff 算法

这里经常会问 template 会什么只能有一个根节点?
参考这里
设计如此, 因为template 会转化为 vNode, 而 diff 算法是比较的 两颗 vnode 树;
一个 tree 结构能有两个根吗? emm...

还有 key 是用来干嘛的?

patch 方法

我们来看一下 patch 方法,patch会将新老VNode节点进行比对,然后将根据两者的比较结果进行最小单位地修改视图,而不是将整个视图根据新的VNode重绘

function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    // vnode不存在则直接调用销毁钩子
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // 初始态 patch, 老节点不存在
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue, parentElm, refElm) // 生成 DOM
    } else {
      const isRealElement = isDef(oldVnode.nodeType)

      /*
       * 只有新旧VNode节点判定为同一节点的时候才会进行patchVnode这个过程
       * 否则就是创建新的DOM,移除旧的DOM
       * diff 过程在patchVnode里执行
       */

      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } 
      else {
        if (isRealElement) {
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            // 当旧的VNode是服务端渲染的元素,hydrating记为true
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          // 如果不是服务端渲染或者合并到真实DOM失败,则创建一个空的VNode节点替换它
          // 创建空节点
          oldVnode = emptyNodeAt(oldVnode)
        }
        // 省略...

        // 移除旧节点
        if (isDef(parentElm)) {
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    // 省略...
  }

patchVnode 方法

patch 前置条件

  • 1.oldVnode存在
  • 2.新旧vNode是否一致
/*
  判断两个VNode节点是否是同一个节点,需要满足以下条件
  key相同
  tag(当前节点的标签名)相同
  isComment(是否为注释节点)相同
  是否data(当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型
  当标签是<input>的时候,type必须相同
*/
function sameVnode (a, b) {
  return (
    a.key === b.key &&
    a.tag === b.tag &&
    a.isComment === b.isComment &&
    isDef(a.data) === isDef(b.data) &&
    sameInputType(a, b)
  )
}
function sameInputType (a, b) {
  if (a.tag !== 'input') return true
  let i
  // 判断当标签是<input>的时候,type是否相同
  const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
  const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
  return typeA === typeB
}

patch 过程

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    // 1.两个VNode节点相同则直接返回, 直接使用老节点
    if (oldVnode === vnode) {
      return
    }
    
    // 2.如果新旧节点满足 static,key,once: 新替换旧,直接赋值
    if (isTrue(vnode.isStatic) &&    // 如果新旧VNode都是静态的
        isTrue(oldVnode.isStatic) && 
        vnode.key === oldVnode.key && // 同时它们的key相同
        // 新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次)
        (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) 
      ) 
    { 
      // 只需要替换真实elm以及componentInstance
      vnode.elm = oldVnode.elm 
      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 elm = vnode.elm = oldVnode.elm

    const oldCh = oldVnode.children // 老节点的子节点
    const ch = vnode.children       // 新节点的子节点

    // 调用update回调以及update钩子
    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)
    }

    // 3.当新节点没有 text 时
    if (isUndef(vnode.text)) {
      // 1.新老节点均有children子节点
      if (isDef(oldCh) && isDef(ch)) {
        // 对比子节点, 不相同则进行 diff
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } 
      // 2.老节点没有子节点而新节点存在子节点
      else if (isDef(ch)) {
        // 先清空elm的文本内容,然后为当前节点加入子节点
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } 
      // 3.当新节点没有子节点而老节点有子节点
      else if (isDef(oldCh)) {
        // 移除所有elm的子节点
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } 
      // 4.当新老节点都无子节点的时候
      else if (isDef(oldVnode.text)) {
        // 只是文本的替换,因为这个逻辑中新节点text不存在,所以直接去除ele的文本
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // 当新老节点text不一样时,直接替换这段文本
      nodeOps.setTextContent(elm, vnode.text)
    }

    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

patch 子节点方法 updateChildren

// 新老节点均有children子节点时进入这一步
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, elmToMove, refElm // 临时变量

    // during leaving transitions
    const canMove = !removeOnly

    // 如果存在key,并且满足sameVnode,会将该DOM节点进行复用,否则则会创建一个新的DOM节点
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        // 老节点头指针 右移
        oldStartVnode = oldCh[++oldStartIdx] 
      } else if (isUndef(oldEndVnode)) {
        // 老节点尾指针 左移
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 1.比较新旧头指针节点
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 2.比较新旧尾指针节点
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
        // 3.比较旧头 和 新尾
        // 说明 oldStartVnode 已经跑到了 oldEndVnode后面, 同时真实 DOM 位移
        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)) {
        // 4.比较旧尾 和 新头
        // 说明oldEndVnode跑到了oldStartVnode的前面,进行patchVnode的同时真实的DOM节点移动到了oldStartVnode的前面
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        /*
          以上4种情况均不符合时:
          生成一个key与旧VNode的key对应的哈希表(只有第一次进来undefined的时候会生成,也为后面检测重复的key值做准备)
          比如childre是这样的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}]  
          beginIdx = 0   endIdx = 2  
          结果生成{key0: 0, key1: 1, key2: 2}
        */
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // 如果newStartVnode新的VNode节点存在key并且这个key在oldVnode中能找到
        // 则返回这个节点的idxInOld(即第几个节点,下标)
        idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
        if (isUndef(idxInOld)) { 
          // newStartVnode没有key或者是该key没有在老节点中找到则创建一个新的节点
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
          newStartVnode = newCh[++newStartIdx]
        } else {
          // 获取相同key的老节点
          elmToMove = oldCh[idxInOld]
          if (process.env.NODE_ENV !== 'production' && !elmToMove) {
            // 如果elmToMove不存在说明之前已经有新节点放入过这个key的DOM中,提示可能存在重复的key,
            // 确保v-for的时候item有唯一的key值
            warn(
              'It seems there are duplicate keys that is causing an update error. ' +
              'Make sure each v-for item has a unique key.'
            )
          }
          if (sameVnode(elmToMove, newStartVnode)) {
            // 如果新VNode与得到的有相同key的节点是同一个VNode则进行patchVnode
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            // 因为已经patchVnode进去了,所以将这个老节点赋值undefined,
            // 之后如果还有新节点与该节点key相同可以检测出来提示已有重复的key
            oldCh[idxInOld] = undefined
            // 当有标识位canMove实可以直接插入oldStartVnode对应的真实DOM节点前面
            canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]
          } else {
            // 当新的VNode与找到的同样key的VNode不是sameVNode的时候
            // 比如说tag不一样或者是有不一样type的input标签),创建一个新的节点
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]
          }
        }
      }
    }
    if (oldStartIdx > oldEndIdx) {
      // 全部比较完成以后,发现oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多,
      // 所以这时候多出来的新节点需要一个一个创建出来加入到真实DOM中
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      // 如果全部比较完成以后发现newStartIdx > newEndIdx,则说明新节点已经遍历完了,老节点多余新节点,
      // 这个时候需要将多余的老节点从真实DOM中移除
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

diff 规则小结

对比新旧 vnode 对象, 新旧替换的过程(如何减少新旧替换的开销和效率),有一套规则

1.前置条件: 新旧 vnode 相同?
  相同则patch, 不同则销毁旧创建新

2.进入 patch 过程(5种情况)
  - 1.老新节点都是静态的,同时它们的key相同(代表同一节点),并且新的VNode是clone或者是标记了v-once:
    只替换elm以及componentInstance

  - 2.老有儿子,新的没儿子:
    移除该DOM节点的所有子节点

  - 3.老没儿子,新的有儿子:
    先清空老节点DOM的文本内容,然后为该DOM节点加入子节点

  - 4.老,新都没儿子:
    只是文本的替换

  - 5.新老节点均有儿子(diff): 进入diff过程
    遍历老新节点对象并比较同层的树节点, 然后递归比较子节点对象
    通过判断 key 和 sameVnode, 是否将该DOM节点进行复用,否则则会创建一个新的DOM节点
    - 1.如果只是节点移动
      只是位移节点, 同时位移 DOM 元素

    - 2.如果有增删改
      会生成一个key与旧VNode的key对应的哈希表, 判断新VNode 中是否有 key与旧的比对,
      进而进行创建或者移除操作
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant