我们知道,当 Vue
模板编译完成后会生成一个 render
函数,该函数内部会通过 createElement
创建虚拟 dom
并返回。
本节将展示 vue
是如何创建虚拟 dom
的,以及如何进行虚拟 dom
的 patch
在开始之前需要先明确下 VNode
的定义:
export default class VNode {
tag: string | void // 节点标签名
data: VNodeData | void // 节点数据
children: ?Array<VNode> // 节点的子节点
text: string | void // 文本节点的值
elm: Node | void // 真实的dom节点
ns: string | void // 命名空间
context: Component | void // 编译的作用域
key: string | number | void // 节点的key值
componentOptions: VNodeComponentOptions | void
componentInstance: Component | void // 组件的实例
parent: VNode | void // 父节点
raw: boolean // contains raw HTML? (server only)
isStatic: boolean // 是否是静态节点
isRootInsert: boolean // 是否作为根节点插入
isComment: boolean // 注释节点
isCloned: boolean // 克隆节点
isOnce: boolean // v-once指令节点
asyncFactory: Function | void // async component factory function
asyncMeta: Object | void
isAsyncPlaceholder: boolean
ssrContext: Object | void
fnContext: Component | void // real context vm for functional nodes
fnOptions: ?ComponentOptions // for SSR caching
devtoolsMeta: ?Object // used to store functional render context for devtools
fnScopeId: ?string // functional scope id support
}
VNode
对象就是虚拟 DOM
的节点对象,其内部属性很好的描述了一个 HTML
标签所包含的内容。
下面介绍下 Vue
是如何创建虚拟 DOM
的
创建虚拟 DOM
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
function createElement(
context,
tag,
data,
children,
normalizationType,
alwaysNormalize
) {
// 兼容不传data的情况
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
// 如果alwaysNormalize是true
// 那么normalizationType应该设置为常量ALWAYS_NORMALIZE的值
if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE
// 调用_createElement创建虚拟节点
return _createElement(context, tag, data, children, normalizationType)
}
function _createElement(context, tag, data, children, normalizationType) {
/**
* 如果存在data.__ob__,说明data是被Observer观察的数据
* 不能用作虚拟节点的data
* 需要抛出警告,并返回一个空节点
*
* 被监控的data不能被用作vnode渲染的数据的原因是:
* data在vnode渲染过程中可能会被改变,这样会触发监控,导致不符合预期的操作
*/
if (data && data.__ob__) {
process.env.NODE_ENV !== 'production' &&
warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(
data
)}\\n` + 'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// 当组件的is属性被设置为一个falsy的值
// Vue将不会知道要把这个组件渲染成什么
// 所以渲染一个空节点
if (!tag) {
return createEmptyVNode()
}
// 作用域插槽
if (Array.isArray(children) && typeof children[0] === 'function') {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 根据normalizationType的值,选择不同的处理方法
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
// 如果标签名是字符串类型
if (typeof tag === 'string') {
let Ctor
// 获取标签名的命名空间
ns = config.getTagNamespace(tag)
// 判断是否为保留标签
if (config.isReservedTag(tag)) {
// 如果是保留标签,就创建一个这样的vnode
vnode = new VNode(
config.parsePlatformTagName(tag),
data,
children,
undefined,
undefined,
context
)
// 如果不是保留标签,那么我们将尝试从vm的components上查找是否有这个标签的定义
} else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
// 如果找到了这个标签的定义,就以此创建虚拟组件节点
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 兜底方案,正常创建一个vnode
vnode = new VNode(tag, data, children, undefined, undefined, context)
}
// 当tag不是字符串的时候,我们认为tag是组件的构造类
// 所以直接创建
} else {
vnode = createComponent(tag, data, context, children)
}
// 如果有vnode
if (vnode) {
// 如果有namespace,就应用下namespace,然后返回vnode
if (ns) applyNS(vnode, ns)
return vnode
// 否则,返回一个 空节点
} else {
return createEmptyVNode()
}
}
当首次渲染时,会创建 Watcher
实例
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
},
true /* isRenderWatcher */
)
updateComponent
首先会执行 render
方法得到 VNode
,之后执行_update
方法将虚拟 Dom
渲染成真实DOM
虚拟 DOM 生成真实 DOM
渲染成真实DOM
的 _update
方法定义在src/core/instance/lifecycle.js
文件中
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
}
_update
的核心就是调用 vm.__patch__
方法。其定义在src/platforms/web/runtime/patch.js
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
该方法的定义是调用 createPatchFunction
方法的返回值,这里传入了一个对象,包含 nodeOps
参数和 modules
参数。其中,nodeOps
封装了一系列 DOM 操作的方法,modules
定义了一些模块的钩子函数的实现。而createPatchFunction
定义在 src/core/vdom/patch.js
中:
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction(backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
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]])
}
}
}
// ...
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, 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(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
createPatchFunction
最终会返回了一个 patch
方法,这个方法就赋值给了 vm._update
函数里调用的 vm.patch
patch
方法接收四个参数
oldVnode
: 旧的虚拟节点或旧的真实 dom 节点vnode
: 新的虚拟节点hydrating
: 是否是服务端渲染removeOnly
: 用于<transition-group>
组件
根据源码可以知道 patch
的过程
- 如果
vnode
不存在且oldVnode
存在,则说明要是销毁oldVnode
- 如果
vnode
存在但oldVnode
不存在,则说明需要创建新的节点直接调用createElm
- 如果
vnode
和oldVnode
同时存在oldVnode
和vnode
指向同一个对象且oldVnode
不是真实的 dom 节点,则调用patchVnode
oldVnode
和vnode
不是同一个对象,如果oldVnode
是真实 dom 节点且hydrating
为 true,则调用hydrate
函数将虚拟 dom 和真是 dom 进行映射
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(...)
}
明确流程后,就可以看看真正的patch
算法,理解oldVnode
和vnode
如何去对比的。该算法流程定义在patchVnode
函数中
patch
算法:
oldVnode
和vnode
相同,则直接返回oldVnode
和vnode
都是静态节点,且key
相同,且当vnode
是克隆节点或是v-once
指令控制的节点时,只需要把oldVnode.elm
和oldVnode.child
都复制到 vnode 上- 否则,如果
vnode
不是文本节点或注释节点- 如果
oldVnode
和vnode
都有子节点,且他们的子节点指向的对象不一致,就执行更新子节点的操作,即执行updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
- 分别获取
oldVnode
和vnode
的firstChild
、lastChild
,赋值给oldStartVnode
、oldEndVnode
、newStartVnode
、newEndVnode
- 如果
oldStartVnode
和newStartVnode
是同一节点,调用patchVnode
进行patch
,并将oldStartVnode
和newStartVnode
都设置为下一个子节点,重复上述流程 - 如果
oldEndVnode
和newEndVnode
是同一节点,调用patchVnode
进行patch
,并将oldEndVnode
和newEndVnode
都设置为上一个子节点,重复上述流程 - 如果
oldStartVnode
和newEndVnode
是同一节点,调用patchVnode
进行patch
,如果removeOnly
是false
,那么可以把oldStartVnode.elm
移动到oldEndVnode.elm
之后,然后把oldStartVnode
设置为下一个节点,newEndVnode
设置为上一个节点,重复上述流程 - 如果
newStartVnode
和oldEndVnode
是同一节点,调用patchVnode
进行patch
,如果removeOnly
是false
,那么可以把oldEndVnode.elm
移动到oldStartVnode.elm
之前,然后把newStartVnode
设置为下一个节点,oldEndVnode
设置为上一个节点,重复上述流程 - 如果以上都不匹配,就尝试在
oldChildren
中寻找跟newStartVnode
具有相同key
的节点,如果找不到相同key
的节点,说明newStartVnode
是一个新节点,就创建一个,然后把newStartVnode
设置为下一个节点 - 如果上一步找到了跟
newStartVnode
相同key
的节点,那么通过其他属性的比较来判断这 2 个节点是否是同一个节点,如果是,就调用patchVnode
进行patch
,如果removeOnly
是false
,就把newStartVnode.elm
插入到oldStartVnode.elm
之前,把newStartVnode
设置为下一个节点,重复上述流程 - 如果在
oldChildren
中没有寻找到newStartVnode
的同一节点,那就创建一个新节点,把newStartVnode
设置为下一个节点,重复上述流程 - 如果
oldStartVnode
跟oldEndVnode
重合了,并且newStartVnode
跟newEndVnode
也重合了,这个循环就结束了
- 分别获取
- 如果只有
oldVnode
有子节点,那就把这些节点都删除 - 如果只有
vnode
有子节点,那就创建这些子节点 - 如果
oldVnode
和vnode
都没有子节点,但是oldVnode
是文本节点或注释节点,就把vnode.elm
的文本设置为空字符串
- 如果
- 如果
vnode
文本节点或注释节点,但是vnode.text != oldVnode.text