fix(runtime-core): respect props from mixins and extends

fix #1236, close #1250
This commit is contained in:
Evan You 2020-06-09 11:27:40 -04:00
parent dc986addd9
commit 2417a0cb30
5 changed files with 95 additions and 40 deletions

View File

@ -15,7 +15,11 @@ import {
exposePropsOnRenderContext, exposePropsOnRenderContext,
exposeSetupStateOnRenderContext exposeSetupStateOnRenderContext
} from './componentProxy' } from './componentProxy'
import { ComponentPropsOptions, initProps } from './componentProps' import {
ComponentPropsOptions,
NormalizedPropsOptions,
initProps
} from './componentProps'
import { Slots, initSlots, InternalSlots } from './componentSlots' import { Slots, initSlots, InternalSlots } from './componentSlots'
import { warn } from './warning' import { warn } from './warning'
import { ErrorCodes, callWithErrorHandling } from './errorHandling' import { ErrorCodes, callWithErrorHandling } from './errorHandling'
@ -50,7 +54,11 @@ export type Data = { [key: string]: unknown }
// Note: can't mark this whole interface internal because some public interfaces // Note: can't mark this whole interface internal because some public interfaces
// extend it. // extend it.
export interface SFCInternalOptions { export interface ComponentInternalOptions {
/**
* @internal
*/
__props?: NormalizedPropsOptions | []
/** /**
* @internal * @internal
*/ */
@ -76,7 +84,7 @@ export interface SFCInternalOptions {
export interface FunctionalComponent< export interface FunctionalComponent<
P = {}, P = {},
E extends EmitsOptions = Record<string, any> E extends EmitsOptions = Record<string, any>
> extends SFCInternalOptions { > extends ComponentInternalOptions {
// use of any here is intentional so it can be a valid JSX Element constructor // use of any here is intentional so it can be a valid JSX Element constructor
(props: P, ctx: SetupContext<E>): any (props: P, ctx: SetupContext<E>): any
props?: ComponentPropsOptions<P> props?: ComponentPropsOptions<P>

View File

@ -44,7 +44,7 @@ export function emit(
const options = normalizeEmitsOptions(instance.type.emits) const options = normalizeEmitsOptions(instance.type.emits)
if (options) { if (options) {
if (!(event in options)) { if (!(event in options)) {
const propsOptions = normalizePropsOptions(instance.type.props)[0] const propsOptions = normalizePropsOptions(instance.type)[0]
if (!propsOptions || !(`on` + capitalize(event) in propsOptions)) { if (!propsOptions || !(`on` + capitalize(event) in propsOptions)) {
warn( warn(
`Component emitted event "${event}" but it is neither declared in ` + `Component emitted event "${event}" but it is neither declared in ` +

View File

@ -2,7 +2,7 @@ import {
ComponentInternalInstance, ComponentInternalInstance,
Data, Data,
SetupContext, SetupContext,
SFCInternalOptions, ComponentInternalOptions,
PublicAPIComponent, PublicAPIComponent,
Component Component
} from './component' } from './component'
@ -87,7 +87,7 @@ export interface ComponentOptionsBase<
EE extends string = string EE extends string = string
> >
extends LegacyOptions<Props, D, C, M, Mixin, Extends>, extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
SFCInternalOptions, ComponentInternalOptions,
ComponentCustomOptions { ComponentCustomOptions {
setup?: ( setup?: (
this: void, this: void,
@ -367,7 +367,6 @@ export function applyOptions(
mixins, mixins,
extends: extendsOptions, extends: extendsOptions,
// state // state
props: propsOptions,
data: dataOptions, data: dataOptions,
computed: computedOptions, computed: computedOptions,
methods, methods,
@ -413,9 +412,12 @@ export function applyOptions(
const checkDuplicateProperties = __DEV__ ? createDuplicateChecker() : null const checkDuplicateProperties = __DEV__ ? createDuplicateChecker() : null
if (__DEV__ && propsOptions) { if (__DEV__) {
for (const key in normalizePropsOptions(propsOptions)[0]) { const propsOptions = normalizePropsOptions(options)[0]
checkDuplicateProperties!(OptionTypes.PROPS, key) if (propsOptions) {
for (const key in propsOptions) {
checkDuplicateProperties!(OptionTypes.PROPS, key)
}
} }
} }

View File

@ -17,7 +17,12 @@ import {
def def
} from '@vue/shared' } from '@vue/shared'
import { warn } from './warning' import { warn } from './warning'
import { Data, ComponentInternalInstance } from './component' import {
Data,
ComponentInternalInstance,
ComponentOptions,
Component
} from './component'
import { isEmitListener } from './componentEmits' import { isEmitListener } from './componentEmits'
import { InternalObjectKey } from './vnode' import { InternalObjectKey } from './vnode'
@ -96,7 +101,7 @@ type NormalizedProp =
// normalized value is a tuple of the actual normalized options // normalized value is a tuple of the actual normalized options
// and an array of prop keys that need value casting (booleans and defaults) // and an array of prop keys that need value casting (booleans and defaults)
type NormalizedPropsOptions = [Record<string, NormalizedProp>, string[]] export type NormalizedPropsOptions = [Record<string, NormalizedProp>, string[]]
export function initProps( export function initProps(
instance: ComponentInternalInstance, instance: ComponentInternalInstance,
@ -108,17 +113,16 @@ export function initProps(
const attrs: Data = {} const attrs: Data = {}
def(attrs, InternalObjectKey, 1) def(attrs, InternalObjectKey, 1)
setFullProps(instance, rawProps, props, attrs) setFullProps(instance, rawProps, props, attrs)
const options = instance.type.props
// validation // validation
if (__DEV__ && options && rawProps) { if (__DEV__) {
validateProps(props, options) validateProps(props, instance.type)
} }
if (isStateful) { if (isStateful) {
// stateful // stateful
instance.props = isSSR ? props : shallowReactive(props) instance.props = isSSR ? props : shallowReactive(props)
} else { } else {
if (!options) { if (!instance.type.props) {
// functional w/ optional props, props === attrs // functional w/ optional props, props === attrs
instance.props = attrs instance.props = attrs
} else { } else {
@ -140,9 +144,8 @@ export function updateProps(
attrs, attrs,
vnode: { patchFlag } vnode: { patchFlag }
} = instance } = instance
const rawOptions = instance.type.props
const rawCurrentProps = toRaw(props) const rawCurrentProps = toRaw(props)
const [options] = normalizePropsOptions(rawOptions) const [options] = normalizePropsOptions(instance.type)
if ((optimized || patchFlag > 0) && !(patchFlag & PatchFlags.FULL_PROPS)) { if ((optimized || patchFlag > 0) && !(patchFlag & PatchFlags.FULL_PROPS)) {
if (patchFlag & PatchFlags.PROPS) { if (patchFlag & PatchFlags.PROPS) {
@ -211,8 +214,8 @@ export function updateProps(
} }
} }
if (__DEV__ && rawOptions && rawProps) { if (__DEV__ && rawProps) {
validateProps(props, rawOptions) validateProps(props, instance.type)
} }
} }
@ -222,9 +225,7 @@ function setFullProps(
props: Data, props: Data,
attrs: Data attrs: Data
) { ) {
const [options, needCastKeys] = normalizePropsOptions( const [options, needCastKeys] = normalizePropsOptions(instance.type)
instance.type.props
)
const emits = instance.type.emits const emits = instance.type.emits
if (rawProps) { if (rawProps) {
@ -292,16 +293,38 @@ function resolvePropValue(
} }
export function normalizePropsOptions( export function normalizePropsOptions(
raw: ComponentPropsOptions | undefined comp: Component
): NormalizedPropsOptions | [] { ): NormalizedPropsOptions | [] {
if (!raw) { if (comp.__props) {
return EMPTY_ARR as any return comp.__props
}
if ((raw as any)._n) {
return (raw as any)._n
} }
const raw = comp.props
const normalized: NormalizedPropsOptions[0] = {} const normalized: NormalizedPropsOptions[0] = {}
const needCastKeys: NormalizedPropsOptions[1] = [] const needCastKeys: NormalizedPropsOptions[1] = []
// apply mixin/extends props
let hasExtends = false
if (__FEATURE_OPTIONS__ && !isFunction(comp)) {
const extendProps = (raw: ComponentOptions) => {
const [props, keys] = normalizePropsOptions(raw)
Object.assign(normalized, props)
if (keys) needCastKeys.push(...keys)
}
if (comp.extends) {
hasExtends = true
extendProps(comp.extends)
}
if (comp.mixins) {
hasExtends = true
comp.mixins.forEach(extendProps)
}
}
if (!raw && !hasExtends) {
return (comp.__props = EMPTY_ARR)
}
if (isArray(raw)) { if (isArray(raw)) {
for (let i = 0; i < raw.length; i++) { for (let i = 0; i < raw.length; i++) {
if (__DEV__ && !isString(raw[i])) { if (__DEV__ && !isString(raw[i])) {
@ -312,7 +335,7 @@ export function normalizePropsOptions(
normalized[normalizedKey] = EMPTY_OBJ normalized[normalizedKey] = EMPTY_OBJ
} }
} }
} else { } else if (raw) {
if (__DEV__ && !isObject(raw)) { if (__DEV__ && !isObject(raw)) {
warn(`invalid props options`, raw) warn(`invalid props options`, raw)
} }
@ -337,7 +360,7 @@ export function normalizePropsOptions(
} }
} }
const normalizedEntry: NormalizedPropsOptions = [normalized, needCastKeys] const normalizedEntry: NormalizedPropsOptions = [normalized, needCastKeys]
def(raw, '_n', normalizedEntry) comp.__props = normalizedEntry
return normalizedEntry return normalizedEntry
} }
@ -368,9 +391,12 @@ function getTypeIndex(
return -1 return -1
} }
function validateProps(props: Data, rawOptions: ComponentPropsOptions) { /**
* dev only
*/
function validateProps(props: Data, comp: Component) {
const rawValues = toRaw(props) const rawValues = toRaw(props)
const options = normalizePropsOptions(rawOptions)[0] const options = normalizePropsOptions(comp)[0]
for (const key in options) { for (const key in options) {
let opt = options[key] let opt = options[key]
if (opt == null) continue if (opt == null) continue
@ -378,6 +404,9 @@ function validateProps(props: Data, rawOptions: ComponentPropsOptions) {
} }
} }
/**
* dev only
*/
function validatePropName(key: string) { function validatePropName(key: string) {
if (key[0] !== '$') { if (key[0] !== '$') {
return true return true
@ -387,6 +416,9 @@ function validatePropName(key: string) {
return false return false
} }
/**
* dev only
*/
function validateProp( function validateProp(
name: string, name: string,
value: unknown, value: unknown,
@ -434,6 +466,9 @@ type AssertionResult = {
expectedType: string expectedType: string
} }
/**
* dev only
*/
function assertType(value: unknown, type: PropConstructor): AssertionResult { function assertType(value: unknown, type: PropConstructor): AssertionResult {
let valid let valid
const expectedType = getType(type) const expectedType = getType(type)
@ -457,6 +492,9 @@ function assertType(value: unknown, type: PropConstructor): AssertionResult {
} }
} }
/**
* dev only
*/
function getInvalidTypeMessage( function getInvalidTypeMessage(
name: string, name: string,
value: unknown, value: unknown,
@ -485,6 +523,9 @@ function getInvalidTypeMessage(
return message return message
} }
/**
* dev only
*/
function styleValue(value: unknown, type: string): string { function styleValue(value: unknown, type: string): string {
if (type === 'String') { if (type === 'String') {
return `"${value}"` return `"${value}"`
@ -495,11 +536,17 @@ function styleValue(value: unknown, type: string): string {
} }
} }
/**
* dev only
*/
function isExplicable(type: string): boolean { function isExplicable(type: string): boolean {
const explicitTypes = ['string', 'number', 'boolean'] const explicitTypes = ['string', 'number', 'boolean']
return explicitTypes.some(elem => type.toLowerCase() === elem) return explicitTypes.some(elem => type.toLowerCase() === elem)
} }
/**
* dev only
*/
function isBoolean(...args: string[]): boolean { function isBoolean(...args: string[]): boolean {
return args.some(elem => elem.toLowerCase() === 'boolean') return args.some(elem => elem.toLowerCase() === 'boolean')
} }

View File

@ -238,7 +238,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
// only cache other properties when instance has declared (thus stable) // only cache other properties when instance has declared (thus stable)
// props // props
type.props && type.props &&
hasOwn(normalizePropsOptions(type.props)[0]!, key) hasOwn(normalizePropsOptions(type)[0]!, key)
) { ) {
accessCache![key] = AccessTypes.PROPS accessCache![key] = AccessTypes.PROPS
return props![key] return props![key]
@ -347,7 +347,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
accessCache![key] !== undefined || accessCache![key] !== undefined ||
(data !== EMPTY_OBJ && hasOwn(data, key)) || (data !== EMPTY_OBJ && hasOwn(data, key)) ||
(setupState !== EMPTY_OBJ && hasOwn(setupState, key)) || (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) ||
(type.props && hasOwn(normalizePropsOptions(type.props)[0]!, key)) || (type.props && hasOwn(normalizePropsOptions(type)[0]!, key)) ||
hasOwn(ctx, key) || hasOwn(ctx, key) ||
hasOwn(publicPropertiesMap, key) || hasOwn(publicPropertiesMap, key) ||
hasOwn(appContext.config.globalProperties, key) hasOwn(appContext.config.globalProperties, key)
@ -430,12 +430,10 @@ export function createRenderContext(instance: ComponentInternalInstance) {
export function exposePropsOnRenderContext( export function exposePropsOnRenderContext(
instance: ComponentInternalInstance instance: ComponentInternalInstance
) { ) {
const { const { ctx, type } = instance
ctx, const propsOptions = normalizePropsOptions(type)[0]
type: { props: propsOptions }
} = instance
if (propsOptions) { if (propsOptions) {
Object.keys(normalizePropsOptions(propsOptions)[0]!).forEach(key => { Object.keys(propsOptions).forEach(key => {
Object.defineProperty(ctx, key, { Object.defineProperty(ctx, key, {
enumerable: true, enumerable: true,
configurable: true, configurable: true,