在前面的博客中,我浅薄的学习了Vue 源码中 的 diff 以及 对 key 的使用,现在 再来学习一下更加难以理解的 patch 的过程

vue 在 虚拟 dom 这一块,是参照了 snabbdom.js 然后在上面进行了部分的修改的,所以如果有不理解的,推荐先去学习这一个东西

1、patch 函数

  1. 在这一系列的函数里面,有很多的钩子函数,类似于 destory 和 create 、insert 的 钩子函数,事实上 这些 钩子函数 用户使用的时候是无知觉的,因为 这个是虚拟 dom 的组件的 钩子函数,类似于 style 组件,事件 on 组件,这些 钩子函数
  2. 在 patch 函数里,调用了 核心函数 patchVnode 函数,也正是 这个函数,调用了 diff :updateChildren 函数

  3. 主要函数判断了老节点是否存在,然后执行销毁或者创建,然后执行 patchVnode

// 用于 比较 新老节点的不同,然后更新的 函数
function patch (oldVnode, vnode, hydrating, removeOnly) {
    // 当新节点不存在的时候,销毁旧节点
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    // 用来存储 insert 钩子函数,在 插入节点之后调用
    const insertedVnodeQueue = []
    // 如果旧节点 是未定义的,直接创建新节点
    if (isUndef(oldVnode)) {
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      // 当老节点不是真实的 dom 节点, 当两个节点是相同节点的时候,进入 patctVnode 的过程
      // 而 patchVnode 也是 传说中 diff updateChildren 的调用者
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // 当老节点是真实存在的 dom 节点的时候
        if (isRealElement) {
          // 当 老节点是 真实节点,而是在 ssr 环境的时候,修改 SSR_ATTR 属性
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          ....
          // 设置 oldVnode 为一个包含 oldVnode 的无属性节点
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        // 获取父亲节点,这样方便 删除或者增加节点
        const parentElm = nodeOps.parentNode(oldElm)

        // 在 dom 中插入新节点
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // 递归 更新父占位符元素
        // 就是执行一遍 父节点的 destory 和 create 、insert 的 钩子函数
        // 类似于 style 组件,事件组件,这些 钩子函数
        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
          }
        }

        // 销毁老节点
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          // 触发老节点 的 destory 钩子
          invokeDestroyHook(oldVnode)
        }
      }
    }
    // 执行 虚拟 dom 的 insert 钩子函数
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    // 返回最新 vnode 的 elm ,也就是真实的 dom节点
    return vnode.elm
  }

2、patchVnode 函数

patchVnode 函数是对比 两个虚拟 dom 不同的地方, 同时 也是 递归 调用 updateChildren 的 函数

  1. 首先看是否是同一个节点、是否是 注释节点、是否是 异步函数组件、是否是静态节点,是否是 once 的节点,是的话,不执行
  2. 是否存在子节点,然后新老节点的子节点是否相同,否就执行 updateChildren 函数深入对比
  3. 否则就查看新老节点是否存在,新节点存在但是老节点不存在,增加,新节点不存在但是老节点存在,删除
  4. 检查 新老节点的 text 属性是否一致,否的话,就更新
function patchVnode (
    oldVnode,  // 旧节点
    vnode,     // 新节点
    insertedVnodeQueue,  // 插入节点的队列
    ownerArray,      // 节点 数组
    index,           // 当前 节点的
    removeOnly       // 只有在 patch 函数中被传入,当老节点不是真实的 dom 节点,当新老节点是相同节点的时候
  ) {
    // 如果新节点和旧节点 相等(使用了 同一个地址,直接返回不进行修改)
    // 这里就是 当 props 没有改变的时候,子组件不会做渲染,而是直接复用
    if (oldVnode === vnode) {
      return
    }

    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
    }

    // 当前节点 是一个静态节点的时候,或者 标记了 once 的时候,那不执行
    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
    // 调用 prepatch 的钩子函数
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    // 调用 update 钩子函数
    if (isDef(data) && isPatchable(vnode)) {
      // 这里 的 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)
    }
    // 新节点 没有 text 属性
    if (isUndef(vnode.text)) {
      // 如果都有子节点,对比更新子节点
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) { // 新节点存在,但是老节点不存在
        // 如果老节点是  text, 清空
        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)) { // 如果老节点是  text, 清空
        nodeOps.setTextContent(elm, '')
      }
       // 新旧节点 text 属性不一样
    } else if (oldVnode.text !== vnode.text) {
      // 将 text 设置为 新节点的 text
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      // 执行 postpatch 钩子函数
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

3、createElm,将 vnode 转换为 真实的 dom 节点

  1. 判断当前的 vnode tag 标签是否是存在的
  2. 如果存在,创建对应的节点,然后 设置 样式的 作用域 
  3. 遍历子元素,并插入节点之中
  4. 触发 create 钩子函数
  5. 如果tag 标签不存在,判断是否是 注释节点,然后创建
  6. 如果tag 标签不存在,且不是 注释节点,直接创建文本节点
// 把 vnode 转换为 真实的 dom,挂载到 节点上
  function createElm (
    vnode,               // vnode
    insertedVnodeQueue,  // inserted 钩子函数 
    parentElm,
    refElm,              // 如果这个存在的话,就插到这个节点之前
    nested,
    ownerArray,
    index
  ) {
    // 如果存在子节点的话,就会克隆一遍
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // This vnode was used in a previous render!
      // now it's used as a new node, overwriting its elm would cause
      // potential patch errors down the road when it's used as an insertion
      // reference node. Instead, we clone the node on-demand before creating
      // associated DOM element for it.
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    vnode.isRootInsert = !nested // for transition enter check
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    // 是否是标签
    if (isDef(tag)) {
      if (process.env.NODE_ENV !== 'production') {
        if (data && data.pre) {
          creatingElmInVPre++
        }
        // 如果是一个未定义标签
        if (isUnknownElement(vnode, creatingElmInVPre)) {
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
            vnode.context
          )
        }
      }
      // 是否有 命名空间,主要是 svg
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)  // 设置样式的作用域 

      /* istanbul ignore if */
      if (__WEEX__) {
        。。。
      } else {
        // 把子元素设置为  vnode 的对象
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          // 触发 create 钩子函数
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        // 将 创建好的 vnode 插入到 parent 中,如果 refElm 存在的话,就插入到 refElm 元素之前
        insert(parentElm, vnode.elm, refElm)
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        creatingElmInVPre--
      }
    } else if (isTrue(vnode.isComment)) {  // 是否是注释节点
      vnode.elm = nodeOps.createComment(vnode.text)  // 创建 注释的文本 节点
      insert(parentElm, vnode.elm, refElm)
    } else {  // Text 标签
      vnode.elm = nodeOps.createTextNode(vnode.text)  // 创建 文本节点
      insert(parentElm, vnode.elm, refElm)
    }
  }

 

Logo

前往低代码交流专区

更多推荐