From c0c06813a763c3425d71832e76f935f5c3ef1de1 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 31 May 2019 18:07:43 +0800 Subject: [PATCH] wip: slots --- packages/runtime-core/src/component.ts | 17 +----- packages/runtime-core/src/componentProps.ts | 18 +++++-- packages/runtime-core/src/componentSlots.ts | 59 +++++++++++++++++++++ packages/runtime-core/src/createRenderer.ts | 12 +++-- packages/runtime-core/src/index.ts | 12 +++-- packages/runtime-core/src/vnode.ts | 26 +++++++-- 6 files changed, 110 insertions(+), 34 deletions(-) create mode 100644 packages/runtime-core/src/componentSlots.ts diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 496ee37c..f13d9ac4 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -7,19 +7,12 @@ import { } from '@vue/observer' import { isFunction, EMPTY_OBJ } from '@vue/shared' import { RenderProxyHandlers } from './componentProxy' -import { ComponentPropsOptions, PropValidator } from './componentProps' +import { ComponentPropsOptions, ExtractPropTypes } from './componentProps' import { PROPS, DYNAMIC_SLOTS, FULL_PROPS } from './patchFlags' +import { Slots } from './componentSlots' export type Data = { [key: string]: any } -type ExtractPropTypes = { - readonly [key in keyof PropOptions]: PropOptions[key] extends PropValidator< - infer V - > - ? V - : PropOptions[key] extends null | undefined ? any : PropOptions[key] -} - export type ComponentPublicProperties

