# Vue源码解读(二)
# 一、Vue虚拟DOM
虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应用的各种状态变化会先作用于虚拟DOM,最终映射到DOM上。
虚拟DOM优点
虚拟DOM轻量、快速,当它们发生变化时通过新旧虚拟DOM比对可以得到最小DOM操作量,从而提升性能和用户体验。本质上是使用JavaScript运算成本替换DOM操作的执行成本,前者运算速度要比后者快得多,这样做很划算,因此才会有虚拟DOM。
vue 1.0中有细粒度的数据变化侦测,它是不需要虚拟DOM的,但是细粒度造成了大量开销,这对于大型项目来说是不可接受的。因此,vue 2.0选择了中等粒度的解决方案,每一个组件对应一个watcher实例, 这样状态变化时只能通知到组件,再通过引入虚拟DOM去进行比对和渲染。
但是有一点需要注意,虽然每一个组件对应一个watcher实例,但是组件的数量和watcher实例的数量不一定是相等的,因为组件内可能还会有用户手动创建的watcher,比如$watch或者watch方法创建的watcher。
# VNode
VNode,就是虚拟DOM,定义在src\core\vdom\vnode.js
// VNode对象,共有6种类型:元素、组件、文本、函数式组件、注释和克隆节点
export default class VNode {
tag: string | void; // 节点标签,文本和注释没有
data: VNodeData | void; // 节点数据
children: ?Array<VNode>; // 元素的子元素
text: string | void; // 文本、注释的内容
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
functionalContext: Component | void; // only for functional component root nodes
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions
) {
/*当前节点的标签名*/
this.tag = tag
/*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
this.data = data
/*当前节点的子节点,是一个数组*/
this.children = children
/*当前节点的文本*/
this.text = text
/*当前虚拟节点对应的真实dom节点*/
this.elm = elm
/*当前节点的名字空间*/
this.ns = undefined
/*编译作用域*/
this.context = context
/*函数化组件作用域*/
this.functionalContext = undefined
/*节点的key属性,被当作节点的标志,用以优化*/
this.key = data && data.key
/*组件的option选项*/
this.componentOptions = componentOptions
/*当前节点对应的组件的实例*/
this.componentInstance = undefined
/*当前节点的父节点*/
this.parent = undefined
/*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
this.raw = false
/*静态节点标志*/
this.isStatic = false
/*是否作为根节点插入*/
this.isRootInsert = true
/*是否为注释节点*/
this.isComment = false
/*是否为克隆节点*/
this.isCloned = false
/*是否有v-once指令*/
this.isOnce = false
}
get child (): Component | void {
return this.componentInstance
}
}
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
这是一个最基础的VNode节点,作为其他派生VNode类的基类,里面定义了下面这些数据。
TIP
tag: 当前节点的标签名
data: 当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息
children: 当前节点的子节点,是一个数组
text: 当前节点的文本
elm: 当前虚拟节点对应的真实dom节点
ns: 当前节点的名字空间
context: 当前节点的编译作用域
functionalContext: 函数化组件作用域
key: 节点的key属性,被当作节点的标志,用以优化
componentOptions: 组件的option选项
componentInstance: 当前节点对应的组件的实例
parent: 当前节点的父节点
raw: 简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false
isStatic: 是否为静态节点
isRootInsert: 是否作为跟节点插入
isComment: 是否为注释节点
isCloned: 是否为克隆节点
isOnce: 是否有v-once指令
打个比方,比如说我现在有这么一个VNode树
{
tag: 'div'
data: {
class: 'test'
},
children: [
{
tag: 'span',
data: {
class: 'demo'
}
text: 'hello,VNode'
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
渲染之后的结果就是这样的
<div class="test">
<span class="demo">hello,VNode</span>
</div>
2
3
# mountComponent
vdom树首页生成、渲染发生在mountComponent中,src\core\instance\lifecycle.js
,核心代码如下
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el // 挂载的宿主元素
callHook(vm, 'beforeMount') // 调用beforeMount钩子
// 定义更新函数updateComponent,实际调用vm._update
let updateComponent = () => {
// vm._update是更新函数,内部会做diff算法,vm._render()是渲染函数,渲染函数返回虚拟DOM
vm._update(vm._render(), hydrating)
}
// 实例化watcher,并传入更新函数
new Watcher(vm, updateComponent, noop, {
before () {
// 如果没有实例已经挂载并且没有销毁,则调用beforeUpdate钩子
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted') // 调用mounted钩子
}
return vm
}
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
从源码可知,实例化watcher时传入了更新函数updateComponent
,updateComponent
实际调用了vm._update
,vm._update
是更新函数,内部会做diff算法,vm._render()
是渲染函数,渲染函数返回虚拟DOM。所以接下来我们要去研究vm._render
和vm._update
。
# vm._render
vm._render
生成虚拟dom,src\core\instance\render.js
,核心代码如下
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options // 从$options里拿出render函数
vm.$vnode = _parentVnode
// 调用render方法生成虚拟DOM
let vnode = render.call(vm._renderProxy, vm.$createElement)
return vnode
}
2
3
4
5
6
7
8
9
# vm._update
vm._update
是更新函数,内部会做虚拟DOM的diff算法,src\core\instance\lifecycle.js
,核心代码如下
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el // 拿到DOM元素
const prevVnode = vm._vnode // 老的Vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// 打补丁利用vm.__patch__
if (!prevVnode) {
// 如果没有老的Vnode,则将DOM元素和虚拟DOM传入,进行初始化渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 否则比较新旧虚拟DOM,得出最小差异,然后操作DOM
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// 更新vue实例的引用__vue__
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
}
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) { // 强制更新
vm._watcher.update()
}
}
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
从源码可知,vm._update
负责更新dom,核心是调用vm.__patch__
,所以接下来我们要去研究vm.__patch__
。
# vm.__patch__和patch
vm.__patch__
是在平台特有代码中指定的, src\platforms\web\runtime\index.js
.
import { patch } from './patch' // 引入了patch.js里的patch方法
Vue.prototype.__patch__ = inBrowser ? patch : noop
2
3
patch方法的定义在 src\platforms\web\runtime\patch.js
,核心代码如下
import * as nodeOps from 'web/runtime/node-ops' // 定义各种原生dom基础操作方法
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
2
3
4
5
6
7
8
可见 patch 实际就是createPatchFunction
的返回值,需要传递nodeOps
和modules
,这里主要是为了跨平台。
# nodeOps和modules
nodeOps定义了各种原生dom基础操作方法,nodeOps定义在src\platforms\web\runtime\node-ops.js
import { namespaceMap } from 'web/util/index'
export function createElement (tagName: string, vnode: VNode): Element {
const elm = document.createElement(tagName)
if (tagName !== 'select') {
return elm
}
if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
elm.setAttribute('multiple', 'multiple')
}
return elm
}
export function createElementNS (namespace: string, tagName: string): Element {
return document.createElementNS(namespaceMap[namespace], tagName)
}
export function createTextNode (text: string): Text {
return document.createTextNode(text)
}
export function createComment (text: string): Comment {
return document.createComment(text)
}
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
export function removeChild (node: Node, child: Node) {
node.removeChild(child)
}
export function appendChild (node: Node, child: Node) {
node.appendChild(child)
}
export function parentNode (node: Node): ?Node {
return node.parentNode
}
export function nextSibling (node: Node): ?Node {
return node.nextSibling
}
export function tagName (node: Element): string {
return node.tagName
}
export function setTextContent (node: Node, text: string) {
node.textContent = text
}
export function setStyleScope (node: Element, scopeId: string) {
node.setAttribute(scopeId, '')
}
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
modules 定义了虚拟dom更新 => dom操作转换的方法,比如attr、class、events、style、transition等的转换方法。定义在src\platforms\web\runtime\modules\index.js
,
拿attr更新来说,具体更新方法定义在src\platforms\web\runtime\modules\attrs.js
,核心代码如下
function updateAttrs (oldVnode: VNodeWithData, vnode: VNodeWithData) {
let key, cur, old
const elm = vnode.elm
const oldAttrs = oldVnode.data.attrs || {} // 老的属性
let attrs: any = vnode.data.attrs || {} // 新的属性
// 遍历新的属性,如果老的属性值和新的属性值不等,则更新属性值
for (key in attrs) {
cur = attrs[key]
old = oldAttrs[key]
if (old !== cur) {
setAttr(elm, key, cur) // 更新属性值
}
}
// 遍历旧的属性
for (key in oldAttrs) {
// 如果新的属性的属性值没有定义,则把这个属性删除
if (isUndef(attrs[key])) {
elm.removeAttribute(key)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# createPatchFunction
createPatchFunction
,创建并返回patch方法,需要传入nodeOps
和modules
,该方法定义在src\core\vdom\patch.js
里面,patch方法才是真正的打补丁的方法。
# patch(新老VNode节点比对)
patch将新老VNode节点进行比对,然后将根据两者的比较结果进行最小单位地修改视图,而不是将整个视图根据新的VNode重绘。patch的核心在于diff算法,这套算法可以高效地比较virtual DOM的变更,得出变化以修改视图。
diff算法:通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,时间复杂度只有O(n),是一种相当高效的算法。同层级只做三件事:增删改。
具体规则是:new VNode不存在就删;old VNode不存在就增;都存在就比较类型,类型不同直接替换、类型相同执行更新;
这两张图代表旧的VNode与新VNode进行patch的过程,他们只是在同层级的VNode之间进行比较得到变化(图中相同颜色的方块代表互相进行比较的VNode节点),然后修改变化的视图,所以十分高效。
// patch 同层级只做三件事:节点的增删改。
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// 如果老的Vnode没有定义,则根据新的VNode创建元素
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// 标记旧的VNode是否有nodeType,如果有,它就是一个真实的dom节点
const isRealElement = isDef(oldVnode.nodeType)
// 是同一个节点的时候就做深层次的更新patchVnode
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 如果是真实的节点,即传了dom元素进来
if (isRealElement) {
// 当旧的VNode是服务端渲染的元素,hydrating记为true
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
// 如果不是服务端渲染或者合并到真实DOM失败,则创建一个空的VNode节点替换
oldVnode = emptyNodeAt(oldVnode)
}
// 取代现有元素
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// 创建新节点
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// 移除老节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
// 调用Destroy钩子
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
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
从代码中不难发现,当oldVnode与vnode是sameVnode
的时候才会进行patchVnode
,也就是新旧VNode节点判定为同一节点的时候才会进行patchVnode这个过程,否则就是创建新的DOM,移除旧的DOM。
怎么样的节点算sameVnode呢?
# sameVnode
该方法定义在src\core\vdom\patch.js
里面。
/*
判断两个VNode节点是否是同一个节点,需要同时满足以下条件
key相同
tag(当前节点的标签名)相同
isComment(是否为注释节点)相同
是否data(当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息)都有定义
当标签是<input>的时候,type必须相同
*/
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)
)
}
// 判断当标签是<input>的时候,type是否相同
// 某些浏览器不支持动态修改<input>类型,所以他们被视为不同节点
function sameInputType (a, b) {
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
return typeA === typeB
}
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:当两个VNode的tag、key、isComment都相同,并且同时定义或未定义data的时候,且如果标签为input则type必须相同。这时候这两个VNode则算sameVnode,可以直接进行patchVnode操作。
# patchVnode
该方法定义在src\core\vdom\patch.js
里面。两个VNode相同执行更新操作,包括三种操作:属性更新、文本更新、子节点更新,具体规则如下:
patchVode规则
- 如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),并且新的VNode是clone或者是标记了v-once,那么只需要替换elm以及componentInstance即可。
- 新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren,这个updateChildren也是diff的核心。
- 如果老节点没有子节点而新节点存在子节点,先清空老节点DOM的文本内容,然后为当前DOM节点加入子节点。
- 当新节点没有子节点而老节点有子节点的时候,则移除该DOM节点的所有子节点。
- 当新老节点都无子节点的时候,只是文本的替换。
/*patchVode*/
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
/*两个VNode节点相同则直接返回*/
if (oldVnode === vnode) {
return
}
/*
如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),
并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),
那么只需要替换elm以及componentInstance即可。
*/
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
vnode.elm = oldVnode.elm
vnode.componentInstance = oldVnode.componentInstance
return
}
/* 如果存在data.hook.prepatch则要先执行 */
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
/* 执行属性、事件、样式等等更新操作 */
if (isDef(data) && isPatchable(vnode)) {
/* 调用update回调以及update钩子 */
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)
}
/* 开始判断children的各种情况 */
/* 如果这个VNode节点没有text文本时*/
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
/*新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren*/
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
/*如果老节点没有子节点而新节点存在子节点,先清空elm的文本内容,然后为当前节点加入子节点*/
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
/*当新节点没有子节点而老节点有子节点的时候,则移除所有ele的子节点*/
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
/*当新老节点都无子节点的时候,只是文本的替换,因为这个逻辑中新节点text不存在,所以直接去除ele的文本*/
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
/*当新老节点text不一样时,直接替换这段文本*/
nodeOps.setTextContent(elm, vnode.text)
}
/*调用postpatch钩子*/
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
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
新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren
。
# updateChildren
该方法定义在src\core\vdom\patch.js
里面。updateChildren主要作用是比对新旧两个VNode的children得出具体DOM操作。执行一个双循环是传统方式,vue中针对web场景特点做了特别的算法优化:
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, elmToMove, refElm
const canMove = !removeOnly
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)) {
/*前四种情况其实是指定key的时候,判定为同一个VNode,则直接patchVnode即可,分别比较oldCh以及newCh的两头节点2*2=4种情况*/
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
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)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 如果不是前四种情况,即开始和结束都没有相同的,则循环去找到sameVode
/*
生成一个key与旧VNode的key对应的哈希表(只有第一次进来undefined的时候会生成,也为后面检测重复的key值做铺垫)
比如childre是这样的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}] beginIdx = 0 endIdx = 2
结果生成{key0: 0, key1: 1, key2: 2}
*/
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
/*如果newStartVnode新的VNode节点存在key并且这个key在oldVnode中能找到则返回这个节点的idxInOld(即第几个节点,下标)*/
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
if (isUndef(idxInOld)) { // New element
/*newStartVnode没有key或者是该key没有在老节点中找到则创建一个新的节点*/
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
/*获取同key的老节点*/
elmToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !elmToMove) {
/*如果elmToMove不存在说明之前已经有新节点放入过这个key的DOM中,提示可能存在重复的key,确保v-for的时候item有唯一的key值*/
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}
if (sameVnode(elmToMove, newStartVnode)) {
/*Github:https://github.com/answershuto*/
/*如果新VNode与得到的有相同key的节点是同一个VNode则进行patchVnode*/
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
/*因为已经patchVnode进去了,所以将这个老节点赋值undefined,之后如果还有新节点与该节点key相同可以检测出来提示已有重复的key*/
oldCh[idxInOld] = undefined
/*当有标识位canMove实可以直接插入oldStartVnode对应的真实DOM节点前面*/
canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
// same key but different element. treat as new element
/*当新的VNode与找到的同样key的VNode不是sameVNode的时候(比如说tag不一样或者是有不一样type的input标签),创建一个新的节点*/
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
}
}
}
/*以下是循环结束后,处理多余或者不够的真实DOM节点*/
if (oldStartIdx > oldEndIdx) {
/*全部比较完成以后,发现oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多,所以这时候多出来的新节点需要一个一个创建出来加入到真实DOM中*/
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
/*如果全部比较完成以后发现newStartIdx > newEndIdx,则说明新节点已经遍历完了,老节点多余新节点,这个时候需要将多余的老节点从真实DOM中移除*/
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
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
直接看源码可能比较难以捋清其中的关系,我们通过图来看一下。
首先,在新老两个VNode节点的左右头尾两侧都有一个变量标记,在遍历过程中这几个变量都会向中间靠拢。当oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx时结束循环。
索引与VNode节点的对应关系:
oldStartIdx => oldStartVnode
oldEndIdx => oldEndVnode
newStartIdx => newStartVnode
newEndIdx => newEndVnode
在遍历中,如果存在key,并且满足sameVnode,会将该DOM节点进行复用,否则则会创建一个新的DOM节点。
# 遍历规则
首先,oldStartVnode、oldEndVnode与newStartVnode、newEndVnode两两交叉比较,一共有2*2=4种比较方法。
1、当新老VNode节点的start或者end满足sameVnode时,也就是sameVnode(oldStartVnode, newStartVnode)或者sameVnode(oldEndVnode, newEndVnode),直接将该VNode节点进行patchVnode即可。
2、如果oldStartVnode与newEndVnode满足sameVnode,即sameVnode(oldStartVnode, newEndVnode)。这时候说明oldStartVnode已经跑到了oldEndVnode后面去了,进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后面。
3、如果oldEndVnode与newStartVnode满足sameVnode,即sameVnode(oldEndVnode, newStartVnode)。这说明oldEndVnode跑到了oldStartVnode的前面,进行patchVnode的同时真实的DOM节点移动到了oldStartVnode的前面。
4、如果以上情况均不符合,则通过createKeyToOldIdx会得到一个oldKeyToIdx,里面存放了一个key为旧的VNode,value为对应index序列的哈希表。从这个哈希表中可以找到是否有与newStartVnode一致key的旧的VNode节点,如果同时满足sameVnode,patchVnode的同时会将这个真实DOM(elmToMove)移动到oldStartVnode对应的真实DOM的前面。
5、当然也有可能newStartVnode在旧的VNode节点找不到一致的key,或者是即便key相同却不是sameVnode,这个时候会调用createElm创建一个新的DOM节点。
到这里循环已经结束了,那么剩下我们还需要处理多余或者不够的真实DOM节点。
(1)当结束时oldStartIdx > oldEndIdx,这个时候老的VNode节点已经遍历完了,但是新的节点还没有。说明了新的VNode节点实际上比老的VNode节点多,也就是比真实DOM多,需要将剩下的(也就是新增的)VNode节点插入到真实DOM节点中去,此时调用addVnodes(批量调用createElm的接口将这些节点加入到真实DOM中去)。
(2)同理,当newStartIdx > newEndIdx时,新的VNode节点已经遍历完了,但是老的节点还有剩余,说明真实DOM节点多余了,需要从文档中删除,这时候调用removeVnodes将这些多余的真实DOM删除。
# DOM操作
由于Vue使用了虚拟DOM,所以虚拟DOM可以在任何支持JavaScript语言的平台上操作,譬如说目前Vue支持的浏览器平台或是weex,在虚拟DOM的实现上是一致的。那么最后虚拟DOM如何映射到真实的DOM节点上呢?
Vue为平台做了一层适配层,浏览器平台见/platforms/web/runtime/node-ops.js
以及weex平台见/platforms/weex/runtime/node-ops.js
。不同平台之间通过适配层对外提供相同的接口,虚拟DOM进行操作真实DOM节点的时候,只需要调用这些适配层的接口即可,而内部实现则不需要关心,它会根据平台的改变而改变。
现在又出现了一个问题,我们只是将虚拟DOM映射成了真实的DOM。那如何给这些DOM加入attr、class、style等DOM属性呢?这要依赖于虚拟DOM的生命钩子。虚拟DOM提供了如下的钩子函数,分别在不同的时期会进行调用。
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
const { modules, nodeOps } = backend
/*构建cbs回调函数,web平台上见/platforms/web/runtime/modules*/
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
function patchVnode (...) {
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
同理,也会根据不同平台有自己不同的实现,我们这里以Web平台为例。Web平台的钩子函数见/platforms/web/runtime/modules
。里面有对attr、class、props、events、style以及transition(过渡状态)的DOM属性进行操作。
# patch算法总结
# 1、啥时候进行patchVnode?
当oldVnode与vnode是sameVnode(同一节点)的时候才会进行patchVnode,也就是新旧VNode节点判定为同一节点的时候才会进行patchVnode这个过程,否则就是创建新的DOM,移除旧的DOM。
SameVnode:当两个VNode的tag、key、isComment都相同,并且同时定义或未定义data的时候,且如果标签为input则type必须相同。这时候这两个VNode则算sameVnode,可以直接进行patchVnode操作。
# 2、patchVnode的规则
1.如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),那么只需要替换elm以及componentInstance即可。
2.新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren,这个updateChildren也是diff的核心。
3.如果老节点没有子节点而新节点存在子节点,先清空老节点DOM的文本内容,然后为当前DOM节点加入子节点。
4.当新节点没有子节点而老节点有子节点的时候,则移除该DOM节点的所有子节点。
5.当新老节点都无子节点的时候,只是文本的替换。
# 3、patchVnode什么情况下进行updateChildren?
当新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren。
# 二、Vue编译器Compile
模板编译的主要目标是将模板(template)转换为渲染函数(render)
带编译器的版本中,可以使用template或el的方式声明模板.
<div id="demo">
<h1>Vue.js测试</h1>
<p>{{foo}}</p>
</div>
<script>
// 使用el方式
new Vue({
data: { foo: 'foo' },
el: "#demo",
});
</script>
2
3
4
5
6
7
8
9
10
11
然后输出渲染函数
const app = new Vue({});
// 输出render函数
console.log(app.$options.render);
2
3
输出结果大致如下
ƒunction anonymous() {
with (this) {
return _c('div', { attrs: { "id": "demo" } }, [
_c('h1', [_v("Vue.js测试")]),
_v(" "),
_c('p', [_v(_s(foo))])
])
}
}
2
3
4
5
6
7
8
9
别名
元素节点使用createElement创建,别名_c
本文节点使用createTextVNode创建,别名_v
表达式先使用toString格式化,别名_s
实现模板编译共有三个阶段:解析、优化和代码生成。
# 解析 - parse
解析器将模板解析为抽象语法树AST,只有将模板解析成AST后,才能基于它做优化或者生成代码字符串。
调试查看得到的AST,src\compiler\parser\index.js
- parse方法,结构如下:
解析器内部分了HTML解析器、文本解析器和过滤器解析器,最主要是HTML解析器,核心算法说明:
parseHTML, src\compiler\parser\index.js
parseHTML(tempalte, {
start(tag, attrs, unary){}, // 遇到开始标签的处理
end(){},// 遇到结束标签的处理
chars(text){},// 遇到文本标签的处理
comment(text){}// 遇到注释标签的处理
})
2
3
4
5
6
# 优化 - optimize
优化器的作用是在AST中找出静态子树并打上标记。静态子树是在AST中永远不变的节点,如纯文本节点。
标记静态子树的好处:
- 每次重新渲染,不需要为静态子树创建新节点
- 虚拟DOM中patch时,可以跳过静态子树
标记过程有两步:
- 找出静态节点并标记
- 找出静态根节点并标记
代码实现,src\compiler\optimizer.js
- optimize
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// 找出静态节点并标记
markStatic(root)
// 找出静态根节点并标记
markStaticRoots(root, false)
}
2
3
4
5
6
7
8
9
标记结束
# 代码生成 - generate
将AST转换成渲染函数中的内容,即代码字符串。
generate方法生成渲染函数代码,src\compiler\codegen\index.js
- generate
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
2
3
4
5
6
7
8
9
10
11
生成的code
"_c('div',{attrs:{"id":"demo"}},[_c('h1',[_v("Vue.js测试")]),_v(" "),_c('p',
[_v(_s(foo))])])"
2
# 关于v-if、v-for
// 解析v-if, 在src\compiler\parser\index.js
function processIf (el) {
const exp = getAndRemoveAttr(el, 'v-if') // 获取v-if=“exp"中exp并删除v-if属性
if (exp) {
el.if = exp // 为ast添加if表示条件
addIfCondition(el, { // 为ast添加ifConditions表示各种情况对应结果
exp: exp,
block: el
})
} else { // 其他情况处理
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
}
const elseif = getAndRemoveAttr(el, 'v-else-if')
if (elseif) {
el.elseif = elseif
}
}
}
// 代码生成,在src\compiler\codegen\index.js
function genIfConditions (
conditions: ASTIfConditions,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
const condition = conditions.shift() // 每次处理一个条件
if (condition.exp) { // 每种条件生成一个三元表达式
return `(${condition.exp})?${
genTernaryExp(condition.block)
}:${
genIfConditions(conditions, state, altGen, altEmpty)
}`
} else {
return `${genTernaryExp(condition.block)}`
}
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
# 关于插槽
组件编译的顺序是先编译是父组件,再编译子组件。src\compiler\parser\index.js
普通插槽是在父组件编译和渲染阶段生成 vnodes ,数据的作用域是父组件,子组件渲染的时候直接拿到这些渲染好的 vnodes 。
作用域插槽,父组件在编译和渲染阶段并不会直接生成 vnodes ,而是在父节点保留一个 scopedSlots 对象,存储着不同名称的插槽以及它们对应的渲染函数,只有在编译和渲染子组件阶段才会执行这个渲染函数生成vnodes ,由于是在子组件环境执行的,所以对应的数据作用域是子组件实例。
简单地说,两种插槽的目的都是让子组件 slot 占位符生成的内容由父组件来决定,但数据的作用域会根据它们vnodes 渲染时机不同而不同。
← Vue源码解读(一) Vue源码解读(三) →