切图妞

vuePress-theme-reco 切图妞    2020 - 2021
切图妞 切图妞
前端知识梳理
  • Vue
  • 浏览器 & 网络
  • HTML & CSS
  • Web安全
  • 算法
文章分类
  • 前端小麻烦
  • 配置乐园
  • 实战不完全手册
  • 手撕源码
宝藏女孩
  • 模板仓
  • 项目简介
  • GitHub
  • Segmentfault
  • CSDN
时间轴
author-avatar

切图妞

19

Article

18

Tag

前端知识梳理
  • Vue
  • 浏览器 & 网络
  • HTML & CSS
  • Web安全
  • 算法
文章分类
  • 前端小麻烦
  • 配置乐园
  • 实战不完全手册
  • 手撕源码
宝藏女孩
  • 模板仓
  • 项目简介
  • GitHub
  • Segmentfault
  • CSDN
时间轴

vue源码——patch. js

vuePress-theme-reco 切图妞    2020 - 2021

vue源码——patch. js

切图妞 2020-07-13 VueVue进阶

页面渲染时调用patch方法,将新旧vnode进行比较(diff算法),映射成实际DOM。

patch. js

页面渲染时调用patch方法,将新旧vnode进行比较(diff算法),映射成实际DOM。

# createElm方法

创建VNode节点的vnode. elm

  1. createComponent的作用是在创建组件实例(init钩子生成),初始化(initComponent方法初始化)并激活组件(insert方法激活)
  2. createChildren创建子节点,子节点为数组时循环调用createElm
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested) {
    // 创建一个组件节点
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
        return
    }
    const data = vnode.data;
    const childre = vnode.children;
    const tag = vnode.tag;
    // ...

    if (isDef(tag)) {
        vnode.elm = vnode.ns ?
            nodeOps.createElementNS(vnode.ns, tag) :
            nodeOps.createElement(tag, vnode)
        setScope(vnode)
        if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        createChildren(vnode, children, insertedVnodeQueue)
    } else if (isTrue(vnode.isComment)) {
        vnode.elm = nodeOps.createComment(vnode.text);
    } else {
        vnode.elm = nodeOps.createTextNode(vnode.te)
    }
    insert(parentElm, vnode.elm, refElm)
}
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

# sameVnode方法

判断两个节点是否相同

  • key相同
  • tag标签类型相同
  • isComment注释节点标示相同(都为注释节点或者都不为注释节点)
  • data值状态相同
  • 或者是存在异步占位符,异步工厂方法且报错的情况
  1. sameInputType方法判断input的type相同
  2. isDef判断是否为null/undefined
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)
            ) || (
                isTrue(a.isAsyncPlaceholder) &&
                a.asyncFactory === b.asyncFactory &&
                isUndef(b.asyncFactory.error)
            )
        )
    )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# patch方法

创建节点的过程是放入队列中统一处理 createComponent(会判断是否有children然后递归调用) createComment createTextNode

  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 ' +
                              '<p>, or missing <tbody>. Bailing hydration and performing ' +
                              '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([oldVnode], 0, 0)
              } else if (isDef(oldVnode.tag)) {
                  invokeDestroyHook(oldVnode)
              }
          }
      }

      invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
      return 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

# patchVnode方法

当两个节点相同时执行patchVnode方法。将旧节点的属性赋值给新节点的elm属性

 function patchVnode(
     oldVnode,
     vnode,
     insertedVnodeQueue,
     ownerArray,
     index,
     removeOnly
 ) {
     // 1. 如果新旧节点完全相同(引用相同 oldVnode === vnode)直接返回不处理
     if (oldVnode === vnode) {
         return
     }
     if (isDef(vnode.elm) && isDef(ownerArray)) {
         // clone reused vnode
         vnode = ownerArray[index] = cloneVNode(vnode)
     }

     const elm = vnode.elm = oldVnode.elm

     if (isTrue(oldVnode.isAsyncPlaceholder)) {
         if (isDef(vnode.asyncFactory.resolved)) {
             hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
         } else {
             vnode.isAsyncPlaceholder = true
         }
         return
     }

     // reuse element for static trees.
     // note we only do this if the vnode is cloned -
     // if the new node is not cloned it means the render functions have been
     // reset by the hot-reload-api and we need to do a proper re-render.
     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
     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)) {
         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)
     }
     // 2. 新节点不是文本节点
     if (isUndef(vnode.text)) {
         // 2-1. 都存在子节点,新旧节点的子节点数组引用不同(oldCh !== ch)调用updateChildren
         if (isDef(oldCh) && isDef(ch)) {
             if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
         } else if (isDef(ch)) { // 2-1. 新节点有子节点,旧节点没有
             // 2-2-1. 查子节点key
             if (process.env.NODE_ENV !== 'production') {
                 checkDuplicateKeys(ch)
             }
             // 2-2-2. 旧节点为文本节点时先清空文本
             if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')

             // 2-2-3.创建子节点DOM元素
             addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
         } else if (isDef(oldCh)) {
             // 2-3. 旧节点有子节点,新节点无时移除子节点和dom
             removeVnodes(oldCh, 0, oldCh.length - 1)
         } else if (isDef(oldVnode.text)) {
             // 2-4. 新节点是文本时直接替换文本内容
             nodeOps.setTextContent(elm, '')
         }
     } else if (oldVnode.text !== vnode.text) {
         // 3. 都是文本节点则替换文字
         nodeOps.setTextContent(elm, vnode.text)
     }
     if (isDef(data)) {
         if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
     }
 }
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

