Vue keep-alive 组件原理
组件实现原理
kotlin
// src/core/components/keep-alive.js
export default {
name: 'keep-alive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
methods: {
cacheVNode() {
const { cache, keys, vnodeToCache, keyToCache } = this
if (vnodeToCache) {
const { tag, componentInstance, componentOptions } = vnodeToCache
cache[keyToCache] = {
name: getComponentName(componentOptions),
tag,
componentInstance,
}
keys.push(keyToCache)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
this.vnodeToCache = null
}
}
},
created () {
this.cache = Object.create(null)
this.keys = []
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
this.cacheVNode()
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
updated () {
this.cacheVNode()
},
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = vnode.key == null
// 相同的构造函数可能会注册为不同的本地组件,因此仅 cid 是不够的
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
// 如果已经缓存该子组件,获取组件的实例
vnode.componentInstance = cache[key].componentInstance
// 更新缓存的key
remove(keys, key)
keys.push(key)
} else {
// 缓存将要缓存节点以及节点的key值
this.vnodeToCache = vnode
this.keyToCache = key
}
// 缓存的节点添加keepAlive属性为true
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
组件原理
是一个抽象组件,只对包裹的子组件做处理,自身不会渲染DOM元素,也不会出现在组件的父组件链中。那如何实现是一个抽象组件呢?
组件有一个abstract属性,值为true,其包裹的子组件在执行initLifcycle方法时,会判断其父组件是否为抽象组件,如果是抽象组件则忽略与此组件的父子关系,设置更高层级组件为父组件。
php
// src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
const options = vm.$options
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
// ...
}
组件没有编写组件模板template,而是实现了render方法。在进行组件渲染时,会调用自身的render方法,通过render方法的返回结果决定渲染结果。
kotlin
// src/core/components/keep-alive.js
render () {
// 获取默认插槽
const slot = this.$slots.default
// 获取插槽中的第一个节点
const vnode: VNode = getFirstComponentChild(slot)
// 获取组件options
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// 获取组件名
const name: ?string = getComponentName(componentOptions)
// 校验是否缓存
const { include, exclude } = this
// 不缓存的情况
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
// 获取组件的key
const key: ?string = vnode.key == null
// 相同的构造函数可能会注册为不同的本地组件,因此仅 cid 是不够的
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
// 如果已经缓存该组件节点,获取组件的实例
vnode.componentInstance = cache[key].componentInstance
// 更新缓存的key
remove(keys, key)
keys.push(key)
} else {
// 缓存将要缓存节点以及节点的key值
this.vnodeToCache = vnode
this.keyToCache = key
}
// 缓存的节点添加keepAlive属性为true
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
在render方法中,首先会获取组件的默认插槽,然后调用getFirstComponentChild方法获取默认插槽中的第一个子元素的vnode,此vnode将作为render方法执行的结果被返回。因此如果组件包裹多个元素只会渲染第一个元素。
arduino
// 获取默认插槽
const slot = this.$slots.default
// 获取插槽中的第一个元素节点
const vnode: VNode = getFirstComponentChild(slot)
接下来调用getComponentName获取组件名,同时获取组件接收到的include和exclude参数。通过判断组件名是否存在于include和exclude列表中来确定该组件是否需要被缓存。如果该组件不需要被缓存,则直接返回节点,否则接着进行后续的处理。
typescript
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
// match 方法
function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
return pattern.test(name)
}
/* istanbul ignore next */
return false
}
组件在created钩子函数中,定义了cache 和key两个属性。cache中保存着需要被缓存的组件节点vnode,keys保存这组件节点的key。
kotlin
// src/core/components/keep-alive.js
created () {
this.cache = Object.create(null)
this.keys = []
}
接着判断节点是否存在于cache中,如果节点已存在于cache中即命中了缓存,会将缓存的节点的实例赋值给vnode.componentInstance,同时更新keys列表中保存的组件节点的key;如果没有命中缓存,则将需要缓存的组件节点以及组件节点的key保存在vnodeToCache和keyToCache中,同时设置vnode.data.keepAlive = true,即标记该节点是被缓存节点,最后将该节点返回。
kotlin
const { cache, keys } = this
const key: ?string = vnode.key == null
// 相同的构造函数可能会注册为不同的本地组件,因此仅 cid 是不够的
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
// 如果已经缓存该子组件,获取组件的实例
vnode.componentInstance = cache[key].componentInstance
// 更新缓存的key
remove(keys, key)
keys.push(key)
} else {
// 缓存将要缓存节点以及节点的key值
this.vnodeToCache = vnode
this.keyToCache = key
}
// 缓存的节点添加keepAlive属性为true
vnode.data.keepAlive = true
在完成组件挂载后,执行组件的mounted钩子函数,此时vnode.componentInstance中已经保存了节点的实例。调用cacheVnode方法会将render方法中缓存的节点和key缓存在cache以及keys中。此外,mounted中还设置了include、exclude的监听方法,以便及时更新cache和keys中缓存节点和节点的key。当设置了max值且keys的列表长度超过max值时,会采取先进先出的原则调用pruneCacheEntry方法将缓存最久未使用的的节点从cache和keys移除并且执行$destroy销毁组件实例,
kotlin
// src/core/components/keep-alive.js
mounted () {
// 缓存节点
this.cacheVNode()
// 监听include数据变化
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
// 监听exclude数据变化
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
}
methods: {
cacheVNode() {
const { cache, keys, vnodeToCache, keyToCache } = this
if (vnodeToCache) {
// 缓存中,防止在同一时间进行多个缓存任务
const { tag, componentInstance, componentOptions } = vnodeToCache
// 存放组件实例
cache[keyToCache] = {
name: getComponentName(componentOptions),
tag,
// 组件实例渲染后会存在 $el 属性,后续直接复用 $el 属性
componentInstance,
}
// 缓存组件的key
keys.push(keyToCache)
// 缓存更新算法(LRU),超过最大长度时,移除第一个缓存的组件以及组件的key
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
this.vnodeToCache = null
}
}
}
function pruneCacheEntry (
cache: CacheEntryMap,
key: string,
keys: Array<string>,
current?: VNode
) {
const entry: ?CacheEntry = cache[key]
if (entry && (!current || entry.tag !== current.tag)) {
entry.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
组件渲染流程
示例:
初始化渲染
Vue的渲染会经过patch过程,在patch过程中会执行createElm方法将节点挂载到界面。而在createElm方法中会执行createComponent方法来挂载组件。
scss
// src/vore/vdom/patch.js
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
/**
* 此虚拟节点在之前的渲染中使用过!
* 当前它被用作新节点,当它用作插入引用节点时,覆盖其 elm 会导致潜在的修补错误。相反,我们在为节点创建关联的 DOM 元素之前按需克隆节点。
*/
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// ...
}
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
/**
* 获取 vnode.data 对象
*/
let i = vnode.data
if (isDef(i)) {
/**
* 验证组件实例是否已经存在 && 被 keep-alive 包裹
*/
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
/**
* 执行 vnode.data.hook.init 钩子函数,该函数在将 render helper 时讲过
* 如果是被 keep-alive 包裹的组件,则再执行 prepatch 钩子,用 vnode 上的各个属性更新 oldVnode 上的相关属性
* 如果是组件没有被 keep-alive 包裹或者首次渲染,则初始化组件,并进入挂载阶段
*/
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
/**
* 如果 vnode 是一个子组件,则调用 init 钩子之后会创建一个组件实例,并挂载
* 这时候就可以给组件执行各个模块的 create 钩子
*/
initComponent(vnode, insertedVnodeQueue)
/**
* 将组件的 DOM 节点插入到父节点内
*/
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
/**
* 组件被 keep-alive 包裹的情况,激活组件
*/
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
createComponent方法中定义了isReactivated用于标记当时节点是否处于被激活的状态。当初次渲染时,由于组件的render方法提前执行,此时keepAlive为true,而此时AC组件还没有被实例化,因此vnode.componentInstance为null,因此此时isReactivated为false。当AC组件节点被实例化后,componentInstance就保存了组件实例。当AC组件再次被激活时,isReactivated为true。
ini
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
接下来执行init方法,在init方法中,会判断组件是否已被缓存,如果被缓存就执行prepatch方法,否则调用createComponentInstanceForVnode方法创建组件实例并且将组件实例赋值给vnode.componentInstance。
初始化渲染时,直接执行createComponentInstanceForVnode方法初始化AC组件实例并且将组件实例赋值给vnode.componentInstance。
php
// src/core/vdom/create-component.js
const componentVNodeHooks = {
/**
* 初始化
* @param {*} vnode
* @param {*} hydrating
*/
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// keep-alive components, treat as a patch
/**
* 被 keep-alive 包裹的组件
*/
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
/**
* 创建组件实例,即 new vnode.componentOptions.Ctor(options) => 得到 Vue 组件实例
*/
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
/**
* 执行组件的 $mount 方法,进入挂载阶段,接下来就是通过编译器得到 render 函数,接着走挂载、patch 这条路,直到组件渲染到界面
*/
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
//...
}
arduino
// src/core/vdom/create-component.js
// 创建子组件实例
export function createComponentInstanceForVnode (
// we know it's MountedComponentVNode but flow doesn't
vnode: any,
// activeInstance in lifecycle state
parent: any
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
/**
* 检查内联模版渲染函数
*/
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
/**
* new vnode.componentOptions.Ctor(options) => Vue 实例
*/
return new vnode.componentOptions.Ctor(options)
}
在createComponentInstanceForVnode方法中,最后执行的vnode.componentOptions.Ctor(options)创建组件实例时,会执行_init()方法。
当组件实例化完成后,执行mount方法,进行render编译、patch挂载。当mount方法执行完成后,vnode.$el已经保存了需要挂载到界面的dom元素。
bash
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
在mount方法执行完,回到createComponent方法中接着执行后续代码。此时vnode.componentInstance已保存了组件的实例以及el。
scss
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
在判断vnode.componentInstance有值后,就做了下面两件事:
- 调用initComponent方法,将vnode.componentInstance.$el赋值给vnode.elm
scss
// src/core/vdom/patch.js
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode)
}
}
- 调用insert方法,将vnode.elm中保存的真实dom元素插入到父元素中
sql
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
至此,初始化渲染是在正常渲染AC组件的同时将AC组件进行缓存,等到重复渲染使用。
重复渲染
当重新激活AC组件时,组件的缓存被重新激活。
再次经历patch过程,在patch过程中会执行patchVnode方法去对比新旧vnode节点以及它们的children节点进行逻辑更新。
scss
// src/core/vdom/patch.js
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
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
}
// 静态树的重用元素
// 仅在克隆虚拟节点时才执行此操作 - 如果未克隆新节点,则意味着渲染函数已被热重载 API 重置,需要执行适当的重新渲染。
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)
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
在执行patchVnode方法时遇到组件节点会执行prepatch方法。在prepatch方法中调用updateChildComponent方法。
arduino
// src/core/vdom/create-compoent.js
const componentVNodeHooks = {
// ...
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
/**
* 新的 VNode 的组件配置项
*/
const options = vnode.componentOptions
/**
* 旧的 VNode 的组件实例
*/
const child = vnode.componentInstance = oldVnode.componentInstance
/**
* 用 VNode 上的属性更新 child 上的各种属性
*/
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
}
}
updateChildComponent方法会更新实例属性和方法。
php
// src/core/instance/lifecycle.js
export function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
if (process.env.NODE_ENV !== 'production') {
isUpdatingChildComponent = true
}
// 确定组件是否具有插槽子项,我们需要在覆盖 $options._renderChildren 之前执行此操作。
// 检查是否有动态作用域槽(手写或编译,但具有动态槽名)。从模板编译的静态范围槽具有"$stable"标记
const newScopedSlots = parentVnode.data.scopedSlots
const oldScopedSlots = vm.$scopedSlots
const hasDynamicScopedSlot = !!(
(newScopedSlots && !newScopedSlots.$stable) ||
(oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
(newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key) ||
(!newScopedSlots && vm.$scopedSlots.$key)
)
// 父级的任何静态插槽子级可能在父级更新期间已更改。动态作用域槽也可能已更改。在这种情况下,必须强制更新以确保正确性。
const needsForceUpdate = !!(
// 存在新的静态插槽
renderChildren ||
// 存在老的静态插槽
vm.$options._renderChildren ||
hasDynamicScopedSlot
)
vm.$options._parentVnode = parentVnode
vm.$vnode = parentVnode // update vm's placeholder node without re-render
if (vm._vnode) {
// 重新设置子节点树的父节点
vm._vnode.parent = parentVnode
}
vm.$options._renderChildren = renderChildren
// 在渲染期间使用他们,更新$attrs和$listeners的hash可能会触发子节点的更新
vm.$attrs = parentVnode.data.attrs || emptyObject
vm.$listeners = listeners || emptyObject
// 更新props
if (propsData && vm.$options.props) {
toggleObserving(false)
const props = vm._props
const propKeys = vm.$options._propKeys || []
for (let i = 0; i < propKeys.length; i++) {
const key = propKeys[i]
const propOptions: any = vm.$options.props // wtf flow?
props[key] = validateProp(key, propOptions, propsData, vm)
}
toggleObserving(true)
// keep a copy of raw propsData
vm.$options.propsData = propsData
}
// 更新listeners
listeners = listeners || emptyObject
const oldListeners = vm.$options._parentListeners
vm.$options._parentListeners = listeners
updateComponentListeners(vm, listeners, oldListeners)
// 更新插槽属性 + 强制更新(如果有子项)
if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
if (process.env.NODE_ENV !== 'production') {
isUpdatingChildComponent = false
}
}
此外,由于组件包裹着插槽,因此在执行updateChildComponent方法的过程中,needForceUpdate为true,它会先去更新实例的插槽属性,接着执行vm.$forceUpdate方法。
ini
if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
vm.$forceUpdate方法会执行vm._watcher.update方法,从而重新执行组件的render方法。当前激活的AC组件已经在初始化阶段被缓存,因此在执行render方法的过程中,会将缓存中的componentInstance复制给vnode.componentInstance,并且更新keys列表,最后返回节点。
kotlin
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
// delay setting the cache until update
this.vnodeToCache = vnode
this.keyToCache = key
}
render方法执行完成,进行节点的patch挂载阶段。在这里又会去执行createComponent方法,接着执行init方法。此时各种条件已经满足会执行prepatch方法,进而调用updateChildComponent方法去更新实例的属性和方法。
此次执行init方法,没有像初始化渲染时进行组件实例化,执行$mount方法,因此不会执行created、mounted钩子函数。
php
// src/core/vdom/create-compoent.js
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
/**
* 新的 VNode 的组件配置项
*/
const options = vnode.componentOptions
/**
* 旧的 VNode 的组件实例
*/
const child = vnode.componentInstance = oldVnode.componentInstance
/**
* 用 VNode 上的属性更新 child 上的各种属性
*/
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
},
// ...
}
init方法执行完成后,回到createComponent方法中,接着执行后续代码,此时isReactivated为true。
scss
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
返回createComponent其实就做了三件事:
- 重新对vnode.elm赋值
- 将vnode.elm插入到父元素中
- 执行缓存节点的相关动画
最后回到patch中,执行removeVnodes方法移除旧节点。至此本次缓存渲染流程结束。
生命周期函数
组件给缓存的组件添加了activated、deactivated两个生命周期函数。
activated
在patch方法中,会执行invokeInsertHook方法。
arduino
// src/core/vdom/patch.js
function invokeInsertHook (vnode, queue, initial) {
// 延迟insert钩子的执行时间,等元素真正插入后在给根节点插入insert钩子函数
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])
}
}
}
invokeInsertHook方法中会调用insert方法。
scss
// src/core/vdom/create-component.js
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// 在更新期间,保持活动状态组件的子组件可能会更改,
// 因此直接在此处遍历树可能会在不正确的子组件上调用激活的钩子。
// 相反,我们将它们推送到一个队列中,该队列将在整个补丁过程结束后进行处理。
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
}
在insert方法中,会判断节点是否被渲染,如果已被渲染就执行queueActivatedComponent。如果未被渲染,则执行activateChildComponent方法。
在queueActivatedComponent方法中,当前实例的钩子函数只是被缓存起来,并没有立即添加到执行的堆栈中执行。
arduino
// src/core/oberser/scheduler.js
export function queueActivatedComponent (vm: Component) {
// setting _inactive to false here so that a render function can
// rely on checking whether it's in an inactive tree (e.g. router-view)
vm._inactive = false
activatedChildren.push(vm)
}
等到所有的渲染完成之后,在nextTick后会执行flushSchedulerQueue方法,进而调用callActivatedHooks方法。
scss
// src/core/oberser/scheduler.js
function flushSchedulerQueue () {
// ...
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// ...
}
callActivatedHooks中会给activatedQueue列表中保存的实例依次执行activateChildComponent方法。而activateChildComponent方法会将每个实例以及递归实例的子实例执行回调activated生命周期函数,等待后续执行。
ini
// src/core/oberser/scheduler.js
function callActivatedHooks (queue) {
for (let i = 0; i < queue.length; i++) {
queue[i]._inactive = true
activateChildComponent(queue[i], true /* true */)
}
}
// src/core/instance/lifecycle.js
export function activateChildComponent (vm: Component, direct?: boolean) {
if (direct) {
vm._directInactive = false
if (isInInactiveTree(vm)) {
return
}
} else if (vm._directInactive) {
return
}
if (vm._inactive || vm._inactive === null) {
vm._inactive = false
for (let i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i])
}
callHook(vm, 'activated')
}
}
deactivated
在patch方法中,会调用removeVnodes方法移除旧节点。而在removeVnodes方法中,会执行removeAndInvokeRemoveHook。
scss
// src/core/vdom/patch.js
function removeVnodes (vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx]
if (isDef(ch)) {
if (isDef(ch.tag)) {
removeAndInvokeRemoveHook(ch)
invokeDestroyHook(ch)
} else { // Text node
removeNode(ch.elm)
}
}
}
}
removeAndInvokeRemoveHook会去执行invokeDestroyHook方法。invokeDestroyHook会执行destroy方法。在destroy方法中会调用deactivateChildComponent方法,deactivateChildComponent方法与activateChildComponent方法类似,回调组件的deactivated钩子函数,并且递归去回调它的所有子组件的deactivated钩子函数。
scss
// src/core/vdom/patch.js
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])
}
}
}
// src/core/vdom/create-component.js
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
总结
组件是抽象组件,不会被渲染到界面上。初始化渲染节点时,它采用LRU策略去缓存组件的vnode,等到再次渲染节点时,会读取缓存中的节点使其渲染到界面上实现状态缓存,同时添加了activated和deactivated两个钩子函数,供组件在使用。