mirror of https://github.com/vuejs/core.git
feat(runtime-vapor): component props (#40)
This commit is contained in:
parent
ecf7da98d7
commit
12250a85b9
|
@ -1,13 +1,25 @@
|
|||
import { type Ref, EffectScope, ref } from '@vue/reactivity'
|
||||
import type { Block } from './render'
|
||||
import type { DirectiveBinding } from './directive'
|
||||
import { EffectScope, Ref, ref } from '@vue/reactivity'
|
||||
|
||||
import { EMPTY_OBJ } from '@vue/shared'
|
||||
import { Block } from './render'
|
||||
import { type DirectiveBinding } from './directive'
|
||||
import {
|
||||
type ComponentPropsOptions,
|
||||
type NormalizedPropsOptions,
|
||||
normalizePropsOptions,
|
||||
} from './componentProps'
|
||||
|
||||
import type { Data } from '@vue/shared'
|
||||
|
||||
export type Component = FunctionalComponent | ObjectComponent
|
||||
|
||||
export type SetupFn = (props: any, ctx: any) => Block | Data
|
||||
export type FunctionalComponent = SetupFn & {
|
||||
props: ComponentPropsOptions
|
||||
render(ctx: any): Block
|
||||
}
|
||||
export interface ObjectComponent {
|
||||
props: ComponentPropsOptions
|
||||
setup: SetupFn
|
||||
render(ctx: any): Block
|
||||
}
|
||||
|
@ -17,13 +29,22 @@ export interface ComponentInternalInstance {
|
|||
container: ParentNode
|
||||
block: Block | null
|
||||
scope: EffectScope
|
||||
|
||||
component: FunctionalComponent | ObjectComponent
|
||||
get isMounted(): boolean
|
||||
isMountedRef: Ref<boolean>
|
||||
propsOptions: NormalizedPropsOptions
|
||||
|
||||
// TODO: type
|
||||
proxy: Data | null
|
||||
|
||||
// state
|
||||
props: Data
|
||||
setupState: Data
|
||||
|
||||
/** directives */
|
||||
dirs: Map<Node, DirectiveBinding[]>
|
||||
|
||||
// lifecycle
|
||||
get isMounted(): boolean
|
||||
isMountedRef: Ref<boolean>
|
||||
// TODO: registory of provides, appContext, lifecycles, ...
|
||||
}
|
||||
|
||||
|
@ -51,14 +72,25 @@ export const createComponentInstance = (
|
|||
block: null,
|
||||
container: null!, // set on mount
|
||||
scope: new EffectScope(true /* detached */)!,
|
||||
|
||||
component,
|
||||
|
||||
// resolved props and emits options
|
||||
propsOptions: normalizePropsOptions(component),
|
||||
// emitsOptions: normalizeEmitsOptions(type, appContext), // TODO:
|
||||
|
||||
proxy: null,
|
||||
|
||||
// state
|
||||
props: EMPTY_OBJ,
|
||||
setupState: EMPTY_OBJ,
|
||||
|
||||
dirs: new Map(),
|
||||
|
||||
// lifecycle
|
||||
get isMounted() {
|
||||
return isMountedRef.value
|
||||
},
|
||||
isMountedRef,
|
||||
|
||||
dirs: new Map(),
|
||||
// TODO: registory of provides, appContext, lifecycles, ...
|
||||
}
|
||||
return instance
|
||||
|
|
|
@ -0,0 +1,267 @@
|
|||
// NOTE: runtime-core/src/componentProps.ts
|
||||
|
||||
import {
|
||||
Data,
|
||||
EMPTY_ARR,
|
||||
EMPTY_OBJ,
|
||||
camelize,
|
||||
extend,
|
||||
hasOwn,
|
||||
hyphenate,
|
||||
isArray,
|
||||
isFunction,
|
||||
isReservedProp,
|
||||
} from '@vue/shared'
|
||||
import { shallowReactive, toRaw } from '@vue/reactivity'
|
||||
import { type ComponentInternalInstance, type Component } from './component'
|
||||
|
||||
export type ComponentPropsOptions<P = Data> =
|
||||
| ComponentObjectPropsOptions<P>
|
||||
| string[]
|
||||
|
||||
export type ComponentObjectPropsOptions<P = Data> = {
|
||||
[K in keyof P]: Prop<P[K]> | null
|
||||
}
|
||||
|
||||
export type Prop<T, D = T> = PropOptions<T, D> | PropType<T>
|
||||
|
||||
type DefaultFactory<T> = (props: Data) => T | null | undefined
|
||||
|
||||
export interface PropOptions<T = any, D = T> {
|
||||
type?: PropType<T> | true | null
|
||||
required?: boolean
|
||||
default?: D | DefaultFactory<D> | null | undefined | object
|
||||
validator?(value: unknown): boolean
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
skipFactory?: boolean
|
||||
}
|
||||
|
||||
export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
|
||||
|
||||
type PropConstructor<T = any> =
|
||||
| { new (...args: any[]): T & {} }
|
||||
| { (): T }
|
||||
| PropMethod<T>
|
||||
|
||||
type PropMethod<T, TConstructor = any> = [T] extends [
|
||||
((...args: any) => any) | undefined,
|
||||
] // if is function with args, allowing non-required functions
|
||||
? { new (): TConstructor; (): T; readonly prototype: TConstructor } // Create Function like constructor
|
||||
: never
|
||||
|
||||
enum BooleanFlags {
|
||||
shouldCast,
|
||||
shouldCastTrue,
|
||||
}
|
||||
|
||||
type NormalizedProp =
|
||||
| null
|
||||
| (PropOptions & {
|
||||
[BooleanFlags.shouldCast]?: boolean
|
||||
[BooleanFlags.shouldCastTrue]?: boolean
|
||||
})
|
||||
|
||||
export type NormalizedProps = Record<string, NormalizedProp>
|
||||
export type NormalizedPropsOptions = [NormalizedProps, string[]] | []
|
||||
|
||||
export function initProps(
|
||||
instance: ComponentInternalInstance,
|
||||
rawProps: Data | null,
|
||||
) {
|
||||
const props: Data = {}
|
||||
|
||||
const [options, needCastKeys] = instance.propsOptions
|
||||
let rawCastValues: Data | undefined
|
||||
if (rawProps) {
|
||||
for (let key in rawProps) {
|
||||
// key, ref are reserved and never passed down
|
||||
if (isReservedProp(key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const valueGetter = () => rawProps[key]
|
||||
let camelKey
|
||||
if (options && hasOwn(options, (camelKey = camelize(key)))) {
|
||||
if (!needCastKeys || !needCastKeys.includes(camelKey)) {
|
||||
// NOTE: must getter
|
||||
// props[camelKey] = value
|
||||
Object.defineProperty(props, camelKey, {
|
||||
get() {
|
||||
return valueGetter()
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// NOTE: must getter
|
||||
// ;(rawCastValues || (rawCastValues = {}))[camelKey] = value
|
||||
rawCastValues || (rawCastValues = {})
|
||||
Object.defineProperty(rawCastValues, camelKey, {
|
||||
get() {
|
||||
return valueGetter()
|
||||
},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// TODO:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needCastKeys) {
|
||||
const rawCurrentProps = toRaw(props)
|
||||
const castValues = rawCastValues || EMPTY_OBJ
|
||||
for (let i = 0; i < needCastKeys.length; i++) {
|
||||
const key = needCastKeys[i]
|
||||
|
||||
// NOTE: must getter
|
||||
// props[key] = resolvePropValue(
|
||||
// options!,
|
||||
// rawCurrentProps,
|
||||
// key,
|
||||
// castValues[key],
|
||||
// instance,
|
||||
// !hasOwn(castValues, key),
|
||||
// )
|
||||
Object.defineProperty(props, key, {
|
||||
get() {
|
||||
return resolvePropValue(
|
||||
options!,
|
||||
rawCurrentProps,
|
||||
key,
|
||||
castValues[key],
|
||||
instance,
|
||||
!hasOwn(castValues, key),
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
instance.props = shallowReactive(props)
|
||||
}
|
||||
|
||||
function resolvePropValue(
|
||||
options: NormalizedProps,
|
||||
props: Data,
|
||||
key: string,
|
||||
value: unknown,
|
||||
instance: ComponentInternalInstance,
|
||||
isAbsent: boolean,
|
||||
) {
|
||||
const opt = options[key]
|
||||
if (opt != null) {
|
||||
const hasDefault = hasOwn(opt, 'default')
|
||||
// default values
|
||||
if (hasDefault && value === undefined) {
|
||||
const defaultValue = opt.default
|
||||
if (
|
||||
opt.type !== Function &&
|
||||
!opt.skipFactory &&
|
||||
isFunction(defaultValue)
|
||||
) {
|
||||
// TODO: caching?
|
||||
// const { propsDefaults } = instance
|
||||
// if (key in propsDefaults) {
|
||||
// value = propsDefaults[key]
|
||||
// } else {
|
||||
// setCurrentInstance(instance)
|
||||
// value = propsDefaults[key] = defaultValue.call(
|
||||
// __COMPAT__ &&
|
||||
// isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
|
||||
// ? createPropsDefaultThis(instance, props, key)
|
||||
// : null,
|
||||
// props,
|
||||
// )
|
||||
// unsetCurrentInstance()
|
||||
// }
|
||||
} else {
|
||||
value = defaultValue
|
||||
}
|
||||
}
|
||||
// boolean casting
|
||||
if (opt[BooleanFlags.shouldCast]) {
|
||||
if (isAbsent && !hasDefault) {
|
||||
value = false
|
||||
} else if (
|
||||
opt[BooleanFlags.shouldCastTrue] &&
|
||||
(value === '' || value === hyphenate(key))
|
||||
) {
|
||||
value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function normalizePropsOptions(comp: Component): NormalizedPropsOptions {
|
||||
// TODO: cahching?
|
||||
|
||||
const raw = comp.props as any
|
||||
const normalized: NormalizedPropsOptions[0] = {}
|
||||
const needCastKeys: NormalizedPropsOptions[1] = []
|
||||
|
||||
if (!raw) {
|
||||
return EMPTY_ARR as any
|
||||
}
|
||||
|
||||
if (isArray(raw)) {
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
const normalizedKey = camelize(raw[i])
|
||||
if (validatePropName(normalizedKey)) {
|
||||
normalized[normalizedKey] = EMPTY_OBJ
|
||||
}
|
||||
}
|
||||
} else if (raw) {
|
||||
for (const key in raw) {
|
||||
const normalizedKey = camelize(key)
|
||||
if (validatePropName(normalizedKey)) {
|
||||
const opt = raw[key]
|
||||
const prop: NormalizedProp = (normalized[normalizedKey] =
|
||||
isArray(opt) || isFunction(opt) ? { type: opt } : extend({}, opt))
|
||||
if (prop) {
|
||||
const booleanIndex = getTypeIndex(Boolean, prop.type)
|
||||
const stringIndex = getTypeIndex(String, prop.type)
|
||||
prop[BooleanFlags.shouldCast] = booleanIndex > -1
|
||||
prop[BooleanFlags.shouldCastTrue] =
|
||||
stringIndex < 0 || booleanIndex < stringIndex
|
||||
// if the prop needs boolean casting or default value
|
||||
if (booleanIndex > -1 || hasOwn(prop, 'default')) {
|
||||
needCastKeys.push(normalizedKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res: NormalizedPropsOptions = [normalized, needCastKeys]
|
||||
return res
|
||||
}
|
||||
|
||||
function validatePropName(key: string) {
|
||||
if (key[0] !== '$') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function getType(ctor: Prop<any>): string {
|
||||
const match = ctor && ctor.toString().match(/^\s*(function|class) (\w+)/)
|
||||
return match ? match[2] : ctor === null ? 'null' : ''
|
||||
}
|
||||
|
||||
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)) {
|
||||
return expectedTypes.findIndex((t) => isSameType(t, type))
|
||||
} else if (isFunction(expectedTypes)) {
|
||||
return isSameType(expectedTypes, type) ? 0 : -1
|
||||
}
|
||||
return -1
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { hasOwn } from '@vue/shared'
|
||||
import { type ComponentInternalInstance } from './component'
|
||||
|
||||
export interface ComponentRenderContext {
|
||||
[key: string]: any
|
||||
_: ComponentInternalInstance
|
||||
}
|
||||
|
||||
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
|
||||
get({ _: instance }: ComponentRenderContext, key: string) {
|
||||
let normalizedProps
|
||||
const { setupState, props } = instance
|
||||
if (hasOwn(setupState, key)) {
|
||||
return setupState[key]
|
||||
} else if (
|
||||
(normalizedProps = instance.propsOptions[0]) &&
|
||||
hasOwn(normalizedProps, key)
|
||||
) {
|
||||
return props![key]
|
||||
}
|
||||
},
|
||||
}
|
|
@ -1,14 +1,20 @@
|
|||
import { reactive } from '@vue/reactivity'
|
||||
import { markRaw, proxyRefs } from '@vue/reactivity'
|
||||
import { type Data } from '@vue/shared'
|
||||
|
||||
import {
|
||||
type Component,
|
||||
type ComponentInternalInstance,
|
||||
type FunctionalComponent,
|
||||
type ObjectComponent,
|
||||
createComponentInstance,
|
||||
setCurrentInstance,
|
||||
unsetCurrentInstance,
|
||||
} from './component'
|
||||
|
||||
import { initProps } from './componentProps'
|
||||
|
||||
import { invokeDirectiveHook } from './directive'
|
||||
|
||||
import { insert, remove } from './dom'
|
||||
import { PublicInstanceProxyHandlers } from './componentPublicInstance'
|
||||
|
||||
export type Block = Node | Fragment | Block[]
|
||||
export type ParentBlock = ParentNode | Node[]
|
||||
|
@ -16,13 +22,13 @@ export type Fragment = { nodes: Block; anchor: Node }
|
|||
export type BlockFn = (props: any, ctx: any) => Block
|
||||
|
||||
export function render(
|
||||
comp: ObjectComponent | FunctionalComponent,
|
||||
comp: Component,
|
||||
props: Data,
|
||||
container: string | ParentNode,
|
||||
): ComponentInternalInstance {
|
||||
const instance = createComponentInstance(comp)
|
||||
setCurrentInstance(instance)
|
||||
mountComponent(instance, (container = normalizeContainer(container)))
|
||||
return instance
|
||||
initProps(instance, props)
|
||||
return mountComponent(instance, (container = normalizeContainer(container)))
|
||||
}
|
||||
|
||||
export function normalizeContainer(container: string | ParentNode): ParentNode {
|
||||
|
@ -39,29 +45,34 @@ export function mountComponent(
|
|||
|
||||
setCurrentInstance(instance)
|
||||
const block = instance.scope.run(() => {
|
||||
const { component } = instance
|
||||
const props = {}
|
||||
const { component, props } = instance
|
||||
const ctx = { expose: () => {} }
|
||||
|
||||
const setupFn =
|
||||
typeof component === 'function' ? component : component.setup
|
||||
|
||||
const state = setupFn(props, ctx)
|
||||
instance.proxy = markRaw(
|
||||
new Proxy({ _: instance }, PublicInstanceProxyHandlers),
|
||||
)
|
||||
if (state && '__isScriptSetup' in state) {
|
||||
return (instance.block = component.render(reactive(state)))
|
||||
instance.setupState = proxyRefs(state)
|
||||
return (instance.block = component.render(instance.proxy))
|
||||
} else {
|
||||
return (instance.block = state as Block)
|
||||
}
|
||||
})!
|
||||
|
||||
invokeDirectiveHook(instance, 'beforeMount')
|
||||
insert(block, instance.container)
|
||||
instance.isMountedRef.value = true
|
||||
invokeDirectiveHook(instance, 'mounted')
|
||||
unsetCurrentInstance()
|
||||
|
||||
// TODO: lifecycle hooks (mounted, ...)
|
||||
// const { m } = instance
|
||||
// m && invoke(m)
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
export function unmountComponent(instance: ComponentInternalInstance) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from 'vue/vapor'
|
||||
|
||||
const modules = import.meta.glob<any>('./*.vue')
|
||||
const modules = import.meta.glob<any>('./*.(vue|js)')
|
||||
const mod = (modules['.' + location.pathname] || modules['./App.vue'])()
|
||||
|
||||
mod.then(({ default: mod }) => render(mod, '#app'))
|
||||
mod.then(({ default: mod }) => render(mod, {}, '#app'))
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
import { watch } from 'vue'
|
||||
import {
|
||||
children,
|
||||
on,
|
||||
ref,
|
||||
template,
|
||||
effect,
|
||||
setText,
|
||||
render as renderComponent // TODO:
|
||||
} from '@vue/vapor'
|
||||
|
||||
export default {
|
||||
props: undefined,
|
||||
|
||||
setup(_, {}) {
|
||||
const count = ref(1)
|
||||
const handleClick = () => {
|
||||
count.value++
|
||||
}
|
||||
|
||||
const __returned__ = { count, handleClick }
|
||||
|
||||
Object.defineProperty(__returned__, '__isScriptSetup', {
|
||||
enumerable: false,
|
||||
value: true
|
||||
})
|
||||
|
||||
return __returned__
|
||||
},
|
||||
|
||||
render(_ctx) {
|
||||
const t0 = template('<button></button>')
|
||||
const n0 = t0()
|
||||
const {
|
||||
0: [n1]
|
||||
} = children(n0)
|
||||
on(n1, 'click', _ctx.handleClick)
|
||||
effect(() => {
|
||||
setText(n1, void 0, _ctx.count)
|
||||
})
|
||||
|
||||
// TODO: create component fn?
|
||||
// const c0 = createComponent(...)
|
||||
// insert(n0, c0)
|
||||
renderComponent(
|
||||
child,
|
||||
|
||||
// TODO: proxy??
|
||||
{
|
||||
/* <Comp :count="count" /> */
|
||||
get count() {
|
||||
return _ctx.count
|
||||
},
|
||||
|
||||
/* <Comp :inline-double="count * 2" /> */
|
||||
get inlineDouble() {
|
||||
return _ctx.count * 2
|
||||
}
|
||||
},
|
||||
n0
|
||||
)
|
||||
|
||||
return n0
|
||||
}
|
||||
}
|
||||
|
||||
const child = {
|
||||
props: {
|
||||
count: { type: Number, default: 1 },
|
||||
inlineDouble: { type: Number, default: 2 }
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
watch(
|
||||
() => props.count,
|
||||
v => console.log('count changed', v)
|
||||
)
|
||||
watch(
|
||||
() => props.inlineDouble,
|
||||
v => console.log('inlineDouble changed', v)
|
||||
)
|
||||
|
||||
const __returned__ = {}
|
||||
|
||||
Object.defineProperty(__returned__, '__isScriptSetup', {
|
||||
enumerable: false,
|
||||
value: true
|
||||
})
|
||||
|
||||
return __returned__
|
||||
},
|
||||
|
||||
render(_ctx) {
|
||||
const t0 = template('<p></p>')
|
||||
const n0 = t0()
|
||||
const {
|
||||
0: [n1]
|
||||
} = children(n0)
|
||||
effect(() => {
|
||||
setText(n1, void 0, _ctx.count + ' * 2 = ' + _ctx.inlineDouble)
|
||||
})
|
||||
return n0
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue