原本想的Vue
中对于自定义组件的处理,无非就是通过调用几个钩子,然后来创建原生的dom
结构。当自己准备写这一部分的分析时,才发现给自己留了一个大坑。自定义组件的创建处理,涉及到整个Vue
的各个阶段,本文可以说是从一个小栗子查看Vue的生命周期在生命周期处理阶段的一个补充。
在vdom——VNode一文的最后,讲解createComponent
方法时,我们提到在这儿会给data.hook
上添加四个钩子函数——init
、prepatch
、insert
、destroy
。我们一起来看一下每个时期,都分别作了哪些处理。
回到patch
方法中,我们在创建dom
元素时,首先会调用一个createComponent
方法来判断当前的vnode
是不是一个自定义组件。
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
...
}
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */, parentElm, refElm)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
上面的代码中,我会调用init
方法,并传入四个参数。从之前patch
相关内容的讲解中,我们知道,第一个参数vnode
就是当前自定义组件的vnode
,第二个参数直接就是false
,第三个参数parentElm
是当前元素的父元素,第四个参数是值要把当前元素插入到refElm
之前。
接着我们来看init
方法里面都进行了哪些操作。
init (
vnode: VNodeWithData,
hydrating: boolean,
parentElm: ?Node,
refElm: ?Node
): ?boolean {
if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance,
parentElm,
refElm
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
} else if (vnode.data.keepAlive) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
}
}
对于keep-alive
组件,我们暂且不管。如果vnode.componentInstance
不存在或已经销毁,则通过createComponentInstanceForVnode
方法来创建新的Vue
实例。
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
parentElm?: ?Node,
refElm?: ?Node
): Component {
const vnodeComponentOptions = vnode.componentOptions
const options: InternalComponentOptions = {
_isComponent: true,
parent,
propsData: vnodeComponentOptions.propsData,
_componentTag: vnodeComponentOptions.tag,
_parentVnode: vnode,
_parentListeners: vnodeComponentOptions.listeners,
_renderChildren: vnodeComponentOptions.children,
_parentElm: parentElm || null,
_refElm: refElm || null
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (inlineTemplate) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnodeComponentOptions.Ctor(options)
}
componentOptions
中包括五项数据,Ctor
是自定义组件的构造函数,propsData
是父组件通过props
传递的数据,listeners
是添加在当前组件上的事件,tag
是自定义的标签名,children
即当前自定义组件的子元素。
createComponentInstanceForVnode
接收了四个参数,第一个就是当前组件的vnode
,第二个是父Vue
实例,第三个是父元素,第四个是后面的兄弟元素。最终我们会调用new vnodeComponentOptions.Ctor(options)
来创建一个新的Vue
实例。
所以,我们又回到了创建Vue
实例的生命周期。
回到src/core/instance/init.js
中的Vue.prototype._init
方法。
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
这一次对于options
的处理我们走进了if
块。
function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
opts.parent = options.parent
opts.propsData = options.propsData
opts._parentVnode = options._parentVnode
opts._parentListeners = options._parentListeners
opts._renderChildren = options._renderChildren
opts._componentTag = options._componentTag
opts._parentElm = options._parentElm
opts._refElm = options._refElm
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
vdom——VNode一文中我们也说过,自定义组件的构造函数,是Vue
对象的一个子对象。在我们新建Vue
对象时,会通过mergeOptions
方法来把vm.constructor
上的options
值与传入的options
合并然后赋值给vm.$options
。而这里我们通过Vue.extends
创建新的对象时,已经把当前自定义组件的配置项合并到了vm.constructor.options
上,所以我们这里vm.$options
只需要继承一下就可以了。
同时,我们还给vm.$options
添加了一些内部属性,具体每个属性的含义,我总结在了Vue实例属性中。
new vnodeComponentOptions.Ctor(options)
仅仅是初始化了一个新的Vue
实例,真正挂载到页面中,是通过child.$mount(hydrating ? vnode.elm : undefined, hydrating)
进行的。上面可以知道hydrating
是false
,所以第一个参数是undefined
,第二个参数是false
。
$mount
方法会先处理模板,最终还是调用src/core/instance/lifecycle
中的Vue.prototype._update
方法渲染组件。
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
又回到了多次谈到的__patch__
方法。
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
...
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
...
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
这一次oldVnode
就是undefined
,所以直接走到if
块代码中,然后调用createElm
方法来创建dom
结点。之后的流程,见patch——创建dom。
从名字我们也可以看出,该方法是在进行diff
操作之前进行的处理。它的调用之处在patchVnode
中:
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
if (oldVnode === vnode) {
return
}
...
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
...
}
来看看它里面进行了哪些操作。
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
},
该方法接受两个参数,分别是旧新Vnode
实例。如果数据有所更新,会再次调用render
函数生成新的VNode
实例,这个过程中会再次调用createComponent
函数生成新的自定义组件的vnode
。
调用prepatch
钩子函数的前提,说明该自定义组件得到了复用,也就是说该自定义组件本身没有被替换,我们只需要根据传入的props
或者slots
等来更新子模板的内容。这里我们直接复用oldVnode.componentInstance
,然后调用updateChildComponent
方法来更新子内容。
export function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: VNode,
renderChildren: ?Array<VNode>
) {
const hasChildren = !!(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
parentVnode.data.scopedSlots || // has new scoped slots
vm.$scopedSlots !== emptyObject // has old scoped slots
)
// 更新vnode相关关系
vm.$options._parentVnode = parentVnode
vm.$vnode = parentVnode // update vm's placeholder node without re-render
if (vm._vnode) { // update child tree's parent
vm._vnode.parent = parentVnode
}
vm.$options._renderChildren = renderChildren
// 更新 props
if (propsData && vm.$options.props) {
observerState.shouldConvert = false
if (process.env.NODE_ENV !== 'production') {
observerState.isSettingProps = true
}
const props = vm._props
const propKeys = vm.$options._propKeys || []
for (let i = 0; i < propKeys.length; i++) {
const key = propKeys[i]
props[key] = validateProp(key, vm.$options.props, propsData, vm)
}
observerState.shouldConvert = true
if (process.env.NODE_ENV !== 'production') {
observerState.isSettingProps = false
}
// keep a copy of raw propsData
vm.$options.propsData = propsData
}
// 更新 listeners
if (listeners) {
const oldListeners = vm.$options._parentListeners
vm.$options._parentListeners = listeners
updateComponentListeners(vm, listeners, oldListeners)
}
// 处理slots并强制更新
if (hasChildren) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
}
该方法所要做的工作主要有以下几个方面。
1、更新vm
上绑定的有关vnode
的各项数据。之前我们通过vm.$options._parentVnode
、vm.$vnode
、vm.$options._renderChildren
等保存了当前自定义组件在父组件中的vnode
以及在父组件中当前自定义组件的子元素,patch
时会生成新的vnode
,所以需要更新相应的内容。
2、更新保存父组件传递过来的数据propsData
,并对传递的数据类型等进行校验。
3、更新绑定的事件
4、更新slots
相关内容
insert
方法的调用是在dom
插入到页面之后调用的。具体方法是在__patch__
中的invokeInsertHook
方法。
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])
}
}
}
insert
钩子函数的具体实现如下所示:
insert (vnode: MountedComponentVNode) {
if (!vnode.componentInstance._isMounted) {
vnode.componentInstance._isMounted = true
callHook(vnode.componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
activateChildComponent(vnode.componentInstance, true /* direct */)
}
},
我们发现,这里的主要操作是调用mounted
钩子函数。回顾一下之前讲的调用mounted
钩子函数的代码:
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
vm.$vnode
保存的是上面init
时传入的_parentVnode
,即自定义组件在父组件中的VNode
对象。根组件的该值为null
,所以在上面代码中调用mounted
,而对于自定义组件,则在insert
钩子函数中调用。
该钩子函数的调用,是在自定义组件销毁时调用。
destroy (vnode: MountedComponentVNode) {
if (!vnode.componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
vnode.componentInstance.$destroy()
} else {
deactivateChildComponent(vnode.componentInstance, true /* direct */)
}
}
}
在patch
过程中调用该钩子函数,是因为在做diff
的过程中,要删除当前的组件。对于普通组件,我们直接调用vnode.componentInstance.$destroy()
方法来销毁。
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// remove reference to DOM nodes (prevents leak)
vm.$options._parentElm = vm.$options._refElm = null
}
}
函数定义如上:
1、调用beforeDestroy
钩子函数,并通过vm._isBeingDestroyed
来标识正在销毁,避免重复调用。
2、从父元素中删除当前元素。
3、销毁watcher
4、vm._data_
的监听对象的vmCount
减1
5、标识vm
已销毁
6、销毁当前组件
7、调用destroyed
钩子函数
8、销毁事件
9、消除各种引用的资源