wip: support returning render fn from setup() + improve createComponent type inference

This commit is contained in:
Evan You 2019-06-12 15:43:19 +08:00
parent bfe6987323
commit fce6a8fa51
5 changed files with 176 additions and 71 deletions

View File

@ -8,10 +8,10 @@ module.exports = {
coverageDirectory: 'coverage',
coverageReporters: ['html', 'lcov', 'text'],
collectCoverageFrom: ['packages/*/src/**/*.ts'],
moduleFileExtensions: ['ts', 'js', 'json'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
moduleNameMapper: {
'^@vue/(.*?)$': '<rootDir>/packages/$1/src'
},
rootDir: __dirname,
testMatch: ['<rootDir>/packages/**/__tests__/**/*spec.(t|j)s']
testMatch: ['<rootDir>/packages/**/__tests__/**/*spec.[jt]s?(x)']
}

View File

@ -2,8 +2,13 @@ import { createComponent } from '../src/component'
import { value } from '@vue/reactivity'
import { PropType } from '../src/componentProps'
// mock React just for TSX testing purposes
const React = {
createElement: () => {}
}
test('createComponent type inference', () => {
createComponent({
const MyComponent = createComponent({
props: {
a: Number,
// required should make property non-void
@ -36,9 +41,7 @@ test('createComponent type inference', () => {
}
}
},
render({ state, props }) {
state.c * 2
state.d.e.slice()
render(props) {
props.a && props.a * 2
props.b.slice()
props.bb.slice()
@ -53,47 +56,53 @@ test('createComponent type inference', () => {
this.dd.push('dd')
}
})
// rename this file to .tsx to test TSX props inference
// ;(<MyComponent a={1} b="foo"/>)
// test TSX props inference
;(<MyComponent a={1} b="foo" dd={['foo']}/>)
})
test('type inference w/ optional props declaration', () => {
createComponent({
setup(props) {
props.anything
const Comp = createComponent({
setup(props: { msg: string }) {
props.msg
return {
a: 1
}
},
render({ props, state }) {
props.foobar
state.a * 2
render(props) {
props.msg
this.a * 2
// should not make state and this indexable
// state.foobar
// this.foobar
}
})
;(<Comp msg="hello"/>)
})
// test('type inference w/ array props declaration', () => {
// createComponent({
// props: ['a', 'b'],
// setup(props) {
// props.a
// props.b
// return {
// c: 1
// }
// },
// render({ props, state }) {
// props.a
// props.b
// state.c
// this.a
// this.b
// this.c
// }
// })
// })
test('type inference w/ direct setup function', () => {
const Comp = createComponent((props: { msg: string }) => {
return () => <div>{props.msg}</div>
})
;(<Comp msg="hello"/>)
})
test('type inference w/ array props declaration', () => {
const Comp = createComponent({
props: ['a', 'b'],
setup(props) {
props.a
props.b
return {
c: 1
}
},
render(props) {
props.a
props.b
this.a
this.b
this.c
}
})
;(<Comp a={1} b={2}/>)
})

View File

@ -5,7 +5,7 @@ import {
state,
immutableState
} from '@vue/reactivity'
import { EMPTY_OBJ } from '@vue/shared'
import { EMPTY_OBJ, isFunction } from '@vue/shared'
import { RenderProxyHandlers } from './componentProxy'
import { ComponentPropsOptions, ExtractPropTypes } from './componentProps'
import { PROPS, DYNAMIC_SLOTS, FULL_PROPS } from './patchFlags'
@ -14,9 +14,9 @@ import { STATEFUL_COMPONENT } from './typeFlags'
export type Data = { [key: string]: any }
export type ComponentPublicProperties<P = {}, S = {}> = {
export type ComponentRenderProxy<P = {}, S = {}, PublicProps = P> = {
$state: S
$props: P
$props: PublicProps
$attrs: Data
// TODO
@ -28,22 +28,61 @@ export type ComponentPublicProperties<P = {}, S = {}> = {
} & P &
S
interface ComponentOptions<
RawProps = ComponentPropsOptions,
type RenderFunction<P = Data> = (
props: P,
slots: Slots,
attrs: Data,
vnode: VNode
) => any
type RenderFunctionWithThis<Props, RawBindings> = <
Bindings extends UnwrapValue<RawBindings>
>(
this: ComponentRenderProxy<Props, Bindings>,
props: Props,
slots: Slots,
attrs: Data,
vnode: VNode
) => VNodeChild
interface ComponentOptionsWithProps<
PropsOptions = ComponentPropsOptions,
RawBindings = Data,
Props = ExtractPropTypes<RawProps>,
ExposedProps = RawProps extends object ? Props : {}
Props = ExtractPropTypes<PropsOptions>
> {
props?: RawProps
setup?: (this: ComponentPublicProperties, props: Props) => RawBindings
render?: <State extends UnwrapValue<RawBindings>>(
this: ComponentPublicProperties<ExposedProps, State>,
ctx: ComponentInstance<Props, State>
) => VNodeChild
props: PropsOptions
setup?: (
this: ComponentRenderProxy<Props>,
props: Props
) => RawBindings | RenderFunction<Props>
render?: RenderFunctionWithThis<Props, RawBindings>
}
export interface FunctionalComponent<P = {}> {
(ctx: ComponentInstance<P>): any
interface ComponentOptionsWithoutProps<Props = Data, RawBindings = Data> {
props?: undefined
setup?: (
this: ComponentRenderProxy<Props>,
props: Props
) => RawBindings | RenderFunction<Props>
render?: RenderFunctionWithThis<Props, RawBindings>
}
interface ComponentOptionsWithArrayProps<
PropNames extends string,
RawBindings = Data,
Props = { [key in PropNames]?: any }
> {
props: PropNames[]
setup?: (
this: ComponentRenderProxy<Props>,
props: Props
) => RawBindings | RenderFunction<Props>
render?: RenderFunctionWithThis<Props, RawBindings>
}
type ComponentOptions = ComponentOptionsWithProps | ComponentOptionsWithoutProps
export interface FunctionalComponent<P = {}> extends RenderFunction<P> {
props?: ComponentPropsOptions<P>
displayName?: string
}
@ -73,8 +112,9 @@ export type ComponentInstance<P = Data, S = Data> = {
subTree: VNode
update: ReactiveEffect
effects: ReactiveEffect[] | null
render: RenderFunction<P> | null
// the rest are only for stateful components
renderProxy: ComponentPublicProperties | null
renderProxy: ComponentRenderProxy | null
propsProxy: Data | null
state: S
props: P
@ -84,13 +124,36 @@ export type ComponentInstance<P = Data, S = Data> = {
} & LifecycleHooks
// no-op, for type inference only
export function createComponent<RawProps, RawBindings>(
options: ComponentOptions<RawProps, RawBindings>
export function createComponent<Props>(
setup: (props: Props) => RenderFunction<Props>
): (props: Props) => any
export function createComponent<PropNames extends string, RawBindings>(
options: ComponentOptionsWithArrayProps<PropNames, RawBindings>
): {
// for TSX
new (): { $props: ExtractPropTypes<RawProps> }
} {
return options as any
// for Vetur and TSX support
new (): ComponentRenderProxy<
{ [key in PropNames]?: any },
UnwrapValue<RawBindings>
>
}
export function createComponent<Props, RawBindings>(
options: ComponentOptionsWithoutProps<Props, RawBindings>
): {
// for Vetur and TSX support
new (): ComponentRenderProxy<Props, UnwrapValue<RawBindings>>
}
export function createComponent<PropsOptions, RawBindings>(
options: ComponentOptionsWithProps<PropsOptions, RawBindings>
): {
// for Vetur and TSX support
new (): ComponentRenderProxy<
ExtractPropTypes<PropsOptions>,
UnwrapValue<RawBindings>,
ExtractPropTypes<PropsOptions, false>
>
}
export function createComponent(options: any) {
return isFunction(options) ? { setup: options } : (options as any)
}
export function createComponentInstance(
@ -105,6 +168,7 @@ export function createComponentInstance(
next: null,
subTree: null as any,
update: null as any,
render: null,
renderProxy: null,
propsProxy: null,
@ -153,23 +217,39 @@ export function setupStatefulComponent(instance: ComponentInstance) {
const propsProxy = (instance.propsProxy = setup.length
? immutableState(instance.props)
: null)
instance.state = state(setup.call(proxy, propsProxy))
const setupResult = setup.call(proxy, propsProxy)
if (isFunction(setupResult)) {
// setup returned a render function
instance.render = setupResult
} else {
// setup returned bindings
instance.state = state(setupResult)
if (__DEV__ && !Component.render) {
// TODO warn missing render fn
}
instance.render = Component.render as RenderFunction
}
currentInstance = null
}
}
export function renderComponentRoot(instance: ComponentInstance): VNode {
const { type: Component, vnode } = instance
const { type: Component, renderProxy, props, slots, attrs, vnode } = instance
if (vnode.shapeFlag & STATEFUL_COMPONENT) {
if (__DEV__ && !(Component as any).render) {
// TODO warn missing render
}
return normalizeVNode(
(Component as any).render.call(instance.renderProxy, instance)
(instance.render as RenderFunction).call(
renderProxy,
props,
slots,
attrs,
vnode
)
)
} else {
// functional
return normalizeVNode((Component as FunctionalComponent)(instance))
return normalizeVNode(
(Component as FunctionalComponent)(props, slots, attrs, vnode)
)
}
}

View File

@ -31,11 +31,18 @@ export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
type PropConstructor<T> = { new (...args: any[]): T & object } | { (): T }
type RequiredKeys<T> = {
[K in keyof T]: T[K] extends { required: true } | { default: any } ? K : never
type RequiredKeys<T, MakeDefautRequired> = {
[K in keyof T]: T[K] extends
| { required: true }
| (MakeDefautRequired extends true ? { default: any } : never)
? K
: never
}[keyof T]
type OptionalKeys<T> = Exclude<keyof T, RequiredKeys<T>>
type OptionalKeys<T, MakeDefautRequired> = Exclude<
keyof T,
RequiredKeys<T, MakeDefautRequired>
>
type InferPropType<T> = T extends null
? any // null & true would fail to infer
@ -45,9 +52,18 @@ type InferPropType<T> = T extends null
? { [key: string]: any }
: T extends Prop<infer V> ? V : T
export type ExtractPropTypes<O> = O extends object
? { readonly [K in RequiredKeys<O>]: InferPropType<O[K]> } &
{ readonly [K in OptionalKeys<O>]?: InferPropType<O[K]> }
export type ExtractPropTypes<
O,
MakeDefautRequired extends boolean = true
> = O extends object
? {
readonly [K in RequiredKeys<O, MakeDefautRequired>]: InferPropType<O[K]>
} &
{
readonly [K in OptionalKeys<O, MakeDefautRequired>]?: InferPropType<
O[K]
>
}
: { [K in string]: any }
const enum BooleanFlags {

View File

@ -12,7 +12,7 @@
"noImplicitAny": true,
"experimentalDecorators": true,
"removeComments": false,
"jsx": "preserve",
"jsx": "react",
"lib": [
"esnext",
"dom"