vue源码阅读(九) $mount 内部实现 --- patch

patch

将VNode转换成真实的node节点,需要通过patch函数来实现

在Vue.prototype._update的定义中有如下调用

1
vm.$el = vm.__patch__(prevVnode, vnode)

__patch__的定义如下:(platforms/runtime/index.js)

1
2
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop

patch:

1
2
import { createPatchFunction } from 'core/vdom/patch'
export const patch: Function = createPatchFunction({ nodeOps, modules })

createPatchFunction的主要代码如下

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
export function createPatchFunction (backend) {
...
/**
* 这里通过createPatchFunction函数,来创建返回一个patch函数。path接收6个参数:
oldVnode: 旧的虚拟节点或旧的真实dom节点
vnode: 新的虚拟节点
hydrating: 是否要跟真是dom混合
removeOnly: 特殊flag,用于组件
*/
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
// 如果vnode不存在但oldVnode存在,则表示要移除旧的node
// 那么就调用invokeDestroyHook(oldVnode)来进行销毁
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}

let isInitialPatch = false
const insertedVnodeQueue = []
// 如果oldVnode不存在,vnode存在,则创建新节点
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
// nodeType 节点的类型,详细:https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType
const isRealElement = isDef(oldVnode.nodeType)
// 如果oldVnode、vnode都存在
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 如果oldVnode与Vnode是同一节点是就调用patchVnode处理去比较两个节点的差异
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
if (isRealElement) {
// 如果存在真实的节点,存在data-server-rendered属性
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
// 需要用hydrate函数将虚拟DOM和真实DOM进行映射
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
}
...
}
// 如果不是server-rendered 或者hydration失败
// 创建一个空VNode,代替oldVnode
oldVnode = emptyNodeAt(oldVnode)
}
// 将oldVnode设置为对应的虚拟dom,找到oldVnode.elm的父节点
// 根据vnode创建一个真实dom节点并插入到该父节点中oldVnode.elm的位置
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)

// 递归更新父级占位节点元素,
if (isDef(vnode.parent)) {
...
}

// 销毁旧节点
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}

invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
// 返回节点
return vnode.elm
}
}

这里通过createPatchFunction函数,来创建返回一个patch函数。path接收6个参数:

  1. oldVnode: 旧的虚拟节点或旧的真实dom节点
  2. vnode: 新的虚拟节点
  3. hydrating: 是否要跟真是dom混合
  4. removeOnly: 特殊flag,用于组件
  5. parentElm:父节点
  6. refElm: 新节点将插入到refElm之前

具体解析看代码注释~抛开调用生命周期钩子和销毁就节点不谈,

我们发现代码中的关键在于sameVnodecreateElmpatchVnode 方法。

sameVnode - 判断2个节点,是否是同一个节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function sameVnode (a, b) {
return (
a.key === b.key && ( // 节点 key 必须相同
( // tag、注释、data是否存在、input类型是否相同
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || ( // 如果isAsyncPlaceholder是true,则需要asyncFactory属性相同
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}

createElm - 创建真实的 DOM 对象

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
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
vnode.isRootInsert = !nested // for transition enter check
// 用于创建组件,在调用了组件初始化钩子之后,初始化组件,并且重新激活组件。
// 在重新激活组件中使用 insert 方法操作 DOM
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}

const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {

// nodeOps 封装的操作dom的合集
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode) // 用于为 scoped CSS 设置作用域 ID 属性

// weex处理
if (__WEEX__) {
...
} else {
// 用于创建子节点,如果子节点是数组,则遍历执行 createElm 方法.
// 如果子节点的 text 属性有数据,则使用 nodeOps.appendChild(...) 在真实 DOM 中插入文本内容。
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// insert 用于将元素插入真实 DOM 中
insert(parentElm, vnode.elm, refElm)
}
...
} else if (isTrue(vnode.isComment)) { // 注释
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else { // 文本
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}

😭 https://github.com/muwoo/blogs/blob/master/src/Vue/11.md