vue源码细读 -- 虚拟dom分解 之 patch.js 分解一

patch方法分解

patch方法的作用:对比新旧的vnode,并根据diff结果对真实dom进行调整。

return function patch (oldVnode, vnode, hydrating, removeOnly) {
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }

  let isInitialPatch = false
  const insertedVnodeQueue = []

  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // patch existing root node
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
      if (isRealElement) {
        // mounting to a real element
        // check if this is server-rendered content and if we can perform
        // a successful hydration.
        if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
          oldVnode.removeAttribute(SSR_ATTR)
          hydrating = true
        }
        if (isTrue(hydrating)) {
          if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
            invokeInsertHook(vnode, insertedVnodeQueue, true)
            return oldVnode
          } else if (process.env.NODE_ENV !== 'production') {
            warn(
              'The client-side rendered virtual DOM tree is not matching ' +
              'server-rendered content. This is likely caused by incorrect ' +
              'HTML markup, for example nesting block-level elements inside ' +
              'full client-side render.'
            )
          }
        }
        // either not server-rendered, or hydration failed.
        // create an empty node and replace it
        oldVnode = emptyNodeAt(oldVnode)
      }

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

      // create new node
      createElm(
        vnode,
        insertedVnodeQueue,
        // extremely rare edge case: do not insert if old element is in a
        // leaving transition. Only happens when combining transition +
        // keep-alive + HOCs. (#4590)
        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)
            }
            // #6513
            // invoke insert hooks that may have been merged by create hooks.
            // e.g. for directives that uses the "inserted" hook.
            const insert = ancestor.data.hook.insert
            if (insert.merged) {
              // start at index 1 to avoid re-invoking component mounted hook
              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], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
    }
  }

  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

第一个模块

if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
}

这个方法涉及到三个方法:isUndef,isDef,invokeDestroyHook

如果vnode不存在摒弃oldVnode存在,直接删除掉

  • isUndef:判断vnode是否存在,这里判断的是为undefined 或者null

          export function isUndef (v: any): boolean %checks {
            return v === undefined || v === null
          }
        
  • isDef:判断oldVnode是否不全等 undefined并且不全等null。如果新节点不存在并且旧节点存在

            export function isDef (v: any): boolean %checks {
              return v !== undefined && v !== null
            }
        
  • invokeDestroyHook: 递归销毁钩子函数

        function invokeDestroyHook (vnode) {
          let i, j
          const data = vnode.data
          if (isDef(data)) {
            if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
            for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
          }
          if (isDef(i = vnode.children)) {
            for (j = 0; j < vnode.children.length; ++j) {
              invokeDestroyHook(vnode.children[j])
            }
          }
        }
        

第二个模块

let isInitialPatch = false
const insertedVnodeQueue = []

if (isUndef(oldVnode)) {
  // empty mount (likely as component), create new root element
  isInitialPatch = true
  createElm(vnode, insertedVnodeQueue)
}

注释一下

// 声明初始化补丁isInitialPatch,插入的虚拟dom队列:insertedVnodeQueue
let isInitialPatch = false
const insertedVnodeQueue = []

// 如果旧的虚拟dom不存在
if (isUndef(oldVnode)) {
  // empty mount (likely as component), create new root element
  // 需要创建一个新的跟节点
  isInitialPatch = true
  // 创建节点
  createElm(vnode, insertedVnodeQueue)
}

第三个模块:旧的节点存在

const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
  // patch existing root node
  patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}

注释一下

  // 如果 oldVnode.nodeType存在,则判定为是真实节点
const isRealElement = isDef(oldVnode.nodeType)
  // 如果不是真实节点(即 oldVnode.nodeType不存在),并且新旧节点是相同的。翻译一下就是,如果新旧节点都不存在
if (!isRealElement && sameVnode(oldVnode, vnode)) {
  // patch existing root node
  // 调整虚拟dom(怎么调整的一会儿在看)
  patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}

第四个模块:是真实节点或者两个节点不相同

if (isRealElement) {
  // mounting to a real element
  // check if this is server-rendered content and if we can perform
  // a successful hydration.
  if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
    oldVnode.removeAttribute(SSR_ATTR)
    hydrating = true
  }
  if (isTrue(hydrating)) {
    if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
      invokeInsertHook(vnode, insertedVnodeQueue, true)
      return oldVnode
    } else if (process.env.NODE_ENV !== 'production') {
      warn(
        'The client-side rendered virtual DOM tree is not matching ' +
        'server-rendered content. This is likely caused by incorrect ' +
        'HTML markup, for example nesting block-level elements inside ' +
        'full client-side render.'
      )
    }
  }
  // either not server-rendered, or hydration failed.
  // create an empty node and replace it
  oldVnode = emptyNodeAt(oldVnode)
}

注释一下

  // 是真实节点
if (isRealElement) {
  // 如果旧节点的nodetype===1,并且旧节点有SSR_ATTR属性
  if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
    // 旧节点移除SSR_ATTR属性
    oldVnode.removeAttribute(SSR_ATTR)
    hydrating = true
  }
  // 如果 hydrating 为true
  if (isTrue(hydrating)) {
    if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
      invokeInsertHook(vnode, insertedVnodeQueue, true)
      return oldVnode
    } else if (process.env.NODE_ENV !== 'production') {
      warn(
        'The client-side rendered virtual DOM tree is not matching ' +
        'server-rendered content. This is likely caused by incorrect ' +
        'HTML markup, for example nesting block-level elements inside ' +
        'full client-side render.'
      )
    }
  }
// 清除oldVnode
  oldVnode = emptyNodeAt(oldVnode)
}

第五个模块:创建新节点

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

// create new node
createElm(
  vnode,
  insertedVnodeQueue,
  // extremely rare edge case: do not insert if old element is in a
  // leaving transition. Only happens when combining transition +
  // keep-alive + HOCs. (#4590)
  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)
      }
      // #6513
      // invoke insert hooks that may have been merged by create hooks.
      // e.g. for directives that uses the "inserted" hook.
      const insert = ancestor.data.hook.insert
      if (insert.merged) {
        // start at index 1 to avoid re-invoking component mounted hook
        for (let i = 1; i < insert.fns.length; i++) {
          insert.fns[i]()
        }
      }
    } else {
      registerRef(ancestor)
    }
    ancestor = ancestor.parent
  }
}

第七个模块

// 如果父节点存在
if (isDef(parentElm)) {
  // 删除父节点
  removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
  // 如果oldVnode.tag存在
  // 移除钩子
  invokeDestroyHook(oldVnode)
}
  ......

// 插入钩子
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
// 返回节点
return vnode.elm

整理

  • 一:如果vnode不存在并且oldVnode存在,则移除oldVnode中的钩子
  • 二:如果oldVnode不存在,则针对vnode创建新节点
  • 三:如果oldVnode存在,oldVnode.nodeType存在,并且oldVnode和vnode是相同vnode,则调整虚拟dom
  • 四:如果是真实节点,并且vnode和oldVnode不同,对vnode 插入钩子,清除oldVnode
  • 五:创建新的vnode节点
  • 六:递归更新父节点
  • 七:将vnode挂在钩子,并返回vnode.elm

随机浏览