# updateChildren方法(diff)

diff算法的比较是同层比较(广度优先),当节点不同时会直接删掉。节省掉很多比较的时间 处理相同新旧节点的子节点。相当于新旧节点的头尾都有一个指针(共四个),对比过程以新起始节点为主导,右两侧往中间进行比较。

  • 旧子节点存在undefined节点

    • 起始undefined,oldStartVnode = oldCh[++oldStartIdx]
    • 结尾undefined,oldEndVnode = oldCh[--oldStartIdx]
  • 新旧子节点的起始节点相同: patchVnode更新内容,并且两个起始指针往后延1

  • 新旧子节点的结尾节点相同: patchVnode更新内容,并且两个结尾指针往前延1

  • 新结尾与旧起始相同: patchVnode更新内容,将旧起始节点DOM加到旧结尾DOM前面,旧起始往后,新结尾往前

  • 新起始与旧结尾相同: patchVnode更新内容,将旧结尾节点DOM加到旧结开始DOM前面,旧结尾往前,新其实往后

  • 其他

    • 和新其实节点都不同:创建新节点DOM并且添加到旧起始DOM前面
    • 存在于新其实key相同,标签相同:patchVnode,旧节点添加到旧起始DOM前,再置为undefined
    • key相同标签类型不同:创建新节点并添加到旧起始节点DOM前
  • 循环结束如果oldStartIdx>oldEndIdx:将剩余未处理新节点DOM添加到上个新结尾节点前

  • oldStartIdx<oldEndIdx: 移除旧节点和DOM

  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, vnodeToMove, refElm

      // removeOnly is a special flag used only by <transition-group>
      // to ensure removed elements stay in correct relative positions
      // during leaving transitions
      const canMove = !removeOnly

      if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(newCh)
      }

      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)
              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)
              idxInOld = isDef(newStartVnode.key) ?
                  oldKeyToIdx[newStartVnode.key] :
                  findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
              if (isUndef(idxInOld)) { // New element
                  createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
              } else {
                  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 {
                      // same key but different element. treat as new element
                      createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
                  }
              }
              newStartVnode = newCh[++newStartIdx]
          }
      }
      if (oldStartIdx > oldEndIdx) {
          refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
          addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
      } else if (newStartIdx > newEndIdx) {
          removeVnodes(oldCh, oldStartIdx, oldEndIdx)
      }
  }
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

# 实例

# 例子

  1. 左边表示新旧节点,节点下面标识起始和结尾节点(即正在处理的节点)。右边表示当前的DOM。

  1. 新节点的起始和结尾节点与旧节点的起始和结尾节点互不相同,并且在旧节点中未找到与新起始节点(新节点f)相同的节点。

所以创建节点f的DOM并添加到旧起始节点(旧节点a)DOM的前面,然后新起始节点序号加1,表示新节点f已处理,当前正在处理新起始节点c。

  1. 新节点的起始和结尾节点与旧节点的起始和结尾节点互不相同,但在旧节点中找到与新起始节点(节点c)相同的节点。

所以将旧节点c的DOM添加到旧起始节点(旧节点a)DOM的前面,旧节点c置空,然后新起始节点序号加1,表示新节点c已处理,当前正在处理新起始节点e。

  1. 新起始节点(新节点e)和旧结尾节点(旧节点e)相同。更新旧节点e的DOM内容,并将旧节点e的DOM移动到旧起始节点(旧节点a)DOM的前面,旧结尾节点序号减1,新起始节点加1,表示新旧节点e已处理,当前正在处理的是新起始节点g和旧结尾节点d。

  1. 新结尾节点(新节点d)和旧结尾节点(旧节点d)相同。仅更新旧节点d的DOM内容。新结尾节点序号减1,旧结尾节点序号减1,表示新旧节点d已处理,当前正在处理的是新结尾节点g和旧结尾节点c。由于旧节点c为空,则旧结尾节点为b。

  1. 新节点的起始和结尾节点与旧节点的起始和结尾节点互不相同,并且在旧节点中未找到与新起始节点(新节点g)相同的节点。

所以创建节点g的DOM并添加到旧起始节点(旧节点a)DOM的前面,然后新起始节点序号加1,表示新节点g已处理,当前正在处理新起始节点d。

  1. 由于新起始和结尾节点序号重叠,新节点已经处理完毕,存在尚未处理的旧节点,则移除未处理的旧节点DOM。

  1. 结束,最终的DOM。

# 无key的diff算法

# 有设置key的diff算法