zoukankan      html  css  js  c++  java
  • Diff 算法核心原理——源码

    在 Vue 里面Diff 算法就是 patch

    一、patch(源码地址:src/core/vdom/patch.js -700行)

    其实 patch 就是一个函数,先介绍一下源码里的核心流程,再来看一下 patch 的源码,源码里每一行也有注释

    1、可以接收四个参数,主要还是前两个

    • oldVnode:老的虚拟 DOM 节点
    • vnode:新的虚拟 DOM 节点
    • hydrating:是不是要和真实 DOM 混合,服务端渲染的话会用到,这里不过多说明
    • removeOnly:transition-group 会用到,这里不过多说明

    2、主要流程是这样的:

    • vnode 不存在,oldVnode 存在,就删掉 oldVnode
    • vnode 存在,oldVnode 不存在,就创建 vnode
    • 两个都存在的话,通过 sameVnode 函数(后面有详解)对比是不是同一节点

      • 如果是同一节点的话,通过 patchVnode 进行后续对比节点文本变化或子节点变化
      • 如果不是同一节点,就把 vnode 挂载到 oldVnode 的父元素下

        • 如果组件的根节点被替换,就遍历更新父节点,然后删掉旧的节点
        • 如果是服务端渲染就用 hydrating 把 oldVnode 和真实 DOM 混合

    下面看完整的 patch 函数源码,注释里有说明:

      1 // 两个判断函数
      2 function isUndef (v: any): boolean %checks {
      3   return v === undefined || v === null
      4 }
      5 function isDef (v: any): boolean %checks {
      6   return v !== undefined && v !== null
      7 }
      8 return function patch (oldVnode, vnode, hydrating, removeOnly) {
      9     // 如果新的 vnode 不存在,但是 oldVnode 存在
     10     if (isUndef(vnode)) {
     11       // 如果 oldVnode 存在,调用 oldVnode 的组件卸载钩子 destroy
     12       if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
     13       return
     14     }
     15 
     16     let isInitialPatch = false
     17     const insertedVnodeQueue = []
     18     
     19     // 如果 oldVnode 不存在的话,新的 vnode 是肯定存在的,比如首次渲染的时候
     20     if (isUndef(oldVnode)) {
     21       isInitialPatch = true
     22       // 就创建新的 vnode
     23       createElm(vnode, insertedVnodeQueue)
     24     } else {
     25       // 剩下的都是新的 vnode 和 oldVnode 都存在的话
     26       
     27       // 是不是元素节点
     28       const isRealElement = isDef(oldVnode.nodeType)
     29       // 是元素节点 && 通过 sameVnode 对比是不是同一个节点 (函数后面有详解)
     30       if (!isRealElement && sameVnode(oldVnode, vnode)) {
     31         // 如果是 就用 patchVnode 进行后续对比 (函数后面有详解)
     32         patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
     33       } else {
     34         // 如果不是同一元素节点的话
     35         if (isRealElement) {
     36           // const SSR_ATTR = 'data-server-rendered'
     37           // 如果是元素节点 并且有 'data-server-rendered' 这个属性
     38           if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
     39             // 就是服务端渲染的,删掉这个属性
     40             oldVnode.removeAttribute(SSR_ATTR)
     41             hydrating = true
     42           }
     43           // 这个判断里是服务端渲染的处理逻辑,就是混合
     44           if (isTrue(hydrating)) {
     45             if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
     46               invokeInsertHook(vnode, insertedVnodeQueue, true)
     47               return oldVnode
     48             } else if (process.env.NODE_ENV !== 'production') {
     49               warn('这是一段很长的警告信息')
     50             }
     51           }
     52           // function emptyNodeAt (elm) {
     53           //    return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
     54           //  }
     55           // 如果不是服务端渲染的,或者混合失败,就创建一个空的注释节点替换 oldVnode
     56           oldVnode = emptyNodeAt(oldVnode)
     57         }
     58         
     59         // 拿到 oldVnode 的父节点
     60         const oldElm = oldVnode.elm
     61         const parentElm = nodeOps.parentNode(oldElm)
     62         
     63         // 根据新的 vnode 创建一个 DOM 节点,挂载到父节点上
     64         createElm(
     65           vnode,
     66           insertedVnodeQueue,
     67           oldElm._leaveCb ? null : parentElm,
     68           nodeOps.nextSibling(oldElm)
     69         )
     70         
     71         // 如果新的 vnode 的根节点存在,就是说根节点被修改了,就需要遍历更新父节点
     72         if (isDef(vnode.parent)) {
     73           let ancestor = vnode.parent
     74           const patchable = isPatchable(vnode)
     75           // 递归更新父节点下的元素
     76           while (ancestor) {
     77             // 卸载老根节点下的全部组件
     78             for (let i = 0; i < cbs.destroy.length; ++i) {
     79               cbs.destroy[i](ancestor)
     80             }
     81             // 替换现有元素
     82             ancestor.elm = vnode.elm
     83             if (patchable) {
     84               for (let i = 0; i < cbs.create.length; ++i) {
     85                 cbs.create[i](emptyNode, ancestor)
     86               }
     87               const insert = ancestor.data.hook.insert
     88               if (insert.merged) {
     89                 for (let i = 1; i < insert.fns.length; i++) {
     90                   insert.fns[i]()
     91                 }
     92               }
     93             } else {
     94               registerRef(ancestor)
     95             }
     96             // 更新父节点
     97             ancestor = ancestor.parent
     98           }
     99         }
    100         // 如果旧节点还存在,就删掉旧节点
    101         if (isDef(parentElm)) {
    102           removeVnodes([oldVnode], 0, 0)
    103         } else if (isDef(oldVnode.tag)) {
    104           // 否则直接卸载 oldVnode
    105           invokeDestroyHook(oldVnode)
    106         }
    107       }
    108     }
    109     // 返回更新后的节点
    110     invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    111     return vnode.elm
    112   }

    3、sameVnode

    源码地址:src/core/vdom/patch.js -35行

    这个是用来判断是不是同一节点的函数,源码:

    function sameVnode (a, b) {
      return (
        a.key === b.key &&  // key 是不是一样
        a.asyncFactory === b.asyncFactory && ( // 是不是异步组件
          (
            a.tag === b.tag && // 标签是不是一样
            a.isComment === b.isComment && // 是不是注释节点
            isDef(a.data) === isDef(b.data) && // 内容数据是不是一样
            sameInputType(a, b) // 判断 input 的 type 是不是一样
          ) || (
            isTrue(a.isAsyncPlaceholder) && // 判断区分异步组件的占位符否存在
            isUndef(b.asyncFactory.error)
          )
        )
      )
    }

    4、patchVnode

    源码地址:src/core/vdom/patch.js -501行

    这个是在新的 vnode 和 oldVnode 是同一节点的情况下,才会执行的函数,主要是对比节点文本变化或子节点变化

    4.1、主要流程是这样的:

    • 如果 oldVnode 和 vnode 的引用地址是一样的,就表示节点没有变化,直接返回
    • 如果 oldVnode 的 isAsyncPlaceholder 存在,就跳过异步组件的检查,直接返回
    • 如果 oldVnode 和 vnode 都是静态节点,并且有一样的 key,并且 vnode 是克隆节点或者 v-once 指令控制的节点时,把 oldVnode.elm 和 oldVnode.child 都复制到 vnode 上,然后返回
    • 如果 vnode 不是文本节点也不是注释的情况下

      • 如果 vnode 和 oldVnode 都有子节点,而且子节点不一样的话,就调用 updateChildren 更新子节点
      • 如果只有 vnode 有子节点,就调用 addVnodes 创建子节点
      • 如果只有 oldVnode 有子节点,就调用 removeVnodes 删除该子节点
      • 如果 vnode 文本为 undefined,就删掉 vnode.elm 文本
    • 如果 vnode 是文本节点但是和 oldVnode 文本内容不一样,就更新文本
      function patchVnode (
        oldVnode, // 老的虚拟 DOM 节点
        vnode, // 新的虚拟 DOM 节点
        insertedVnodeQueue, // 插入节点的队列
        ownerArray, // 节点数组
        index, // 当前节点的下标
        removeOnly // 只有在
      ) {
        // 新老节点引用地址是一样的,直接返回
        // 比如 props 没有改变的时候,子组件就不做渲染,直接复用
        if (oldVnode === vnode) return
        
        // 新的 vnode 真实的 DOM 元素
        if (isDef(vnode.elm) && isDef(ownerArray)) {
          // clone reused vnode
          vnode = ownerArray[index] = cloneVNode(vnode)
        }
    
        const elm = vnode.elm = oldVnode.elm
        // 如果当前节点是注释或 v-if 的,或者是异步函数,就跳过检查异步组件
        if (isTrue(oldVnode.isAsyncPlaceholder)) {
          if (isDef(vnode.asyncFactory.resolved)) {
            hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
          } else {
            vnode.isAsyncPlaceholder = true
          }
          return
        }
        // 当前节点是静态节点的时候,key 也一样,或者有 v-once 的时候,就直接赋值返回
        if (isTrue(vnode.isStatic) &&
          isTrue(oldVnode.isStatic) &&
          vnode.key === oldVnode.key &&
          (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
        ) {
          vnode.componentInstance = oldVnode.componentInstance
          return
        }
        // hook 相关的不用管
        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)) {
          // 遍历调用 update 更新 oldVnode 所有属性,比如 class,style,attrs,domProps,events...
          // 这里的 update 钩子函数是 vnode 本身的钩子函数
          for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
          // 这里的 update 钩子函数是我们传过来的函数
          if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
        }
        // 如果新节点不是文本节点,也就是说有子节点
        if (isUndef(vnode.text)) {
          // 如果新老节点都有子节点
          if (isDef(oldCh) && isDef(ch)) {
            // 如果新老节点的子节点不一样,就执行 updateChildren 函数,对比子节点
            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(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)) {
          // 执行 postpatch 钩子
          if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
        }
      }

    5、updateChildren

    源码地址:src/core/vdom/patch.js -404行

    这个是新的 vnode 和 oldVnode 都有子节点,且子节点不一样的时候进行对比子节点的函数,这里很关键,很关键!

    比如现在有两个子节点列表对比,对比主要流程如下:

    循环遍历两个列表,循环停止条件是:其中一个列表的开始指针 startIdx 和 结束指针 endIdx 重合

    循环内容是:{

    • 新的头和老的头对比
    • 新的尾和老的尾对比
    • 新的头和老的尾对比
    • 新的尾和老的头对比。 这四种对比如图

     

    以上四种只要有一种判断相等,就调用 patchVnode 对比节点文本变化或子节点变化,然后移动对比的下标,继续下一轮循环对比

    如果以上四种情况都没有命中,就不断拿新的开始节点的 key 去老的 children 里找

    • 如果没找到,就创建一个新的节点
    • 如果找到了,再对比标签是不是同一个节点

      • 如果是同一个节点,就调用 patchVnode 进行后续对比,然后把这个节点插入到老的开始前面,并且移动新的开始下标,继续下一轮循环对比
      • 如果不是相同节点,就创建一个新的节点

      }

    • 如果老的 vnode 先遍历完,就添加新的 vnode 没有遍历的节点
    • 如果新的 vnode 先遍历完,就删除老的 vnode 没有遍历的节点

    为什么会有头对尾,尾对头的操作?

    因为可以快速检测出 reverse 操作,加快 Diff 效率

    function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
        let oldStartIdx = 0 // 老 vnode 遍历的下标
        let newStartIdx = 0 // 新 vnode 遍历的下标
        let oldEndIdx = oldCh.length - 1 // 老 vnode 列表长度
        let oldStartVnode = oldCh[0] // 老 vnode 列表第一个子元素
        let oldEndVnode = oldCh[oldEndIdx] // 老 vnode 列表最后一个子元素
        let newEndIdx = newCh.length - 1 // 新 vnode 列表长度
        let newStartVnode = newCh[0] // 新 vnode 列表第一个子元素
        let newEndVnode = newCh[newEndIdx] // 新 vnode 列表最后一个子元素
        let oldKeyToIdx, idxInOld, vnodeToMove, refElm
    
        const canMove = !removeOnly
        
        // 循环,规则是开始指针向右移动,结束指针向左移动移动
        // 当开始和结束的指针重合的时候就结束循环
        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, newCh, newStartIdx)
            // 然后把指针后移一位,从前往后依次对比
            // 比如第一次对比两个列表的[0],然后比[1]...,后面同理
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
            
            // 老结束和新结束对比
          } else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            // 然后把指针前移一位,从后往前比
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
            
            // 老开始和新结束对比
          } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            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, newCh, newStartIdx)
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
            // 老的列表从后往前取值,新的列表从前往后取值,然后对比
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
            
            // 以上四种情况都没有命中的情况
          } else {
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
            // 拿到新开始的 key,在老的 children 里去找有没有某个节点有这个 key
            idxInOld = isDef(newStartVnode.key)
              ? oldKeyToIdx[newStartVnode.key]
              : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
              
            // 新的 children 里有,可是没有在老的 children 里找到对应的元素
            if (isUndef(idxInOld)) {
              /// 就创建新的元素
              createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
            } else {
              // 在老的 children 里找到了对应的元素
              vnodeToMove = oldCh[idxInOld]
              // 判断标签如果是一样的
              if (sameVnode(vnodeToMove, newStartVnode)) {
                // 就把两个相同的节点做一个更新
                patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                oldCh[idxInOld] = undefined
                canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
              } else {
                // 如果标签是不一样的,就创建新的元素
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
              }
            }
            newStartVnode = newCh[++newStartIdx]
          }
        }
        // oldStartIdx > oldEndIdx 说明老的 vnode 先遍历完
        if (oldStartIdx > oldEndIdx) {
          // 就添加从 newStartIdx 到 newEndIdx 之间的节点
          refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
          addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
        
        // 否则就说明新的 vnode 先遍历完
        } else if (newStartIdx > newEndIdx) {
          // 就删除掉老的 vnode 里没有遍历的节点
          removeVnodes(oldCh, oldStartIdx, oldEndIdx)
        }
      }

    至此,整个 Diff 流程的核心逻辑源码到这就结束了。

  • 相关阅读:
    【SSRS】入门篇(六) -- 分组和总计
    【SSRS】入门篇(五) -- 设置报表格式
    【SSRS】入门篇(四) -- 向报表添加数据
    【SSRS】入门篇(三) -- 为报表定义数据集
    【SSRS】入门篇(二) -- 建立数据源
    【SSRS】入门篇(一) -- 创建SSRS项目
    【MS SQL】数据库维护计划之数据库备份(二)
    【MS SQL】数据库维护计划之数据库备份(一)
    【MS SQL】查看任务执行进度
    c++ 在客户端的GCC使用
  • 原文地址:https://www.cnblogs.com/CandyDChen/p/15702045.html
Copyright © 2011-2022 走看看