refactor: component setup flow

This commit is contained in:
Evan You 2019-05-29 11:36:16 +08:00
parent dbbb36bd76
commit 6ceb732114
3 changed files with 54 additions and 97 deletions

View File

@ -2,12 +2,7 @@ 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, EMPTY_OBJ } from '@vue/shared'
import { RenderProxyHandlers } from './componentProxy' import { RenderProxyHandlers } from './componentProxy'
import { import { ComponentPropsOptions, PropValidator } from './componentProps'
resolveProps,
ComponentPropsOptions,
initializeProps,
PropValidator
} from './componentProps'
interface Value<T> { interface Value<T> {
value: T value: T
@ -37,13 +32,6 @@ export interface ComponentPublicProperties<P = Data, S = Data> {
$slots: Data $slots: Data
} }
interface RenderFunctionArg<B = Data, P = Data> {
state: B
props: P
attrs: Data
slots: Slots
}
export interface ComponentOptions< export interface ComponentOptions<
RawProps = ComponentPropsOptions, RawProps = ComponentPropsOptions,
RawBindings = Data | void, RawBindings = Data | void,
@ -54,22 +42,16 @@ 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: RenderFunctionArg<B, Props> ctx: ComponentInstance<Props, B>
) => VNodeChild ) => VNodeChild
} }
export interface FunctionalComponent<P = {}> { export interface FunctionalComponent<P = {}> {
(ctx: RenderFunctionArg): any (ctx: ComponentInstance<P>): any
props?: ComponentPropsOptions<P> props?: ComponentPropsOptions<P>
displayName?: string displayName?: string
} }
export type Slot = (...args: any[]) => VNode[]
export type Slots = Readonly<{
[name: string]: Slot
}>
type LifecycleHook = Function[] | null type LifecycleHook = Function[] | null
export interface LifecycleHooks { export interface LifecycleHooks {
@ -86,18 +68,26 @@ export interface LifecycleHooks {
ec: LifecycleHook // errorCaptured ec: LifecycleHook // errorCaptured
} }
export type ComponentInstance = { export type Slot = (...args: any[]) => VNode[]
export type Slots = Readonly<{
[name: string]: Slot
}>
export type ComponentInstance<P = Data, S = Data> = {
type: FunctionalComponent | ComponentOptions type: FunctionalComponent | ComponentOptions
vnode: VNode vnode: VNode
next: VNode | null next: VNode | null
subTree: VNode subTree: VNode
update: ReactiveEffect update: ReactiveEffect
// the rest are only for stateful components // the rest are only for stateful components
bindings: Data | null proxy: ComponentPublicProperties | null
proxy: Data | null state: S
} & LifecycleHooks & props: P
ComponentPublicProperties attrs: Data
slots: Slots
refs: Data
} & LifecycleHooks
// no-op, for type inference only // no-op, for type inference only
export function createComponent< export function createComponent<
@ -121,7 +111,6 @@ export function createComponentInstance(type: any): ComponentInstance {
next: null, next: null,
subTree: null as any, subTree: null as any,
update: null as any, update: null as any,
bindings: null,
proxy: null, proxy: null,
bm: null, bm: null,
@ -137,64 +126,41 @@ export function createComponentInstance(type: any): ComponentInstance {
ec: null, ec: null,
// public properties // public properties
$attrs: EMPTY_OBJ, state: EMPTY_OBJ,
$props: EMPTY_OBJ, props: EMPTY_OBJ,
$refs: EMPTY_OBJ, attrs: EMPTY_OBJ,
$slots: EMPTY_OBJ, slots: EMPTY_OBJ,
$state: EMPTY_OBJ refs: EMPTY_OBJ
} }
} }
export let currentInstance: ComponentInstance | null = null export let currentInstance: ComponentInstance | null = null
export function setupStatefulComponent( export function setupStatefulComponent(instance: ComponentInstance) {
instance: ComponentInstance,
props: Data | null
) {
const Component = instance.type as ComponentOptions const Component = instance.type as ComponentOptions
// 1. create render proxy // 1. create render proxy
const proxy = (instance.proxy = new Proxy(instance, RenderProxyHandlers)) const proxy = (instance.proxy = new Proxy(
// 2. resolve initial props instance,
initializeProps(instance, Component.props, props) RenderProxyHandlers
// 3. call setup() ) as any)
// 2. call setup()
if (Component.setup) { if (Component.setup) {
currentInstance = instance currentInstance = instance
instance.bindings = Component.setup.call(proxy, proxy) // TODO should pass reactive props here
instance.state = Component.setup.call(proxy, instance.props)
currentInstance = null currentInstance = null
} }
} }
export function renderComponentRoot( export function renderComponentRoot(instance: ComponentInstance): VNode {
instance: ComponentInstance, const { type: Component, proxy } = instance
useAlreadyResolvedProps?: boolean if (isFunction(Component)) {
): VNode { return normalizeVNode(Component(instance))
const { type, vnode, proxy, bindings, $slots } = instance
const renderArg: RenderFunctionArg = {
state: bindings || EMPTY_OBJ,
slots: $slots,
props: null as any,
attrs: null as any
}
if (useAlreadyResolvedProps) {
// initial render for stateful components with setup()
// props are already resolved
renderArg.props = instance.$props
renderArg.attrs = instance.$attrs
} else { } else {
const { 0: props, 1: attrs } = resolveProps( if (__DEV__ && !Component.render) {
(vnode as VNode).props,
type.props
)
instance.$props = renderArg.props = props
instance.$attrs = renderArg.attrs = attrs
}
if (isFunction(type)) {
return normalizeVNode(type(renderArg))
} else {
if (__DEV__ && !type.render) {
// TODO warn missing render // TODO warn missing render
} }
return normalizeVNode((type.render as Function).call(proxy, renderArg)) return normalizeVNode((Component.render as Function).call(proxy, instance))
} }
} }

View File

@ -43,20 +43,6 @@ type NormalizedPropsOptions = Record<string, NormalizedProp>
const isReservedKey = (key: string): boolean => key[0] === '_' || key[0] === '$' const isReservedKey = (key: string): boolean => key[0] === '_' || key[0] === '$'
export function initializeProps(
instance: ComponentInstance,
options: ComponentPropsOptions | undefined,
rawProps: Data | null
) {
const { 0: props, 1: attrs } = resolveProps(rawProps, options)
instance.$props = __DEV__ ? immutable(props) : props
instance.$attrs = options
? __DEV__
? immutable(attrs)
: attrs
: instance.$props
}
// resolve raw VNode data. // resolve raw VNode data.
// - filter out reserved keys (key, ref, slots) // - filter out reserved keys (key, ref, slots)
// - extract class and style into $attrs (to be merged onto child // - extract class and style into $attrs (to be merged onto child
@ -65,16 +51,15 @@ export function initializeProps(
// - if has declared props: put declared ones in `props`, the rest in `attrs` // - if has declared props: put declared ones in `props`, the rest in `attrs`
// - else: everything goes in `props`. // - else: everything goes in `props`.
const EMPTY_PROPS = [EMPTY_OBJ, EMPTY_OBJ] as [Data, Data]
export function resolveProps( export function resolveProps(
instance: ComponentInstance,
rawProps: any, rawProps: any,
_options: ComponentPropsOptions | void _options: ComponentPropsOptions | void
): [Data, Data] { ) {
const hasDeclaredProps = _options != null const hasDeclaredProps = _options != null
const options = normalizePropsOptions(_options) as NormalizedPropsOptions const options = normalizePropsOptions(_options) as NormalizedPropsOptions
if (!rawProps && !hasDeclaredProps) { if (!rawProps && !hasDeclaredProps) {
return EMPTY_PROPS return
} }
const props: any = {} const props: any = {}
let attrs: any = void 0 let attrs: any = void 0
@ -126,7 +111,13 @@ export function resolveProps(
// if component has no declared props, $attrs === $props // if component has no declared props, $attrs === $props
attrs = props attrs = props
} }
return [props, attrs]
instance.props = __DEV__ ? immutable(props) : props
instance.attrs = options
? __DEV__
? immutable(attrs)
: attrs
: instance.props
} }
const normalizationMap = new WeakMap() const normalizationMap = new WeakMap()

View File

@ -18,6 +18,7 @@ import { isString, isArray, EMPTY_OBJ, EMPTY_ARR } from '@vue/shared'
import { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags' import { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags'
import { queueJob, queuePostFlushCb, flushPostFlushCbs } from './scheduler' import { queueJob, queuePostFlushCb, flushPostFlushCbs } from './scheduler'
import { effect, stop, ReactiveEffectOptions } from '@vue/observer' import { effect, stop, ReactiveEffectOptions } from '@vue/observer'
import { resolveProps } from './componentProps'
const prodEffectOptions = { const prodEffectOptions = {
scheduler: queueJob scheduler: queueJob
@ -367,18 +368,16 @@ export function createRenderer(options: RendererOptions) {
const instance: ComponentInstance = (vnode.component = createComponentInstance( const instance: ComponentInstance = (vnode.component = createComponentInstance(
Component Component
)) ))
const needsSetup = typeof Component === 'object' && Component.setup
if (needsSetup) {
setupStatefulComponent(instance, vnode.props)
}
instance.update = effect(() => { instance.update = effect(() => {
if (!instance.vnode) { if (!instance.vnode) {
// initial mount // initial mount
instance.vnode = vnode instance.vnode = vnode
const subTree = (instance.subTree = renderComponentRoot( resolveProps(instance, vnode.props, Component.props)
instance, // setup stateful
needsSetup if (typeof Component === 'object' && Component.setup) {
)) setupStatefulComponent(instance)
}
const subTree = (instance.subTree = renderComponentRoot(instance))
// beforeMount hook // beforeMount hook
if (instance.bm !== null) { if (instance.bm !== null) {
invokeHooks(instance.bm) invokeHooks(instance.bm)
@ -396,6 +395,7 @@ export function createRenderer(options: RendererOptions) {
next.component = instance next.component = instance
instance.vnode = next instance.vnode = next
instance.next = null instance.next = null
resolveProps(instance, next.props, Component.props)
} }
const prevTree = instance.subTree const prevTree = instance.subTree
const nextTree = (instance.subTree = renderComponentRoot(instance)) const nextTree = (instance.subTree = renderComponentRoot(instance))