import config from '../config' import { warn } from './debug' import { set } from '../observer/index' import { unicodeRegExp } from './lang' import { nativeWatch, hasSymbol } from './env' import { isArray, isFunction } from 'shared/util' import { ASSET_TYPES, LIFECYCLE_HOOKS } from 'shared/constants' import { extend, hasOwn, camelize, toRawType, capitalize, isBuiltInTag, isPlainObject } from 'shared/util' import type { Component } from 'types/component' import type { ComponentOptions } from 'types/options' /** * Option overwriting strategies are functions that handle * how to merge a parent option value and a child option * value into the final value. */ const strats = config.optionMergeStrategies /** * Options with restrictions */ if (__DEV__) { strats.el = strats.propsData = function ( parent: any, child: any, vm: any, key: any ) { if (!vm) { warn( `option "${key}" can only be used during instance ` + 'creation with the `new` keyword.' ) } return defaultStrat(parent, child) } } /** * Helper that recursively merges two data objects together. */ function mergeData( to: Record, from: Record | null, recursive = true ): Record { if (!from) return to let key, toVal, fromVal const keys = hasSymbol ? (Reflect.ownKeys(from) as string[]) : Object.keys(from) for (let i = 0; i < keys.length; i++) { key = keys[i] // in case the object is already observed... if (key === '__ob__') continue toVal = to[key] fromVal = from[key] if (!recursive || !hasOwn(to, key)) { set(to, key, fromVal) } else if ( toVal !== fromVal && isPlainObject(toVal) && isPlainObject(fromVal) ) { mergeData(toVal, fromVal) } } return to } /** * Data */ export function mergeDataOrFn( parentVal: any, childVal: any, vm?: Component ): Function | null { if (!vm) { // in a Vue.extend merge, both should be functions if (!childVal) { return parentVal } if (!parentVal) { return childVal } // when parentVal & childVal are both present, // we need to return a function that returns the // merged result of both functions... no need to // check if parentVal is a function here because // it has to be a function to pass previous merges. return function mergedDataFn() { return mergeData( isFunction(childVal) ? childVal.call(this, this) : childVal, isFunction(parentVal) ? parentVal.call(this, this) : parentVal ) } } else { return function mergedInstanceDataFn() { // instance merge const instanceData = isFunction(childVal) ? childVal.call(vm, vm) : childVal const defaultData = isFunction(parentVal) ? parentVal.call(vm, vm) : parentVal if (instanceData) { return mergeData(instanceData, defaultData) } else { return defaultData } } } } strats.data = function ( parentVal: any, childVal: any, vm?: Component ): Function | null { if (!vm) { if (childVal && typeof childVal !== 'function') { __DEV__ && warn( 'The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm ) return parentVal } return mergeDataOrFn(parentVal, childVal) } return mergeDataOrFn(parentVal, childVal, vm) } /** * Hooks and props are merged as arrays. */ export function mergeLifecycleHook( parentVal: Array | null, childVal: Function | Array | null ): Array | null { const res = childVal ? parentVal ? parentVal.concat(childVal) : isArray(childVal) ? childVal : [childVal] : parentVal return res ? dedupeHooks(res) : res } function dedupeHooks(hooks: any) { const res: Array = [] for (let i = 0; i < hooks.length; i++) { if (res.indexOf(hooks[i]) === -1) { res.push(hooks[i]) } } return res } LIFECYCLE_HOOKS.forEach(hook => { strats[hook] = mergeLifecycleHook }) /** * Assets * * When a vm is present (instance creation), we need to do * a three-way merge between constructor options, instance * options and parent options. */ function mergeAssets( parentVal: Object | null, childVal: Object | null, vm: Component | null, key: string ): Object { const res = Object.create(parentVal || null) if (childVal) { __DEV__ && assertObjectType(key, childVal, vm) return extend(res, childVal) } else { return res } } ASSET_TYPES.forEach(function (type) { strats[type + 's'] = mergeAssets }) /** * Watchers. * * Watchers hashes should not overwrite one * another, so we merge them as arrays. */ strats.watch = function ( parentVal: Record | null, childVal: Record | null, vm: Component | null, key: string ): Object | null { // work around Firefox's Object.prototype.watch... //@ts-expect-error work around if (parentVal === nativeWatch) parentVal = undefined //@ts-expect-error work around if (childVal === nativeWatch) childVal = undefined /* istanbul ignore if */ if (!childVal) return Object.create(parentVal || null) if (__DEV__) { assertObjectType(key, childVal, vm) } if (!parentVal) return childVal const ret: Record = {} extend(ret, parentVal) for (const key in childVal) { let parent = ret[key] const child = childVal[key] if (parent && !isArray(parent)) { parent = [parent] } ret[key] = parent ? parent.concat(child) : isArray(child) ? child : [child] } return ret } /** * Other object hashes. */ strats.props = strats.methods = strats.inject = strats.computed = function ( parentVal: Object | null, childVal: Object | null, vm: Component | null, key: string ): Object | null { if (childVal && __DEV__) { assertObjectType(key, childVal, vm) } if (!parentVal) return childVal const ret = Object.create(null) extend(ret, parentVal) if (childVal) extend(ret, childVal) return ret } strats.provide = function (parentVal: Object | null, childVal: Object | null) { if (!parentVal) return childVal return function () { const ret = Object.create(null) mergeData(ret, isFunction(parentVal) ? parentVal.call(this) : parentVal) if (childVal) { mergeData( ret, isFunction(childVal) ? childVal.call(this) : childVal, false // non-recursive ) } return ret } } /** * Default strategy. */ const defaultStrat = function (parentVal: any, childVal: any): any { return childVal === undefined ? parentVal : childVal } /** * Validate component names */ function checkComponents(options: Record) { for (const key in options.components) { validateComponentName(key) } } export function validateComponentName(name: string) { if ( !new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$`).test(name) ) { warn( 'Invalid component name: "' + name + '". Component names ' + 'should conform to valid custom element name in html5 specification.' ) } if (isBuiltInTag(name) || config.isReservedTag(name)) { warn( 'Do not use built-in or reserved HTML elements as component ' + 'id: ' + name ) } } /** * Ensure all props option syntax are normalized into the * Object-based format. */ function normalizeProps(options: Record, vm?: Component | null) { const props = options.props if (!props) return const res: Record = {} let i, val, name if (isArray(props)) { i = props.length while (i--) { val = props[i] if (typeof val === 'string') { name = camelize(val) res[name] = { type: null } } else if (__DEV__) { warn('props must be strings when using array syntax.') } } } else if (isPlainObject(props)) { for (const key in props) { val = props[key] name = camelize(key) res[name] = isPlainObject(val) ? val : { type: val } } } else if (__DEV__) { warn( `Invalid value for option "props": expected an Array or an Object, ` + `but got ${toRawType(props)}.`, vm ) } options.props = res } /** * Normalize all injections into Object-based format */ function normalizeInject(options: Record, vm?: Component | null) { const inject = options.inject if (!inject) return const normalized: Record = (options.inject = {}) if (isArray(inject)) { for (let i = 0; i < inject.length; i++) { normalized[inject[i]] = { from: inject[i] } } } else if (isPlainObject(inject)) { for (const key in inject) { const val = inject[key] normalized[key] = isPlainObject(val) ? extend({ from: key }, val) : { from: val } } } else if (__DEV__) { warn( `Invalid value for option "inject": expected an Array or an Object, ` + `but got ${toRawType(inject)}.`, vm ) } } /** * Normalize raw function directives into object format. */ function normalizeDirectives(options: Record) { const dirs = options.directives if (dirs) { for (const key in dirs) { const def = dirs[key] if (isFunction(def)) { dirs[key] = { bind: def, update: def } } } } } function assertObjectType(name: string, value: any, vm: Component | null) { if (!isPlainObject(value)) { warn( `Invalid value for option "${name}": expected an Object, ` + `but got ${toRawType(value)}.`, vm ) } } /** * Merge two option objects into a new one. * Core utility used in both instantiation and inheritance. */ export function mergeOptions( parent: Record, child: Record, vm?: Component | null ): ComponentOptions { if (__DEV__) { checkComponents(child) } if (isFunction(child)) { // @ts-expect-error child = child.options } normalizeProps(child, vm) normalizeInject(child, vm) normalizeDirectives(child) // Apply extends and mixins on the child options, // but only if it is a raw options object that isn't // the result of another mergeOptions call. // Only merged options has the _base property. if (!child._base) { if (child.extends) { parent = mergeOptions(parent, child.extends, vm) } if (child.mixins) { for (let i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm) } } } const options: ComponentOptions = {} as any let key for (key in parent) { mergeField(key) } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key) } } function mergeField(key: any) { const strat = strats[key] || defaultStrat options[key] = strat(parent[key], child[key], vm, key) } return options } /** * Resolve an asset. * This function is used because child instances need access * to assets defined in its ancestor chain. */ export function resolveAsset( options: Record, type: string, id: string, warnMissing?: boolean ): any { /* istanbul ignore if */ if (typeof id !== 'string') { return } const assets = options[type] // check local registration variations first if (hasOwn(assets, id)) return assets[id] const camelizedId = camelize(id) if (hasOwn(assets, camelizedId)) return assets[camelizedId] const PascalCaseId = capitalize(camelizedId) if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId] // fallback to prototype chain const res = assets[id] || assets[camelizedId] || assets[PascalCaseId] if (__DEV__ && warnMissing && !res) { warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id) } return res }