wip: slots

This commit is contained in:
Evan You 2019-05-31 18:07:43 +08:00
parent e0a66d0381
commit c0c06813a7
6 changed files with 110 additions and 34 deletions

View File

@ -7,19 +7,12 @@ import {
} from '@vue/observer' } 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 { ComponentPropsOptions, PropValidator } from './componentProps' import { ComponentPropsOptions, ExtractPropTypes } from './componentProps'
import { PROPS, DYNAMIC_SLOTS, FULL_PROPS } from './patchFlags' import { PROPS, DYNAMIC_SLOTS, FULL_PROPS } from './patchFlags'
import { Slots } from './componentSlots'
export type Data = { [key: string]: any } export type Data = { [key: string]: any }
type ExtractPropTypes<PropOptions> = {
readonly [key in keyof PropOptions]: PropOptions[key] extends PropValidator<
infer V
>
? V
: PropOptions[key] extends null | undefined ? any : PropOptions[key]
}
export type ComponentPublicProperties<P = Data, S = Data> = { export type ComponentPublicProperties<P = Data, S = Data> = {
$state: S $state: S
$props: P $props: P
@ -70,12 +63,6 @@ export interface LifecycleHooks {
ec: LifecycleHook // errorCaptured ec: LifecycleHook // errorCaptured
} }
export type Slot = (...args: any[]) => VNode[]
export type Slots = Readonly<{
[name: string]: Slot
}>
export type ComponentInstance<P = Data, S = Data> = { export type ComponentInstance<P = Data, S = Data> = {
type: FunctionalComponent | ComponentOptions type: FunctionalComponent | ComponentOptions
vnode: VNode vnode: VNode

View File

@ -16,17 +16,25 @@ export type ComponentPropsOptions<P = Data> = {
[K in keyof P]: PropValidator<P[K]> [K in keyof P]: PropValidator<P[K]>
} }
export type Prop<T> = { (): T } | { new (...args: any[]): T & object } type Prop<T> = { (): T } | { new (...args: any[]): T & object }
export type PropType<T> = Prop<T> | Prop<T>[] type PropType<T> = Prop<T> | Prop<T>[]
export type PropValidator<T> = PropOptions<T> | PropType<T> type PropValidator<T> = PropOptions<T> | PropType<T>
export interface PropOptions<T = any> { interface PropOptions<T = any> {
type?: PropType<T> | true | null type?: PropType<T> | true | null
required?: boolean required?: boolean
default?: T | null | undefined | (() => T | null | undefined) default?: T | null | undefined | (() => T | null | undefined)
validator?(value: T): boolean validator?(value: any): boolean
}
export type ExtractPropTypes<PropOptions> = {
readonly [key in keyof PropOptions]: PropOptions[key] extends PropValidator<
infer V
>
? V
: PropOptions[key] extends null | true ? any : PropOptions[key]
} }
const enum BooleanFlags { const enum BooleanFlags {

View File

@ -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
}
}

View File

@ -34,6 +34,7 @@ import {
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' import { resolveProps } from './componentProps'
import { resolveSlots } from './componentSlots'
const prodEffectOptions = { const prodEffectOptions = {
scheduler: queueJob scheduler: queueJob
@ -201,7 +202,7 @@ export function createRenderer(options: RendererOptions) {
} }
if (isString(vnode.children)) { if (isString(vnode.children)) {
hostSetElementText(el, vnode.children) hostSetElementText(el, vnode.children)
} else if (vnode.children != null) { } else if (isArray(vnode.children)) {
mountChildren(vnode.children, el) mountChildren(vnode.children, el)
} }
hostInsert(el, container, anchor) hostInsert(el, container, anchor)
@ -382,7 +383,7 @@ export function createRenderer(options: RendererOptions) {
if (target != null) { if (target != null) {
if (isString(children)) { if (isString(children)) {
hostSetElementText(target, children) hostSetElementText(target, children)
} else if (children != null) { } else if (isArray(children)) {
mountChildren(children, target) mountChildren(children, target)
} }
} else { } else {
@ -407,7 +408,7 @@ export function createRenderer(options: RendererOptions) {
if (isString(children)) { if (isString(children)) {
hostSetElementText(target, '') hostSetElementText(target, '')
hostSetElementText(nextTarget, children) hostSetElementText(nextTarget, children)
} else if (children != null) { } else if (isArray(children)) {
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
move(children[i] as VNode, nextTarget, null) move(children[i] as VNode, nextTarget, null)
} }
@ -454,6 +455,7 @@ export function createRenderer(options: RendererOptions) {
// initial mount // initial mount
instance.vnode = vnode instance.vnode = vnode
resolveProps(instance, vnode.props, Component.props) resolveProps(instance, vnode.props, Component.props)
resolveSlots(instance, vnode.children)
// setup stateful // setup stateful
if (typeof Component === 'object') { if (typeof Component === 'object') {
setupStatefulComponent(instance) setupStatefulComponent(instance)
@ -479,7 +481,7 @@ export function createRenderer(options: RendererOptions) {
instance.vnode = next instance.vnode = next
instance.next = null instance.next = null
resolveProps(instance, next.props, Component.props) resolveProps(instance, next.props, Component.props)
// TODO slots resolveSlots(instance, next.children)
} }
const prevTree = instance.subTree const prevTree = instance.subTree
const nextTree = (instance.subTree = renderComponentRoot(instance)) const nextTree = (instance.subTree = renderComponentRoot(instance))
@ -551,7 +553,7 @@ export function createRenderer(options: RendererOptions) {
} else { } else {
if (isString(c1)) { if (isString(c1)) {
hostSetElementText(container, '') hostSetElementText(container, '')
if (c2 != null) { if (isArray(c2)) {
mountChildren(c2, container, anchor) mountChildren(c2, container, anchor)
} }
} else if (isArray(c1)) { } else if (isArray(c1)) {

View File

@ -10,13 +10,15 @@ export {
export { export {
ComponentOptions, ComponentOptions,
FunctionalComponent, FunctionalComponent,
Slots,
Slot,
createComponent createComponent
} from './component' } 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 './reactivity'
export * from './componentLifecycle'
export { createRenderer, RendererOptions } from './createRenderer'
export { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags'

View File

@ -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 { ComponentInstance } from './component'
import { HostNode } from './createRenderer' import { HostNode } from './createRenderer'
import { RawSlots } from './componentSlots'
export const Fragment = Symbol('Fragment') export const Fragment = Symbol('Fragment')
export const Text = Symbol('Text') export const Text = Symbol('Text')
@ -19,11 +20,13 @@ type VNodeChildAtom = VNode | string | number | null | void
export interface VNodeChildren extends Array<VNodeChildren | VNodeChildAtom> {} export interface VNodeChildren extends Array<VNodeChildren | VNodeChildAtom> {}
export type VNodeChild = VNodeChildAtom | VNodeChildren export type VNodeChild = VNodeChildAtom | VNodeChildren
export type NormalizedChildren = string | VNodeChildren | RawSlots | null
export interface VNode { export interface VNode {
type: VNodeTypes type: VNodeTypes
props: { [key: string]: any } | null props: { [key: string]: any } | null
key: string | number | null key: string | number | null
children: string | VNodeChildren | null children: NormalizedChildren
component: ComponentInstance | null component: ComponentInstance | null
// DOM // DOM
@ -91,7 +94,7 @@ export function createVNode(
type, type,
props, props,
key: props && props.key, key: props && props.key,
children: typeof children === 'number' ? children + '' : children, children: normalizeChildren(children),
component: null, component: null,
el: null, el: null,
anchor: null, anchor: null,
@ -127,10 +130,25 @@ export function normalizeVNode(child: VNodeChild): VNode {
// fragment // fragment
return createVNode(Fragment, null, child) return createVNode(Fragment, null, child)
} else if (typeof child === 'object') { } 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 return child as VNode
} else { } else {
// primitive types // primitive types
return createVNode(Text, null, child + '') 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 + ''
}
}