虚拟 dom 桥接着 view-model 模型和实际的 dom 节点树。首先 view-model 通常不只包含一个节点,而是一颗节点树,和实际的 dom 节点有一对多的关联。因此借助于虚拟 dom 节点,我们就可以与实际的 dom 节点产生一对一的关联。同时,view-model 保存着全量的数据,通过它不能直接对实际的 dom 节点树进行增量更新。有了同样是树结构的虚拟 dom 作为 view-model 和实际 dom 节点树的中继者,一方面我们就能将 view-model 中的数据、模板很好地反映到实际的 dom 节点树上;另一方面 view-model 增量数据更新可表现为虚拟 dom 节点树的前后差异,从而局部重绘实际的 dom 节点树。
1 整体流程
虚拟 dom 的核心操作分为三个步骤:对虚拟 dom 进行建模,并创建虚拟 dom;比较虚拟 dom 树的前后差异;将差异渲染到页面上。在 Vue 中,第二步和第三步在 patch 过程中合为一体。广义的虚拟 dom 还包含从模板解析出虚拟 dom 树的过程,本文针对的是 Vue 中虚拟 dom 的实现,这里仅指明由模板生成的渲染函数。至于解析模板的具体逻辑,笔者将在后续的文章加以分析。下面是 Vue 通过模板解析出的渲染函数。
// 由 src/compiler/codegen/index.js 中 generate 函数输出
// _s 函数:toString 方法
// _v 函数:创建文本节点
// _c 函数:创建标签节点
{
render: `with (this) {
return _c(
'div', { attrs: { "id": "app" } },
[
_c(
'h1', { staticStyle: { "color": "red" }, attrs: { "data-id": "1" } },
[_v(_s(message))]
)
]
)
}`
}
在上述代码中,有必要指明的是,_c 即 Vue 生命周期 - 创建实例 中的 vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) 以及 vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)。参数 a 为 tag 节点的标签名,b 为 data 节点的数据,c 为 children 子节点,d 为 normalizationType;而 createElement 函数则用于创建 VNode。实际上,上述代码中的 render 渲染函数会被包裹成 vm.$options.render 高阶函数,最终在 vm._render 方法中以 vm.$options.render.call(vm._renderProxy, vm.$createElement) 的调用形式创建 VNode,即通过高阶函数传入 _c, _s, _v 等工具函数。
当通过 vm._render 获得 vnode (该 vnode 将包含从模板中解析到的父 vnode 节点信息)以后,Vue 会调用 vm._update 方法将 vnode 填入文档或进行重绘。vm._update 方法实际基于 vm.patch 方法完成组件的挂载或重绘,参考 Vue 生命周期 - 组件挂载及更新。下图是单个 Vue 组件在父模板中作为模板节点的解析过程。
![206e562600d87af70825c17d9c7335f2.png](https://img-blog.csdnimg.cn/img_convert/206e562600d87af70825c17d9c7335f2.png)
对于用户手动 new 出的 Vue 实例,完成挂载需要手动调用 $mount 方法,然后将该 Vue 实例关联的模板解析为渲染函数并完成渲染。这时 template 和 _render 方法一样都是 Vue 实例的一部分。当该组件模板 template 包含其他组件节点或原生节点等时,在渲染函数执行期间,注入的 vm._c 方法将会创建与该组件节点或原生节点相对应的 vnode。对于组件节点,在 patch 过程中通过 init 钩子创建关联的 Vue 实例。处理过程参见下文的 patch 一小节。
2 vnode 模型
![c80735e0a85f619da5a5d1a35577d8ab.png](https://img-blog.csdnimg.cn/img_convert/c80735e0a85f619da5a5d1a35577d8ab.png)
vnode 大致可以分为以下几类:
- EmptyVNode:注释节点,对应原生的注释节点,text 属性有值 且 isComment 属性为真值。
- TextVNode:文本节点,对应原生的文本节点,text 属性有值。
- ElementVNode:元素节点,对应原生的元素节点,elm 属性在渲染或重绘后有值。
- ComponentVNode:组件节点,elm 属性在渲染或重绘后有值,包含类组件、函数式组件、异步组件等。
- CloneVNode:拷贝节点,isCloned 属性为真值,以便于复用。
3 创建 vnode
![a9c849d217883768fa4ece39d84def3a.png](https://img-blog.csdnimg.cn/img_convert/a9c849d217883768fa4ece39d84def3a.png)
由上文可以看出,在创建 vnode 的过程中,将调用 createElement 函数生成 vnode 的一般属性,包含 tag, data, children, text, ns, context, key。创建 vnode 有三种方式:对于内置的 html 节点,直接创建 VNode 实例;对于 Vue 组件构成的模板节点,通过 createComponent 创建 VNode 实例;对于 tag 不可识别或为对象的其他节点,同样通过 createComponent 创建 VNode 实例。
3.1 createElement
在 Vue 中,createElement 函数的功能主要由 _createElement 实现。createElement 的特殊意义是对参数进行处理,余下的过程都由 _createElement 完成。createElement 通过 vm._c 注入到模板渲染函数中;当渲染函数执行过程中,可用于创建 vnode 节点树。createElement 本身的实现也较为简单:针对模板中的原生节点,直接使用 new VNode 创建 vnode 实例;针对模板中的组件节点或其他,调用 createComponent 创建 vnode 实例。以下是 _createElement 函数的扼要实现:
// createElement 函数处理参数,并调用 _createElement 创建 vnode
function _createElement (
context: Component,// 模板对应的 vm 实例
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// v-bind:is 或 is 属性的存在,意为动态组件
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
// 插槽支持单个函数作为子组件
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
if (normalizationType === ALWAYS_NORMALIZE) {
// normalizeChildren 函数意义为:
// 如果 children 是原始类型,字符串、数值、symbol或布尔值,使用 createTextVNode 创建文本节点
// 如果 children 是数组,递归处理,文本节点聚合,参考 normalizeArrayChildren 函数
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// simpleNormalizeChildren 如遇到数组,不作递归处理,只是简单地拼接
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
// config 根据不同环境有不同实现
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 针对环境支持的 html 节点标签
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
// resolveAsset 从 context.$options.components 获取指定的组件构造器
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)// 将 ns 写入 vnode
// registerDeepBindings 使用 observer 包提供的 traverse 函数递归地调用响应式数据 data 的 getter,绑定依赖
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
3.2 createComponent
createComponent 函数意义在于创建不同类型的组件:异步组件、函数式组件、类组件等。
function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
const baseCtor = context.$options._base// Vue 构造函数
if (isObject(Ctor)) {// 当 Ctor 为对象,构造 Vue 的子类
Ctor = baseCtor.extend(Ctor)
}
// 处理异步组件
let asyncFactory
if (isUndef(Ctor.cid)) {// Ctor 异步组件形式
asyncFactory = Ctor
// 异步组件加载过程中,Ctor 为 undefined;加载完成,Ctor 为待渲染的组件
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
// 异步组件加载过程中,创建异步占位符
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
data = data || {}
// 更新类所携带的选项
resolveConstructorOptions(Ctor)
// 根据 Ctor.options.model 更新 data 中的指定数据以及绑定事件
// model 选项的意义就在于双向绑定,可包含的属性有 prop, event, callback
// prop 指定模板和响应式数据交互的 key;event, callback 指定事件和绑定函数
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// 根据 Ctor.options.props 解析出 data.attrs, data.props(父组件传入的 props)
// options.props 指定子组件接受数据的 key 键
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// 处理函数式组件
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
const listeners = data.on// dom 事件
data.on = data.nativeOn
// 抽象组件,data 中仅保留 slot
if (isTrue(Ctor.options.abstract)) {
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// 将 componentVNodeHooks 钩子灌入到 data.hook 中
installComponentHooks(data)
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },// 作为 componentOptions
asyncFactory
)
return vnode
}
3.2.1 函数式组件
函数式组件 对应 React 中的无状态组件。不同于类组件以 vm 实例作为上下文,函数式组件在生成过程中将构建新的上下文对象,因此没有对应的 vm 实例,也就没有响应的生命周期和响应式数据。
函数式组件所对应的 vnode 实例包含特殊属性如 fnContext, fnOptions, fnScopeId。fnContext 即模板所对应的 vm 实例;fnOptions 即 vm 构造函数所划定的选项;fnScopeId 即选项中包含的作用域 id。
// 创建函数式-无状态组件
function createFunctionalComponent (
Ctor: Class<Component>,
propsData: ?Object,
data: VNodeData,
contextVm: Component,
children: ?Array<VNode>
): VNode | Array<VNode> | void {
const options = Ctor.options
const props = {}
const propOptions = options.props
if (isDef(propOptions)) {
for (const key in propOptions) {
props[key] = validateProp(key, propOptions, propsData || emptyObject)
}
} else {
if (isDef(data.attrs)) mergeProps(props, data.attrs)
if (isDef(data.props)) mergeProps(props, data.props)
}
// 构建新的上下文对象,对应类组件的 vm 实例
const renderContext = new FunctionalRenderContext(
data,
props,
children,
contextVm,
Ctor
)
// 没有使用 vm 实例作为上下文,因此没有生命周期和响应式数据
// renderContext._c 即上文中的 createElement
const vnode = options.render.call(null, renderContext._c, renderContext)
if (vnode instanceof VNode) {
return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext)
} else if (Array.isArray(vnode)) {
const vnodes = normalizeChildren(vnode) || []
const res = new Array(vnodes.length)
for (let i = 0; i < vnodes.length; i++) {
res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options, renderContext)
}
return res
}
}
function FunctionalRenderContext (
data: VNodeData,
props: Object,
children: ?Array<VNode>,
parent: Component,
Ctor: Class<Component>
) {
const options = Ctor.options
let contextVm
if (hasOwn(parent, '_uid')) {
contextVm = Object.create(parent)
contextVm._original = parent
} else {
contextVm = parent
parent = parent._original
}
const isCompiled = isTrue(options._compiled)
const needNormalization = !isCompiled
this.data = data
this.props = props
this.children = children
this.parent = parent
this.listeners = data.on || emptyObject
this.injections = resolveInject(options.inject, parent)
this.slots = () => {
if (!this.$slots) {
normalizeScopedSlots(
data.scopedSlots,
this.$slots = resolveSlots(children, parent)
)
}
return this.$slots
}
Object.defineProperty(this, 'scopedSlots', ({
enumerable: true,
get () {
return normalizeScopedSlots(data.scopedSlots, this.slots())
}
}: any))
if (isCompiled) {
this.$options = options
this.$slots = this.slots()
this.$scopedSlots = normalizeScopedSlots(data.scopedSlots, this.$slots)
}
if (options._scopeId) {
this._c = (a, b, c, d) => {
const vnode = createElement(contextVm, a, b, c, d, needNormalization)
if (vnode && !Array.isArray(vnode)) {
vnode.fnScopeId = options._scopeId
vnode.fnContext = parent
}
return vnode
}
} else {
this._c = (a, b, c, d) => createElement(contextVm, a, b, c, d, needNormalization)
}
}
installRenderHelpers(FunctionalRenderContext.prototype)
// 拷贝 vnode,以便于根据 isCloned 属性判断是否拷贝节点,如是,可复用
function cloneAndMarkFunctionalResult (vnode, data, contextVm, options, renderContext) {
const clone = cloneVNode(vnode)
clone.fnContext = contextVm
clone.fnOptions = options
if (process.env.NODE_ENV !== 'production') {
(clone.devtoolsMeta = clone.devtoolsMeta || {}).renderContext = renderContext
}
if (data.slot) {
(clone.data || (clone.data = {})).slot = data.slot
}
return clone
}
3.2.2 异步组件
异步组件 通常是 asyncFactory = (resolve, reject) => {} 或 () => ({ component }) 函数注册的组件。异步组件主要由 resolveAsyncComponent 函数处理其加载逻辑。当异步组件在加载过程中,Vue 会使用 createAsyncPlaceholder 创建占位节点。当异步组件加载完成后,Vue 会通过强制重绘父组件的方式,启动该异步组件的渲染过程。实际上,在加载完成后,Vue 会将该异步组件填充到 asyncFactory.resolved 属性中。因此,若有其他组件需要渲染该异步组件时,直接取出 asyncFactory.resolved 作为 vm 实例的构造器并完成渲染即可。
回溯上文的 vnode 模型,其中的 asyncFactory, asyncMeta 属性实际只存在于占位节点中。
// 异步组件的渲染机制
// 初次加载异步组件,二次以后直接消费已加载的异步组件
function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>
): Class<Component> | void {
// 当异步组件加载完成再次被使用时,factory.errorComp, factory.resolved 等属性可能填满了值
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}
if (isDef(factory.resolved)) {
return factory.resolved
}
const owner = currentRenderingInstance// 当前渲染的组件实例,作为异步组件的父组件
// factory.owners 记录异步组件的所有父组件,通过强制更新父组件渲染异步组件
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
factory.owners.push(owner)
}
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
if (owner && !isDef(factory.owners)) {
const owners = factory.owners = [owner]
let sync = true
let timerLoading = null
let timerTimeout = null
;(owner: any).$on('hook:destroyed', () => remove(owners, owner))
// 通过强制重绘父组件完成异步组件的渲染
const forceRender = (renderCompleted: boolean) => {
for (let i = 0, l = owners.length; i < l; i++) {
(owners[i]: any).$forceUpdate()
}
if (renderCompleted) {
owners.length = 0
if (timerLoading !== null) {
clearTimeout(timerLoading)
timerLoading = null
}
if (timerTimeout !== null) {
clearTimeout(timerTimeout)
timerTimeout = null
}
}
}
const resolve = once((res: Object | Class<Component>) => {
// factory.resolved 即 import 加载的组件,或者通过后端返回数据构建的 Vue 子类
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
forceRender(true)
} else {
owners.length = 0
}
})
const reject = once(reason => {
process.env.NODE_ENV !== 'production' && warn(
`Failed to resolve async component: ${String(factory)}` +
(reason ? `nReason: ${reason}` : '')
)
if (isDef(factory.errorComp)) {
factory.error = true// 置为真值,以便在重绘时渲染 factory.errorComp
forceRender(true)
}
})
const res = factory(resolve, reject)// 加载异步组件
if (isObject(res)) {
if (isPromise(res)) {
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
} else if (isPromise(res.component)) {
res.component.then(resolve, reject)
if (isDef(res.error)) {
// 构建 factory.errorComp,以便二次渲染
factory.errorComp = ensureCtor(res.error, baseCtor)
}
if (isDef(res.loading)) {
factory.loadingComp = ensureCtor(res.loading, baseCtor)
if (res.delay === 0) {
factory.loading = true
} else {
timerLoading = setTimeout(() => {
timerLoading = null
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender(false)
}
}, res.delay || 200)
}
}
if (isDef(res.timeout)) {
timerTimeout = setTimeout(() => {
timerTimeout = null
if (isUndef(factory.resolved)) {
reject(
process.env.NODE_ENV !== 'production'
? `timeout (${res.timeout}ms)`
: null
)
}
}, res.timeout)
}
}
}
sync = false
// 异步组件已加载完成,返回待渲染的组件
return factory.loading
? factory.loadingComp
: factory.resolved
}
}
// 创建占位节点
createAsyncPlaceholder (
factory: Function,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag: ?string
): VNode {
const node = createEmptyVNode()
node.asyncFactory = factory
node.asyncMeta = { data, context, children, tag }
return node
}
4 渲染 vnode
依据上文,vnode 需要通过 vm.__patch__ 方法完成渲染或重绘出真实 dom。vm.__patch__ 方法的创建过程是基于高阶函数 createPatchFunction,首先将不同平台的 dom 操作作为参数传入 createPatchFunction,然后生成实际针对不同平台的 patch 函数。在 createPatchFunction(backend) 函数中,backend 即不同平台中以钩子形式提供的 dom 操作函数集。
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
// 将平台钩子注入到 createPatchFunction 函数中,以便调用
export function createPatchFunction (backend) {
const cbs = {}
const { modules, nodeOps } = backend// 不同环境的节点、样式、事件等操作
// 从 modules 中读取平台钩子(可能是浏览器钩子),以钩子形式更新节点的属性、事件
// modules 即包含 class, style, attrs, events, domProps, transition
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) {
// ...
}
}
因此,下面以浏览器钩子展开说明。
4.1 浏览器钩子
在浏览器环境中,钩子函数集合包含 class 样式类、style 样式、attrs 节点属性、events 事件、domProps 节点内容等、transition 动效六类操作。
- class.js: 提供 create, update 钩子,使用 el.setAttribute('class', cls) 方法设置或更新 vnode.elm 元素的样式类。
- style.js: 提供 create, update 钩子,使用 el.style.setProperty(name, val) 或 el.style[name] = val 形式设置或更新 vnode.elm 元素的样式。
- attrs.js: 提供 create, update 钩子,使用 el.setAttribute(key, value) 或 el.removeAttribute(key) 方法设置或更新 vnode.elm 元素的 html 属性。
- events.js: 提供 create, update 钩子,使用 el.addEventListener, el.removeEventListener 方法设置或更新 vnode.elm 元素的绑定函数。
- domProps.js: 提供 create, update 钩子,包含:当 vnode.data.domProps 属性包含 textContent, innerHTML 时,使用 el.removeChild 移除子节点,并将值写入 el 属性中等。
- transition.js: 提供 create, activate, remove 钩子,用于实现 css transition,这里不作解读。
钩子的种类以及执行时机为:
- create: 创建真实 dom 节点时。
- activate: transition 过程中动效执行时。
- update: 更新真实 dom 节点时。
- remove: transition 过程中移除节点时。
- destroy: 移除真实 dom 节点时,即组件卸载或 keep-alive 子组件置为非激活状态时。
4.2 vnode 钩子
除了平台钩子以外,patch 执行过程中还包含 vnode 钩子。通过上文也可以发现,Vue 在执行 createComponent 函数时,会通过调用 installComponentHooks 函数将 vnode 钩子注入到 vnode.data.hook 中。
- init 钩子: 如果 vnode 对应未销毁状态的 keep-alive 组件,更新该 keep-alive 组件;如果 vnode 对应组件节点,实例化该组件,使用该组件的内置模板完成渲染,生成 vnode.elm。执行时机为更新组件时。
- prepatch 钩子: 更新组件。执行时机为更新组件时。
- insert 钩子: 将完成挂载或更新的 vnode 节点插入到父节点中,且调用组件的 mounted 生命周期。执行时机为组件所对应的节点树都渲染到文档中时。
- destroy 钩子: 卸载组件,或者将 keep-alive 子组件置为非激活状态。执行时机为 vnode 节点移除时。
const componentVNodeHooks = {
// 更新 keep-alive 组件,或实例化模板中的子节点组件并完成挂载
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
// keep-alive 组件,复用 vnode,并对其追加补丁
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
const mountedNode: any = vnode
componentVNodeHooks.prepatch(mountedNode, mountedNode)
// 如模板节点为组件节点,实例化该组件,并予以挂载
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
// vm.$mount 方法通过 vm._update 挂载组件
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
// 更新组件
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
// 更新组件 vm 实例的属性,并酌情重绘
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
},
// 当组件所对应的 vnode 树都渲染到文档中时,调用 mounted 生命周期
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
// 调用组件的 mounted 生命周期
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// 缓存 vm 实例,直到 patch 过程结束才予以激活
queueActivatedComponent(componentInstance)
} else {
// 直接激活 vm 实例
activateChildComponent(componentInstance, true /* direct */)
}
}
},
// 启动卸载、或将 keep-alive 组件置为未激活状态
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
}
4.3 钩子触发器
Vue 封装了如下的钩子触发器:
// 触发 create 平台钩子以及 vnode 钩子
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)
}
}
// vnode 树中的子节点须等待组件内容都渲染到文档中时,才执行 insert 钩子
function invokeInsertHook (vnode, queue, initial) {
// 根组件中的子节点须等待跟组件实际渲染到文档中
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
// 递归销毁组件、移除 vnode 节点属性
function invokeDestroyHook (vnode) {
let i, j
const data = vnode.data
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
}
if (isDef(i = vnode.children)) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j])
}
}
}
4.4 patch
vnode 的 patch 过程就是完成挂载、重绘或卸载。patch 过程中渲染的直接表现是生成 vnode.elm 或 vnode.text。
![fc4e08f85e0c9f3d8edd271ea8e6c752.png](https://img-blog.csdnimg.cn/img_convert/fc4e08f85e0c9f3d8edd271ea8e6c752.png)
function patch (oldVnode, vnode, hydrating, removeOnly) {
// 组件卸载
if (isUndef(vnode)) {
// 深度优先遍历 oldVnode 树中子节点,执行 cbs.destory 以及 vnode.data.hook.destory 钩子
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
// 组件挂载
if (isUndef(oldVnode)) {
isInitialPatch = true
// createElm 函数将 vnode 及 children 解析成节点树 vnode.elm
createElm(vnode, insertedVnodeQueue)
// 组件更新
} else {
const isRealElement = isDef(oldVnode.nodeType)// 原生节点
// 组件节点的数据发生变更,更新组件
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patchVnode 函数更新组件节点或原生节点
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// 服务端渲染的是元素节点,采用 hydrate 函数进行渲染,oldVnode 即真实的 dom 元素
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
// hydrate 函数将 oldVnode 作为 vnode 的 elm 属性,并递归处理子节点等
// 当为 component 模板节点或原生节点、文本节点、注释节点时返回真值,否则返回否值
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
// invokeInsertHook 函数对于根节点,将 insertedVnodeQueue 存入 vnode.parent.data.pendingInsert 中,等待调用 initComponent 时执行
// 其他,触发 insertedVnodeQueue 数组项中的 data.hook.insert 钩子
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode// 真实的 dom 节点
} 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.'
)
}
}
// emptyNodeAt 函数构建一个 VNode 实例,该实例使用 oldVnode 作为 elm 属性
oldVnode = emptyNodeAt(oldVnode)
}
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,// 在执行 leave 动效时不将 vnode 插入到 parentElm 中
nodeOps.nextSibling(oldElm)// 使用兄弟节点锁定插入 parentElm 时的位置
)
// vnode.parent 查找模板节点
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
// isPatchable 函数向上寻找非 keep-alive 组件,且须判断该组件对应的 vnode.tag 为已定义
// patchable 意味着 keep-alive 组件下复用已缓存的某子组件
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)
}
const insert = ancestor.data.hook.insert
if (insert.merged) {
for (let i = 1; i < insert.fns.length; i++) {// i 从 1 起始,避免执行组件的 mounted 钩子
insert.fns[i]()
}
}
} else {
// registerRef 函数为外层组件实例 ancestor.context 设置 ref 引用
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
if (isDef(parentElm)) {
// 移除原始节点
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
// 执行 destroy 平台钩子以及 vnode 钩子
invokeDestroyHook(oldVnode)
}
}
}
// 只针对 createElm, hybrate 收集到插入情形
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
5 参考
Vue原理解析之Virtual Dom
patch