wip: props resolving
This commit is contained in:
		
							parent
							
								
									5c069eeae7
								
							
						
					
					
						commit
						9dd133b1e9
					
				| @ -1,6 +1,7 @@ | |||||||
| import { VNode, normalizeVNode, VNodeChild } from './vnode' | import { VNode, normalizeVNode, VNodeChild } from './vnode' | ||||||
| import { ReactiveEffect } from '@vue/observer' | import { ReactiveEffect } from '@vue/observer' | ||||||
| import { isFunction, EMPTY_OBJ } from '@vue/shared' | import { isFunction } from '@vue/shared' | ||||||
|  | import { resolveProps, ComponentPropsOptions } from './componentProps' | ||||||
| 
 | 
 | ||||||
| interface Value<T> { | interface Value<T> { | ||||||
|   value: T |   value: T | ||||||
| @ -18,14 +19,28 @@ type ExtractPropTypes<PropOptions> = { | |||||||
|     : PropOptions[key] extends null | undefined ? any : PropOptions[key] |     : PropOptions[key] extends null | undefined ? any : PropOptions[key] | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface ComponentPublicProperties<P, S> { | export type Data = { [key: string]: any } | ||||||
|   $props: P | 
 | ||||||
|  | export interface ComponentPublicProperties<P = Data, S = Data> { | ||||||
|   $state: S |   $state: S | ||||||
|  |   $props: P | ||||||
|  |   $attrs: Data | ||||||
|  | 
 | ||||||
|  |   // TODO
 | ||||||
|  |   $refs: Data | ||||||
|  |   $slots: Data | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | interface RenderFunctionArg<B = Data, P = Data> { | ||||||
|  |   state: B | ||||||
|  |   props: P | ||||||
|  |   attrs: Data | ||||||
|  |   slots: Slots | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface ComponentOptions< | export interface ComponentOptions< | ||||||
|   RawProps = { [key: string]: Prop<any> }, |   RawProps = ComponentPropsOptions, | ||||||
|   RawBindings = { [key: string]: any } | void, |   RawBindings = Data | void, | ||||||
|   Props = ExtractPropTypes<RawProps>, |   Props = ExtractPropTypes<RawProps>, | ||||||
|   Bindings = UnwrapBindings<RawBindings> |   Bindings = UnwrapBindings<RawBindings> | ||||||
| > { | > { | ||||||
| @ -33,13 +48,22 @@ export interface ComponentOptions< | |||||||
|   setup?: (props: Props) => RawBindings |   setup?: (props: Props) => RawBindings | ||||||
|   render?: <B extends Bindings>( |   render?: <B extends Bindings>( | ||||||
|     this: ComponentPublicProperties<Props, B>, |     this: ComponentPublicProperties<Props, B>, | ||||||
|     ctx: { |     ctx: RenderFunctionArg<B, Props> | ||||||
|       state: B |  | ||||||
|       props: Props |  | ||||||
|     } |  | ||||||
|   ) => VNodeChild |   ) => VNodeChild | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export interface FunctionalComponent<P = {}> { | ||||||
|  |   (ctx: RenderFunctionArg): any | ||||||
|  |   props?: ComponentPropsOptions<P> | ||||||
|  |   displayName?: string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export type Slot = (...args: any[]) => VNode[] | ||||||
|  | 
 | ||||||
|  | export type Slots = Readonly<{ | ||||||
|  |   [name: string]: Slot | ||||||
|  | }> | ||||||
|  | 
 | ||||||
| // no-op, for type inference only
 | // no-op, for type inference only
 | ||||||
| export function createComponent< | export function createComponent< | ||||||
|   RawProps, |   RawProps, | ||||||
| @ -55,19 +79,25 @@ export function createComponent< | |||||||
|   return options as any |   return options as any | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface ComponentHandle { | export type ComponentHandle = { | ||||||
|   type: Function | ComponentOptions |   type: FunctionalComponent | ComponentOptions | ||||||
|   vnode: VNode | null |   vnode: VNode | null | ||||||
|   next: VNode | null |   next: VNode | null | ||||||
|   subTree: VNode | null |   subTree: VNode | null | ||||||
|   update: ReactiveEffect |   update: ReactiveEffect | ||||||
| } | } & ComponentPublicProperties | ||||||
| 
 | 
 | ||||||
| export function renderComponentRoot(handle: ComponentHandle): VNode { | export function renderComponentRoot(handle: ComponentHandle): VNode { | ||||||
|   const { type, vnode } = handle |   const { type, vnode } = handle | ||||||
|   // TODO actually resolve props
 |   const { 0: props, 1: attrs } = resolveProps( | ||||||
|  |     (vnode as VNode).props, | ||||||
|  |     type.props | ||||||
|  |   ) | ||||||
|   const renderArg = { |   const renderArg = { | ||||||
|     props: (vnode as VNode).props || EMPTY_OBJ |     state: handle.$state, | ||||||
|  |     slots: handle.$slots, | ||||||
|  |     props, | ||||||
|  |     attrs | ||||||
|   } |   } | ||||||
|   if (isFunction(type)) { |   if (isFunction(type)) { | ||||||
|     return normalizeVNode(type(renderArg)) |     return normalizeVNode(type(renderArg)) | ||||||
|  | |||||||
							
								
								
									
										326
									
								
								packages/runtime-core/src/componentProps.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										326
									
								
								packages/runtime-core/src/componentProps.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,326 @@ | |||||||
|  | import { immutable, unwrap } from '@vue/observer' | ||||||
|  | import { | ||||||
|  |   EMPTY_OBJ, | ||||||
|  |   camelize, | ||||||
|  |   hyphenate, | ||||||
|  |   capitalize, | ||||||
|  |   isString, | ||||||
|  |   isFunction, | ||||||
|  |   isArray, | ||||||
|  |   isObject | ||||||
|  | } from '@vue/shared' | ||||||
|  | import { warn } from './warning' | ||||||
|  | import { Data, ComponentHandle } from './component' | ||||||
|  | 
 | ||||||
|  | export type ComponentPropsOptions<P = Data> = { | ||||||
|  |   [K in keyof P]: PropValidator<P[K]> | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export type Prop<T> = { (): T } | { new (...args: any[]): T & object } | ||||||
|  | 
 | ||||||
|  | export type PropType<T> = Prop<T> | Prop<T>[] | ||||||
|  | 
 | ||||||
|  | export type PropValidator<T> = PropOptions<T> | PropType<T> | ||||||
|  | 
 | ||||||
|  | export interface PropOptions<T = any> { | ||||||
|  |   type?: PropType<T> | true | null | ||||||
|  |   required?: boolean | ||||||
|  |   default?: T | null | undefined | (() => T | null | undefined) | ||||||
|  |   validator?(value: T): boolean | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const enum BooleanFlags { | ||||||
|  |   shouldCast = '1', | ||||||
|  |   shouldCastTrue = '2' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type NormalizedProp = PropOptions & { | ||||||
|  |   [BooleanFlags.shouldCast]?: boolean | ||||||
|  |   [BooleanFlags.shouldCastTrue]?: boolean | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type NormalizedPropsOptions = Record<string, NormalizedProp> | ||||||
|  | 
 | ||||||
|  | const isReservedKey = (key: string): boolean => key[0] === '_' || key[0] === '$' | ||||||
|  | 
 | ||||||
|  | export function initializeProps( | ||||||
|  |   instance: ComponentHandle, | ||||||
|  |   options: NormalizedPropsOptions | undefined, | ||||||
|  |   data: Data | null | ||||||
|  | ) { | ||||||
|  |   const { 0: props, 1: attrs } = resolveProps(data, options) | ||||||
|  |   instance.$props = __DEV__ ? immutable(props) : props | ||||||
|  |   instance.$attrs = options | ||||||
|  |     ? __DEV__ | ||||||
|  |       ? immutable(attrs) | ||||||
|  |       : attrs | ||||||
|  |     : instance.$props | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // resolve raw VNode data.
 | ||||||
|  | // - filter out reserved keys (key, ref, slots)
 | ||||||
|  | // - extract class and style into $attrs (to be merged onto child
 | ||||||
|  | //   component root)
 | ||||||
|  | // - for the rest:
 | ||||||
|  | //   - if has declared props: put declared ones in `props`, the rest in `attrs`
 | ||||||
|  | //   - else: everything goes in `props`.
 | ||||||
|  | 
 | ||||||
|  | const EMPTY_PROPS = [EMPTY_OBJ, EMPTY_OBJ] as [Data, Data] | ||||||
|  | 
 | ||||||
|  | export function resolveProps( | ||||||
|  |   rawData: any, | ||||||
|  |   _options: ComponentPropsOptions | void | ||||||
|  | ): [Data, Data] { | ||||||
|  |   const hasDeclaredProps = _options != null | ||||||
|  |   const options = normalizePropsOptions(_options) as NormalizedPropsOptions | ||||||
|  |   if (!rawData && !hasDeclaredProps) { | ||||||
|  |     return EMPTY_PROPS | ||||||
|  |   } | ||||||
|  |   const props: any = {} | ||||||
|  |   let attrs: any = void 0 | ||||||
|  |   if (rawData != null) { | ||||||
|  |     for (const key in rawData) { | ||||||
|  |       // key, ref, slots are reserved
 | ||||||
|  |       if (key === 'key' || key === 'ref' || key === 'slots') { | ||||||
|  |         continue | ||||||
|  |       } | ||||||
|  |       // any non-declared data are put into a separate `attrs` object
 | ||||||
|  |       // for spreading
 | ||||||
|  |       if (hasDeclaredProps && !options.hasOwnProperty(key)) { | ||||||
|  |         ;(attrs || (attrs = {}))[key] = rawData[key] | ||||||
|  |       } else { | ||||||
|  |         props[key] = rawData[key] | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   // set default values, cast booleans & run validators
 | ||||||
|  |   if (hasDeclaredProps) { | ||||||
|  |     for (const key in options) { | ||||||
|  |       let opt = options[key] | ||||||
|  |       if (opt == null) continue | ||||||
|  |       const isAbsent = !props.hasOwnProperty(key) | ||||||
|  |       const hasDefault = opt.hasOwnProperty('default') | ||||||
|  |       const currentValue = props[key] | ||||||
|  |       // default values
 | ||||||
|  |       if (hasDefault && currentValue === undefined) { | ||||||
|  |         const defaultValue = opt.default | ||||||
|  |         props[key] = isFunction(defaultValue) ? defaultValue() : defaultValue | ||||||
|  |       } | ||||||
|  |       // boolean casting
 | ||||||
|  |       if (opt[BooleanFlags.shouldCast]) { | ||||||
|  |         if (isAbsent && !hasDefault) { | ||||||
|  |           props[key] = false | ||||||
|  |         } else if ( | ||||||
|  |           opt[BooleanFlags.shouldCastTrue] && | ||||||
|  |           (currentValue === '' || currentValue === hyphenate(key)) | ||||||
|  |         ) { | ||||||
|  |           props[key] = true | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       // runtime validation
 | ||||||
|  |       if (__DEV__ && rawData) { | ||||||
|  |         validateProp(key, unwrap(rawData[key]), opt, isAbsent) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } else { | ||||||
|  |     // if component has no declared props, $attrs === $props
 | ||||||
|  |     attrs = props | ||||||
|  |   } | ||||||
|  |   return [props, attrs] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const normalizationMap = new WeakMap() | ||||||
|  | 
 | ||||||
|  | function normalizePropsOptions( | ||||||
|  |   raw: ComponentPropsOptions | void | ||||||
|  | ): NormalizedPropsOptions | void { | ||||||
|  |   if (!raw) { | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |   if (normalizationMap.has(raw)) { | ||||||
|  |     return normalizationMap.get(raw) | ||||||
|  |   } | ||||||
|  |   const normalized: NormalizedPropsOptions = {} | ||||||
|  |   normalizationMap.set(raw, normalized) | ||||||
|  |   if (isArray(raw)) { | ||||||
|  |     for (let i = 0; i < raw.length; i++) { | ||||||
|  |       if (__DEV__ && !isString(raw[i])) { | ||||||
|  |         warn(`props must be strings when using array syntax.`, raw[i]) | ||||||
|  |       } | ||||||
|  |       const normalizedKey = camelize(raw[i]) | ||||||
|  |       if (!isReservedKey(normalizedKey)) { | ||||||
|  |         normalized[normalizedKey] = EMPTY_OBJ | ||||||
|  |       } else if (__DEV__) { | ||||||
|  |         warn(`Invalid prop name: "${normalizedKey}" is a reserved property.`) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } else { | ||||||
|  |     if (__DEV__ && !isObject(raw)) { | ||||||
|  |       warn(`invalid props options`, raw) | ||||||
|  |     } | ||||||
|  |     for (const key in raw) { | ||||||
|  |       const normalizedKey = camelize(key) | ||||||
|  |       if (!isReservedKey(normalizedKey)) { | ||||||
|  |         const opt = raw[key] | ||||||
|  |         const prop = (normalized[normalizedKey] = | ||||||
|  |           isArray(opt) || isFunction(opt) ? { type: opt } : opt) | ||||||
|  |         if (prop) { | ||||||
|  |           const booleanIndex = getTypeIndex(Boolean, prop.type) | ||||||
|  |           const stringIndex = getTypeIndex(String, prop.type) | ||||||
|  |           ;(prop as NormalizedProp)[BooleanFlags.shouldCast] = booleanIndex > -1 | ||||||
|  |           ;(prop as NormalizedProp)[BooleanFlags.shouldCastTrue] = | ||||||
|  |             booleanIndex < stringIndex | ||||||
|  |         } | ||||||
|  |       } else if (__DEV__) { | ||||||
|  |         warn(`Invalid prop name: "${normalizedKey}" is a reserved property.`) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return normalized | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // use function string name to check type constructors
 | ||||||
|  | // so that it works across vms / iframes.
 | ||||||
|  | function getType(ctor: Prop<any>): string { | ||||||
|  |   const match = ctor && ctor.toString().match(/^\s*function (\w+)/) | ||||||
|  |   return match ? match[1] : '' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function isSameType(a: Prop<any>, b: Prop<any>): boolean { | ||||||
|  |   return getType(a) === getType(b) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getTypeIndex( | ||||||
|  |   type: Prop<any>, | ||||||
|  |   expectedTypes: PropType<any> | void | null | true | ||||||
|  | ): number { | ||||||
|  |   if (isArray(expectedTypes)) { | ||||||
|  |     for (let i = 0, len = expectedTypes.length; i < len; i++) { | ||||||
|  |       if (isSameType(expectedTypes[i], type)) { | ||||||
|  |         return i | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } else if (isObject(expectedTypes)) { | ||||||
|  |     return isSameType(expectedTypes, type) ? 0 : -1 | ||||||
|  |   } | ||||||
|  |   return -1 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type AssertionResult = { | ||||||
|  |   valid: boolean | ||||||
|  |   expectedType: string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function validateProp( | ||||||
|  |   name: string, | ||||||
|  |   value: any, | ||||||
|  |   prop: PropOptions<any>, | ||||||
|  |   isAbsent: boolean | ||||||
|  | ) { | ||||||
|  |   const { type, required, validator } = prop | ||||||
|  |   // required!
 | ||||||
|  |   if (required && isAbsent) { | ||||||
|  |     warn('Missing required prop: "' + name + '"') | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |   // missing but optional
 | ||||||
|  |   if (value == null && !prop.required) { | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |   // type check
 | ||||||
|  |   if (type != null && type !== true) { | ||||||
|  |     let isValid = false | ||||||
|  |     const types = isArray(type) ? type : [type] | ||||||
|  |     const expectedTypes = [] | ||||||
|  |     // value is valid as long as one of the specified types match
 | ||||||
|  |     for (let i = 0; i < types.length && !isValid; i++) { | ||||||
|  |       const { valid, expectedType } = assertType(value, types[i]) | ||||||
|  |       expectedTypes.push(expectedType || '') | ||||||
|  |       isValid = valid | ||||||
|  |     } | ||||||
|  |     if (!isValid) { | ||||||
|  |       warn(getInvalidTypeMessage(name, value, expectedTypes)) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   // custom validator
 | ||||||
|  |   if (validator && !validator(value)) { | ||||||
|  |     warn('Invalid prop: custom validator check failed for prop "' + name + '".') | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/ | ||||||
|  | 
 | ||||||
|  | function assertType(value: any, type: Prop<any>): AssertionResult { | ||||||
|  |   let valid | ||||||
|  |   const expectedType = getType(type) | ||||||
|  |   if (simpleCheckRE.test(expectedType)) { | ||||||
|  |     const t = typeof value | ||||||
|  |     valid = t === expectedType.toLowerCase() | ||||||
|  |     // for primitive wrapper objects
 | ||||||
|  |     if (!valid && t === 'object') { | ||||||
|  |       valid = value instanceof type | ||||||
|  |     } | ||||||
|  |   } else if (expectedType === 'Object') { | ||||||
|  |     valid = toRawType(value) === 'Object' | ||||||
|  |   } else if (expectedType === 'Array') { | ||||||
|  |     valid = isArray(value) | ||||||
|  |   } else { | ||||||
|  |     valid = value instanceof type | ||||||
|  |   } | ||||||
|  |   return { | ||||||
|  |     valid, | ||||||
|  |     expectedType | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getInvalidTypeMessage( | ||||||
|  |   name: string, | ||||||
|  |   value: any, | ||||||
|  |   expectedTypes: string[] | ||||||
|  | ): string { | ||||||
|  |   let message = | ||||||
|  |     `Invalid prop: type check failed for prop "${name}".` + | ||||||
|  |     ` Expected ${expectedTypes.map(capitalize).join(', ')}` | ||||||
|  |   const expectedType = expectedTypes[0] | ||||||
|  |   const receivedType = toRawType(value) | ||||||
|  |   const expectedValue = styleValue(value, expectedType) | ||||||
|  |   const receivedValue = styleValue(value, receivedType) | ||||||
|  |   // check if we need to specify expected value
 | ||||||
|  |   if ( | ||||||
|  |     expectedTypes.length === 1 && | ||||||
|  |     isExplicable(expectedType) && | ||||||
|  |     !isBoolean(expectedType, receivedType) | ||||||
|  |   ) { | ||||||
|  |     message += ` with value ${expectedValue}` | ||||||
|  |   } | ||||||
|  |   message += `, got ${receivedType} ` | ||||||
|  |   // check if we need to specify received value
 | ||||||
|  |   if (isExplicable(receivedType)) { | ||||||
|  |     message += `with value ${receivedValue}.` | ||||||
|  |   } | ||||||
|  |   return message | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function styleValue(value: any, type: string): string { | ||||||
|  |   if (type === 'String') { | ||||||
|  |     return `"${value}"` | ||||||
|  |   } else if (type === 'Number') { | ||||||
|  |     return `${Number(value)}` | ||||||
|  |   } else { | ||||||
|  |     return `${value}` | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function toRawType(value: any): string { | ||||||
|  |   return Object.prototype.toString.call(value).slice(8, -1) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function isExplicable(type: string): boolean { | ||||||
|  |   const explicitTypes = ['string', 'number', 'boolean'] | ||||||
|  |   return explicitTypes.some(elem => type.toLowerCase() === elem) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function isBoolean(...args: string[]): boolean { | ||||||
|  |   return args.some(elem => elem.toLowerCase() === 'boolean') | ||||||
|  | } | ||||||
| @ -349,11 +349,16 @@ export function createRenderer(options: RendererOptions) { | |||||||
|     anchor?: HostNode |     anchor?: HostNode | ||||||
|   ) { |   ) { | ||||||
|     const instance: ComponentHandle = (vnode.component = { |     const instance: ComponentHandle = (vnode.component = { | ||||||
|       type: vnode.type as Function, |       type: vnode.type as any, | ||||||
|       vnode: null, |       vnode: null, | ||||||
|       next: null, |       next: null, | ||||||
|       subTree: null, |       subTree: null, | ||||||
|       update: null as any |       update: null as any, | ||||||
|  |       $attrs: EMPTY_OBJ, | ||||||
|  |       $props: EMPTY_OBJ, | ||||||
|  |       $refs: EMPTY_OBJ, | ||||||
|  |       $slots: EMPTY_OBJ, | ||||||
|  |       $state: EMPTY_OBJ | ||||||
|     }) |     }) | ||||||
| 
 | 
 | ||||||
|     // TODO call setup, handle bindings and render context
 |     // TODO call setup, handle bindings and render context
 | ||||||
|  | |||||||
| @ -7,6 +7,15 @@ export { | |||||||
|   Text, |   Text, | ||||||
|   Empty |   Empty | ||||||
| } from './vnode' | } from './vnode' | ||||||
|  | 
 | ||||||
|  | export { | ||||||
|  |   ComponentOptions, | ||||||
|  |   FunctionalComponent, | ||||||
|  |   Slots, | ||||||
|  |   Slot, | ||||||
|  |   createComponent | ||||||
|  | } from './component' | ||||||
|  | 
 | ||||||
| export { createRenderer, RendererOptions } from './createRenderer' | export { createRenderer, RendererOptions } from './createRenderer' | ||||||
| export * from '@vue/observer' |  | ||||||
| export { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags' | export { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags' | ||||||
|  | export * from '@vue/observer' | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								packages/runtime-core/src/warning.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								packages/runtime-core/src/warning.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | export function warn(...args: any[]) { | ||||||
|  |   // TODO
 | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user