还有一处重要的代码是installComponentHooks(data)
。该方法会给组件vnode
的data
添加组件钩子,这些钩子在组件的不同阶段被调用,例如init
钩子在组件patch
时会调用。
function installComponentHooks (data: VNodeData) { const hooks = data.hook || (data.hook = {}) for (let i = 0; i < hooksToMerge.length; i++) { const key = hooksToMerge[i] // 外部定义的钩子 const existing = hooks[key] // 内置的组件vnode钩子 const toMerge = componentVNodeHooks[key] // 合并钩子 if (existing !== toMerge && !(existing && existing._merged)) { hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge } } } // 组件vnode的钩子。 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 ) // 挂载组件,与vue的$mount一样 child.$mount(hydrating ? vnode.elm : undefined, hydrating) } }, 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 ) }, insert (vnode: MountedComponentVNode) { const { context, componentInstance } = vnode if (!componentInstance._isMounted) { componentInstance._isMounted = true // 触发组件的mounted钩子 callHook(componentInstance, 'mounted') } if (vnode.data.keepAlive) { if (context._isMounted) { queueActivatedComponent(componentInstance) } else { activateChildComponent(componentInstance, true /* direct */) } } }, destroy (vnode: MountedComponentVNode) { const { componentInstance } = vnode if (!componentInstance._isDestroyed) { if (!vnode.data.keepAlive) { componentInstance.$destroy() } else { deactivateChildComponent(componentInstance, true /* direct */) } } } } const hooksToMerge = Object.keys(componentVNodeHooks)
最后,与普通HTML
标签一样,为组件生成vnode
节点:
// 创建 vnode const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory )
组件在patch
时对vnode的处理与普通标签有所不同。
Vue
如果发现正在patch
的vnode
是组件,那么调用createComponent
方法。
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data if (isDef(i)) { const isReactivated = isDef(vnode.componentInstance) && i.keepAlive // 执行组件钩子中的init钩子,创建组件实例 if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */) } // init钩子执行后,如果vnode是个子组件,该组件应该创建一个vue子实例,并挂载到DOM元素上。子组件的vnode.elm也设置完成。然后我们只需要返回该DOM元素。 if (isDef(vnode.componentInstance)) { // 设置vnode.elm initComponent(vnode, insertedVnodeQueue) // 将组件的elm插入到父组件的dom节点上 insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } }
createComponent
会调用组件vnode
的data
对象上定义的init
钩子方法,创建组件实例。现在我们回过头来看下init
钩子的代码:
// ... 省略其他代码 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 ) // 挂载组件,与vue的$mount一样 child.$mount(hydrating ? vnode.elm : undefined, hydrating) } } // ...省略其他代码
由于组件是初次创建,因此init
钩子会调用createComponentInstanceForVnode
创建一个组件实例,并赋值给vnode.componentInstance
。
export function createComponentInstanceForVnode ( vnode: any, parent: any, ): Component { // 内部组件选项 const options: InternalComponentOptions = { // 标记是否是组件 _isComponent: true, // 父Vnode _parentVnode: vnode, // 父Vue实例 parent } // check inline-template render functions const inlineTemplate = vnode.data.inlineTemplate if (isDef(inlineTemplate)) { options.render = inlineTemplate.render options.staticRenderFns = inlineTemplate.staticRenderFns } // new 一个组件实例。组件实例化 与 new Vue() 执行的过程相同。 return new vnode.componentOptions.Ctor(options) }
createComponentInstanceForVnode
中会执行 new vnode.componentOptions.Ctor(options)
。由前面我们在创建组件vnode
时可知,vnode.componentOptions
的值是一个对象:{ Ctor, propsData, listeners, tag, children }
,其中包含了组件的构造函数Ctor
。因此 new vnode.componentOptions.Ctor(options)
等价于new VueComponent(options)
。
// 生成组件实例 const child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance) // 挂载组件,与vue的$mount一样 child.$mount(hydrating ? vnode.elm : undefined, hydrating)
等价于:
new VueComponent(options).$mount(hydrating ? vnode.elm : undefined, hydrating)
这段代码想必大家都很熟悉了,是组件初始化和挂载的过程。组件的初始化和挂载与在前文中所介绍Vue初始化和挂载过程相同,因此不再展开说明。大致的过程就是创建了一个组件实例并挂载后。使用initComponent
将组件实例的$el
设置为vnode.elm
的值。最后,调用insert
将组件实例的DOM根节点插入其父节点。然后就完成了组件的处理。
总结
通过对组件底层实现的分析,我们可以知道,每个组件都是一个VueComponent
实例,而VueComponent
又是继承自Vue
。每个组件实例独立维护自己的状态、模板的解析、DOM的创建和更新。篇幅有限,文中只分析了基本的组件的注册解析过程,未对异步组件
、keep-alive
等做分析。等后面再慢慢补上。