]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: props resolving
authorEvan You <yyx990803@gmail.com>
Tue, 28 May 2019 10:06:00 +0000 (18:06 +0800)
committerEvan You <yyx990803@gmail.com>
Tue, 28 May 2019 10:06:00 +0000 (18:06 +0800)
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentProps.ts [new file with mode: 0644]
packages/runtime-core/src/createRenderer.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/warning.ts [new file with mode: 0644]

index 3ba82c316ddfe2ee49f30c5c5ec9aae78df292b6..a37355dcd83d25901b748d28a15cf59e87ef9f42 100644 (file)
@@ -1,6 +1,7 @@
 import { VNode, normalizeVNode, VNodeChild } from './vnode'
 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> {
   value: T
@@ -18,14 +19,28 @@ type ExtractPropTypes<PropOptions> = {
     : PropOptions[key] extends null | undefined ? any : PropOptions[key]
 }
 
-interface ComponentPublicProperties<P, S> {
-  $props: P
+export type Data = { [key: string]: any }
+
+export interface ComponentPublicProperties<P = Data, S = Data> {
   $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<
-  RawProps = { [key: string]: Prop<any> },
-  RawBindings = { [key: string]: any } | void,
+  RawProps = ComponentPropsOptions,
+  RawBindings = Data | void,
   Props = ExtractPropTypes<RawProps>,
   Bindings = UnwrapBindings<RawBindings>
 > {
@@ -33,13 +48,22 @@ export interface ComponentOptions<
   setup?: (props: Props) => RawBindings
   render?: <B extends Bindings>(
     this: ComponentPublicProperties<Props, B>,
-    ctx: {
-      state: B
-      props: Props
-    }
+    ctx: RenderFunctionArg<B, Props>
   ) => 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
 export function createComponent<
   RawProps,
@@ -55,19 +79,25 @@ export function createComponent<
   return options as any
 }
 
-export interface ComponentHandle {
-  type: Function | ComponentOptions
+export type ComponentHandle = {
+  type: FunctionalComponent | ComponentOptions
   vnode: VNode | null
   next: VNode | null
   subTree: VNode | null
   update: ReactiveEffect
-}
+} & ComponentPublicProperties
 
 export function renderComponentRoot(handle: ComponentHandle): VNode {
   const { type, vnode } = handle
-  // TODO actually resolve props
+  const { 0: props, 1: attrs } = resolveProps(
+    (vnode as VNode).props,
+    type.props
+  )
   const renderArg = {
-    props: (vnode as VNode).props || EMPTY_OBJ
+    state: handle.$state,
+    slots: handle.$slots,
+    props,
+    attrs
   }
   if (isFunction(type)) {
     return normalizeVNode(type(renderArg))
diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts
new file mode 100644 (file)
index 0000000..1544c1e
--- /dev/null
@@ -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')
+}
index 9a90785525edccf7a548428134005ecdec0ce44e..169ad0d9be25798f1d2c810091ec8afe174b4be9 100644 (file)
@@ -349,11 +349,16 @@ export function createRenderer(options: RendererOptions) {
     anchor?: HostNode
   ) {
     const instance: ComponentHandle = (vnode.component = {
-      type: vnode.type as Function,
+      type: vnode.type as any,
       vnode: null,
       next: 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
index 09bbadef42873ebaadc7147ccf3436ae49bee495..92850f94831a5f5bbfea80a5c7dddfca70584b67 100644 (file)
@@ -7,6 +7,15 @@ export {
   Text,
   Empty
 } from './vnode'
+
+export {
+  ComponentOptions,
+  FunctionalComponent,
+  Slots,
+  Slot,
+  createComponent
+} from './component'
+
 export { createRenderer, RendererOptions } from './createRenderer'
-export * from '@vue/observer'
 export { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags'
+export * from '@vue/observer'
diff --git a/packages/runtime-core/src/warning.ts b/packages/runtime-core/src/warning.ts
new file mode 100644 (file)
index 0000000..78a8981
--- /dev/null
@@ -0,0 +1,3 @@
+export function warn(...args: any[]) {
+  // TODO
+}