patch 辅助函数

VNode/DOM 操作相关

sameVnode

/**
 * 判断两个 VNode 节点是否是同一种 VNode
 */
function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        // 若是元素类型的 VNode,则需要相同的元素标签;若是组件占位 VNode,则需要是相同组件的 VNode
        a.tag === b.tag &&
        // 都是注释 VNode,或都不是注释 VNode
        a.isComment === b.isComment &&
        // VNode 的 data 都定义了,或都没定义
        isDef(a.data) === isDef(b.data) &&
        // (对于 input 输入框来说),相同的输入类型
        sameInputType(a, b)
      ) || (
        // 对于异步组件占位 VNode 来说,工厂函数要完全相同;且新的异步组件占位 VNode 不能是失败状态
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

/**
 * 判断两个 VNode 是否是同一种 input 输入类型
 */
function sameInputType (a, b) {
  // 若不是 input 标签,返回 true
  if (a.tag !== 'input') return true
  let i
  const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
  const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
  // input 的 type 相同或者两个 input 都是文本输入类型
  return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
export const isTextInputType = makeMap('text,number,password,search,email,tel,url')
1

removeVnodes:移除子 VNode 及其 DOM 元素节点

  /**
   * 移除一批子 VNode 及其 DOM 元素节点
   * @param {Element} parentElm 父 DOM 元素节点
   * @param {Vnode} vnodes 要移除的子 VNode 数组
   * @param {Number} startIdx 要移除的开始索引(包含)
   * @param {Number} endIdx 要移除的结束索引(包含)
   */
  function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx]
      if (isDef(ch)) {
        if (isDef(ch.tag)) {
          // 移除 DOM 元素节点,并调用 remove 钩子
          removeAndInvokeRemoveHook(ch)
          // 移除 Vnode,并调用 destroy 钩子
          invokeDestroyHook(ch)
        } else { // Text node
          // vnode 没有 tag 属性,即为文本节点,则删除文本节点
          removeNode(ch.elm)
        }
      }
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

removeAndInvokeRemoveHook:移除 VNode 对应的 DOM 元素节点

  /**
   * (递归地)移除 VNode 对应的 DOM 元素节点
   * @param {Vnode} vnode Vnode 节点
   * @param {Function} rm 回调函数,在其中删除 DOM 元素节点
   */
  function removeAndInvokeRemoveHook (vnode, rm) {
    if (isDef(rm) || isDef(vnode.data)) {
      let i
      // 加一是因为除了要调用 cbs.remove 上的所有函数,还要执行 vnode.data.hook.remove 函数
      const listeners = cbs.remove.length + 1
      if (isDef(rm)) {
        // we have a recursively passed down rm callback
        // increase the listeners count
        rm.listeners += listeners
      } else {
        // directly removing
        rm = createRmCb(vnode.elm, listeners)
      }
      // recursively invoke hooks on child component root node
      if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {
        removeAndInvokeRemoveHook(i, rm)
      }
      for (i = 0; i < cbs.remove.length; ++i) {
        cbs.remove[i](vnode, rm)
      }
      if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
        i(vnode, rm)
      } else {
        rm()
      }
    } else {
      removeNode(vnode.elm)
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

removeNode:移除 DOM 元素节点

  /**
   * 移除 DOM 节点节点
   */
  function removeNode (el) {
    const parent = nodeOps.parentNode(el)
    // element may have already been removed due to v-html / v-text
    if (isDef(parent)) {
      nodeOps.removeChild(parent, el)
    }
  }
1
2
3
4
5
6
7
8
9
10

钩子函数

invokeCreateHooks

DOM 元素在创建好之后、插入到父元素之前,会调用create钩子。子组件实例在创建好之后、初始化子组件时、插入到父元素之前,也会调用create钩子。

createPatchFunction - 合并各模块的钩子函数里可以看到,每个模块都有create钩子,这些模块的create钩子处理新创建的 DOM 元素/子组件实例,包括:

  • 注册ref
  • 注册directives
  • 添加class特性
  • 添加style属性
  • 添加其他attrs特性
  • 添加原生事件处理
  • 添加dom-props,如textContent/innerHTML/value
  • (待补充)
  /**
   * 单个 DOM 元素节点创建好后,添加其 ref、directives、class、style 等等
   */
  function invokeCreateHooks (vnode, insertedVnodeQueue) {
    for (let i = 0; i < cbs.create.length; ++i) {

      cbs.create[i](emptyNode, vnode)
    }
    i = vnode.data.hook // Reuse variable
    if (isDef(i)) {
      if (isDef(i.create)) i.create(emptyNode, vnode)
      if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

此外,若 VNode 存在vnode.data.hook.create钩子,将调用该钩子;若存在vnode.data.hook.insert钩子,会将该 VNode 加入到父组件的insertedVnodeQueue数组里。

invokeDestroyHook:销毁 VNode

  /**
   * (递归地)销毁 VNode 节点及其子 VNode 节点
   */
  function invokeDestroyHook (vnode) {
    let i, j
    const data = vnode.data
    if (isDef(data)) {
      // 组件占位 VNode 的 destroy 钩子
      if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
      // 各个模块的 detroy 钩子
      for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
    }
    // 递归地销毁子 VNode
    if (isDef(i = vnode.children)) {
      for (j = 0; j < vnode.children.length; ++j) {
        invokeDestroyHook(vnode.children[j])
      }
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

组件占位 VNode 的 destroy 钩子

const componentVNodeHooks = {
  // ...
  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        // 销毁组件实例
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14