import VNode from './vnode' import { isArray } from 'core/util' import { resolveConstructorOptions } from 'core/instance/init' import { queueActivatedComponent } from 'core/observer/scheduler' import { createFunctionalComponent } from './create-functional-component' import { warn, isDef, isUndef, isTrue, isObject } from '../util/index' import { resolveAsyncComponent, createAsyncPlaceholder, extractPropsFromVNodeData } from './helpers/index' import { callHook, activeInstance, updateChildComponent, activateChildComponent, deactivateChildComponent } from '../instance/lifecycle' import type { MountedComponentVNode, VNodeData, VNodeWithData } from 'types/vnode' import type { Component } from 'types/component' import type { ComponentOptions, InternalComponentOptions } from 'types/options' export function getComponentName(options: ComponentOptions) { return options.name || options.__name || options._componentTag } // inline hooks to be invoked on component VNodes during patch const componentVNodeHooks = { init(vnode: VNodeWithData, hydrating: boolean): boolean | void { 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) { 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 callHook(componentInstance, 'mounted') } if (vnode.data.keepAlive) { if (context._isMounted) { // vue-router#1212 // During updates, a kept-alive component's child components may // change, so directly walking the tree here may call activated hooks // on incorrect children. Instead we push them into a queue which will // be processed after the whole patch process ended. 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) export function createComponent( Ctor: typeof Component | Function | ComponentOptions | void, data: VNodeData | undefined, context: Component, children?: Array, tag?: string ): VNode | Array | void { if (isUndef(Ctor)) { return } const baseCtor = context.$options._base // plain options object: turn it into a constructor if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor as typeof Component) } // if at this stage it's not a constructor or an async component factory, // reject. if (typeof Ctor !== 'function') { if (__DEV__) { warn(`Invalid Component definition: ${String(Ctor)}`, context) } return } // async component let asyncFactory // @ts-expect-error if (isUndef(Ctor.cid)) { asyncFactory = Ctor Ctor = resolveAsyncComponent(asyncFactory, baseCtor) if (Ctor === undefined) { // return a placeholder node for async component, which is rendered // as a comment node but preserves all the raw information for the node. // the information will be used for async server-rendering and hydration. return createAsyncPlaceholder(asyncFactory, data, context, children, tag) } } data = data || {} // resolve constructor options in case global mixins are applied after // component constructor creation resolveConstructorOptions(Ctor as typeof Component) // transform component v-model data into props & events if (isDef(data.model)) { // @ts-expect-error transformModel(Ctor.options, data) } // extract props // @ts-expect-error const propsData = extractPropsFromVNodeData(data, Ctor, tag) // functional component // @ts-expect-error if (isTrue(Ctor.options.functional)) { return createFunctionalComponent( Ctor as typeof Component, propsData, data, context, children ) } // extract listeners, since these needs to be treated as // child component listeners instead of DOM listeners const listeners = data.on // replace with listeners with .native modifier // so it gets processed during parent component patch. data.on = data.nativeOn // @ts-expect-error if (isTrue(Ctor.options.abstract)) { // abstract components do not keep anything // other than props & listeners & slot // work around flow const slot = data.slot data = {} if (slot) { data.slot = slot } } // install component management hooks onto the placeholder node installComponentHooks(data) // return a placeholder vnode // @ts-expect-error const name = getComponentName(Ctor.options) || tag const vnode = new VNode( // @ts-expect-error `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, // @ts-expect-error { Ctor, propsData, listeners, tag, children }, asyncFactory ) return vnode } 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 } // check inline-template render functions const inlineTemplate = vnode.data.inlineTemplate if (isDef(inlineTemplate)) { options.render = inlineTemplate.render options.staticRenderFns = inlineTemplate.staticRenderFns } return new vnode.componentOptions.Ctor(options) } 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] const toMerge = componentVNodeHooks[key] // @ts-expect-error if (existing !== toMerge && !(existing && existing._merged)) { hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge } } } function mergeHook(f1: any, f2: any): Function { const merged = (a, b) => { // flow complains about extra args which is why we use any f1(a, b) f2(a, b) } merged._merged = true return merged } // transform component v-model info (value and callback) into // prop and event handler respectively. function transformModel(options, data: any) { const prop = (options.model && options.model.prop) || 'value' const event = (options.model && options.model.event) || 'input' ;(data.attrs || (data.attrs = {}))[prop] = data.model.value const on = data.on || (data.on = {}) const existing = on[event] const callback = data.model.callback if (isDef(existing)) { if ( isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback ) { on[event] = [callback].concat(existing) } } else { on[event] = callback } }