vue
Version:
Reactive, component-oriented view layer for modern web interfaces.
323 lines (292 loc) • 8.73 kB
JavaScript
/* @flow */
import VNode from './vnode'
import { resolveConstructorOptions } from '../instance/init'
import { activeInstance, callHook } from '../instance/lifecycle'
import { resolveSlots } from '../instance/render'
import { createElement } from './create-element'
import { warn, isObject, hasOwn, hyphenate, validateProp } from '../util/index'
const hooks = { init, prepatch, insert, destroy }
const hooksToMerge = Object.keys(hooks)
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data?: VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | void {
if (!Ctor) {
return
}
const baseCtor = context.$options._base
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// async component
if (!Ctor.cid) {
if (Ctor.resolved) {
Ctor = Ctor.resolved
} else {
Ctor = resolveAsyncComponent(Ctor, baseCtor, () => {
// it's ok to queue this on every render because
// $forceUpdate is buffered by the scheduler.
context.$forceUpdate()
})
if (!Ctor) {
// return nothing if this is indeed an async component
// wait for the callback to trigger parent update.
return
}
}
}
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)
data = data || {}
// extract props
const propsData = extractProps(data, Ctor)
// functional component
if (Ctor.options.functional) {
return createFunctionalComponent(Ctor, 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
data.on = data.nativeOn
if (Ctor.options.abstract) {
// abstract components do not keep anything
// other than props & listeners
data = {}
}
// merge component management hooks onto the placeholder node
mergeHooks(data)
// return a placeholder vnode
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 }
)
return vnode
}
function createFunctionalComponent (
Ctor: Class<Component>,
propsData: ?Object,
data: VNodeData,
context: Component,
children: ?Array<VNode>
): VNode | void {
const props = {}
const propOptions = Ctor.options.props
if (propOptions) {
for (const key in propOptions) {
props[key] = validateProp(key, propOptions, propsData)
}
}
// ensure the createElement function in functional components
// gets a unique context - this is necessary for correct named slot check
const _context = Object.create(context)
const h = (a, b, c, d) => createElement(_context, a, b, c, d, true)
const vnode = Ctor.options.render.call(null, h, {
props,
data,
parent: context,
children,
slots: () => resolveSlots(children, context)
})
if (vnode instanceof VNode) {
vnode.functionalContext = context
if (data.slot) {
(vnode.data || (vnode.data = {})).slot = data.slot
}
}
return vnode
}
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)
}
function 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
prepatch(mountedNode, mountedNode)
}
}
function prepatch (
oldVnode: MountedComponentVNode,
vnode: MountedComponentVNode
) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
child._updateFromParent(
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
}
function insert (vnode: MountedComponentVNode) {
if (!vnode.componentInstance._isMounted) {
vnode.componentInstance._isMounted = true
callHook(vnode.componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
vnode.componentInstance._inactive = false
callHook(vnode.componentInstance, 'activated')
}
}
function destroy (vnode: MountedComponentVNode) {
if (!vnode.componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
vnode.componentInstance.$destroy()
} else {
vnode.componentInstance._inactive = true
callHook(vnode.componentInstance, 'deactivated')
}
}
}
function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>,
cb: Function
): Class<Component> | void {
if (factory.requested) {
// pool callbacks
factory.pendingCallbacks.push(cb)
} else {
factory.requested = true
const cbs = factory.pendingCallbacks = [cb]
let sync = true
const resolve = (res: Object | Class<Component>) => {
if (isObject(res)) {
res = baseCtor.extend(res)
}
// cache resolved
factory.resolved = res
// invoke callbacks only if this is not a synchronous resolve
// (async resolves are shimmed as synchronous during SSR)
if (!sync) {
for (let i = 0, l = cbs.length; i < l; i++) {
cbs[i](res)
}
}
}
const reject = reason => {
process.env.NODE_ENV !== 'production' && warn(
`Failed to resolve async component: ${String(factory)}` +
(reason ? `\nReason: ${reason}` : '')
)
}
const res = factory(resolve, reject)
// handle promise
if (res && typeof res.then === 'function' && !factory.resolved) {
res.then(resolve, reject)
}
sync = false
// return in case resolved synchronously
return factory.resolved
}
}
function extractProps (data: VNodeData, Ctor: Class<Component>): ?Object {
// we are only extracting raw values here.
// validation and default values are handled in the child
// component itself.
const propOptions = Ctor.options.props
if (!propOptions) {
return
}
const res = {}
const { attrs, props, domProps } = data
if (attrs || props || domProps) {
for (const key in propOptions) {
const altKey = hyphenate(key)
checkProp(res, props, key, altKey, true) ||
checkProp(res, attrs, key, altKey) ||
checkProp(res, domProps, key, altKey)
}
}
return res
}
function checkProp (
res: Object,
hash: ?Object,
key: string,
altKey: string,
preserve?: boolean
): boolean {
if (hash) {
if (hasOwn(hash, key)) {
res[key] = hash[key]
if (!preserve) {
delete hash[key]
}
return true
} else if (hasOwn(hash, altKey)) {
res[key] = hash[altKey]
if (!preserve) {
delete hash[altKey]
}
return true
}
}
return false
}
function mergeHooks (data: VNodeData) {
if (!data.hook) {
data.hook = {}
}
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const fromParent = data.hook[key]
const ours = hooks[key]
data.hook[key] = fromParent ? mergeHook(ours, fromParent) : ours
}
}
function mergeHook (one: Function, two: Function): Function {
return function (a, b, c, d) {
one(a, b, c, d)
two(a, b, c, d)
}
}