= { $state: S $props: P @@ -70,12 +63,6 @@ export interface LifecycleHooks { ec: LifecycleHook // errorCaptured } -export type Slot = (...args: any[]) => VNode[] - -export type Slots = Readonly<{ - [name: string]: Slot -}> - export type ComponentInstance

= { type: FunctionalComponent | ComponentOptions vnode: VNode diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index e2aad8da..35fafc8a 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -16,17 +16,25 @@ export type ComponentPropsOptions

= { [K in keyof P]: PropValidator } -export type Prop = { (): T } | { new (...args: any[]): T & object } +type Prop = { (): T } | { new (...args: any[]): T & object } -export type PropType = Prop | Prop[] +type PropType = Prop | Prop[] -export type PropValidator = PropOptions | PropType +type PropValidator = PropOptions | PropType -export interface PropOptions { +interface PropOptions { type?: PropType | true | null required?: boolean default?: T | null | undefined | (() => T | null | undefined) - validator?(value: T): boolean + validator?(value: any): boolean +} + +export type ExtractPropTypes = { + readonly [key in keyof PropOptions]: PropOptions[key] extends PropValidator< + infer V + > + ? V + : PropOptions[key] extends null | true ? any : PropOptions[key] } const enum BooleanFlags { diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts new file mode 100644 index 00000000..65ca9842 --- /dev/null +++ b/packages/runtime-core/src/componentSlots.ts @@ -0,0 +1,59 @@ +import { ComponentInstance } from './component' +import { VNode, NormalizedChildren, normalizeVNode, VNodeChild } from './vnode' +import { isArray, isObject, isFunction } from '@vue/shared' + +export type Slot = (...args: any[]) => VNode[] +export type Slots = Readonly<{ + [name: string]: Slot +}> +export type RawSlots = { + [name: string]: unknown +} + +const normalizeSlotValue = (value: unknown): VNode[] => + isArray(value) + ? value.map(normalizeVNode) + : [normalizeVNode(value as VNodeChild)] + +const normalizeSlot = (rawSlot: Function): Slot => (props: any) => + normalizeSlotValue(rawSlot(props)) + +export function resolveSlots( + instance: ComponentInstance, + children: NormalizedChildren +) { + let slots: Slots | void + if (isObject(children) && !isArray(children)) { + // pre-normalized slots object generated by compiler + if ((children as any)._normalized) { + slots = children as Slots + } else { + slots = {} + for (const key in children) { + let value = children[key] + if (isFunction(value)) { + ;(slots as any)[key] = normalizeSlot(value) + } else { + if (__DEV__) { + // TODO show tip on using functions + console.log('use function slots!') + } + value = normalizeSlotValue(value) + ;(slots as any)[key] = () => value + } + } + } + } else if (children != null) { + // Array, string or null. + // non object children passed to a component + if (__DEV__) { + // TODO show tip on using functions + console.log('use function slots!') + } + const normalized = normalizeSlotValue(children) + slots = { default: () => normalized } + } + if (slots !== void 0) { + instance.slots = slots + } +} diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index deda9abb..fa7a6175 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -34,6 +34,7 @@ import { import { queueJob, queuePostFlushCb, flushPostFlushCbs } from './scheduler' import { effect, stop, ReactiveEffectOptions } from '@vue/observer' import { resolveProps } from './componentProps' +import { resolveSlots } from './componentSlots' const prodEffectOptions = { scheduler: queueJob @@ -201,7 +202,7 @@ export function createRenderer(options: RendererOptions) { } if (isString(vnode.children)) { hostSetElementText(el, vnode.children) - } else if (vnode.children != null) { + } else if (isArray(vnode.children)) { mountChildren(vnode.children, el) } hostInsert(el, container, anchor) @@ -382,7 +383,7 @@ export function createRenderer(options: RendererOptions) { if (target != null) { if (isString(children)) { hostSetElementText(target, children) - } else if (children != null) { + } else if (isArray(children)) { mountChildren(children, target) } } else { @@ -407,7 +408,7 @@ export function createRenderer(options: RendererOptions) { if (isString(children)) { hostSetElementText(target, '') hostSetElementText(nextTarget, children) - } else if (children != null) { + } else if (isArray(children)) { for (let i = 0; i < children.length; i++) { move(children[i] as VNode, nextTarget, null) } @@ -454,6 +455,7 @@ export function createRenderer(options: RendererOptions) { // initial mount instance.vnode = vnode resolveProps(instance, vnode.props, Component.props) + resolveSlots(instance, vnode.children) // setup stateful if (typeof Component === 'object') { setupStatefulComponent(instance) @@ -479,7 +481,7 @@ export function createRenderer(options: RendererOptions) { instance.vnode = next instance.next = null resolveProps(instance, next.props, Component.props) - // TODO slots + resolveSlots(instance, next.children) } const prevTree = instance.subTree const nextTree = (instance.subTree = renderComponentRoot(instance)) @@ -551,7 +553,7 @@ export function createRenderer(options: RendererOptions) { } else { if (isString(c1)) { hostSetElementText(container, '') - if (c2 != null) { + if (isArray(c2)) { mountChildren(c2, container, anchor) } } else if (isArray(c1)) { diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 2069e2f9..df111f70 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -10,13 +10,15 @@ export { export { ComponentOptions, FunctionalComponent, - Slots, - Slot, createComponent } from './component' -export * from './componentLifecycle' +export { Slot, Slots } from './componentSlots' + +export { ComponentPropsOptions } from './componentProps' -export { createRenderer, RendererOptions } from './createRenderer' -export { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags' export * from './reactivity' +export * from './componentLifecycle' +export { createRenderer, RendererOptions } from './createRenderer' + +export { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags' diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 6ac85823..d6f9e3c2 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -1,6 +1,7 @@ -import { isArray, EMPTY_ARR } from '@vue/shared' +import { isArray, isFunction, isString, EMPTY_ARR } from '@vue/shared' import { ComponentInstance } from './component' import { HostNode } from './createRenderer' +import { RawSlots } from './componentSlots' export const Fragment = Symbol('Fragment') export const Text = Symbol('Text') @@ -19,11 +20,13 @@ type VNodeChildAtom = VNode | string | number | null | void export interface VNodeChildren extends Array {} export type VNodeChild = VNodeChildAtom | VNodeChildren +export type NormalizedChildren = string | VNodeChildren | RawSlots | null + export interface VNode { type: VNodeTypes props: { [key: string]: any } | null key: string | number | null - children: string | VNodeChildren | null + children: NormalizedChildren component: ComponentInstance | null // DOM @@ -91,7 +94,7 @@ export function createVNode( type, props, key: props && props.key, - children: typeof children === 'number' ? children + '' : children, + children: normalizeChildren(children), component: null, el: null, anchor: null, @@ -127,10 +130,25 @@ export function normalizeVNode(child: VNodeChild): VNode { // fragment return createVNode(Fragment, null, child) } else if (typeof child === 'object') { - // already vnode + // already vnode, this should be the most common since compiled templates + // always produce all-vnode children arrays return child as VNode } else { // primitive types return createVNode(Text, null, child + '') } } + +export function normalizeChildren(children: unknown): NormalizedChildren { + if (children == null) { + return null + } else if (isArray(children)) { + return children + } else if (typeof children === 'object') { + return children as RawSlots + } else if (isFunction(children)) { + return { default: children } + } else { + return isString(children) ? children : children + '' + } +}