From e2edd62f663fd611e048cdcb0644a78e4d48adc6 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Tue, 5 Dec 2023 00:50:34 +0900 Subject: [PATCH 01/15] wip: component props --- packages/runtime-vapor/src/component.ts | 36 ++- packages/runtime-vapor/src/componentProps.ts | 270 +++++++++++++++++++ packages/runtime-vapor/src/render.ts | 30 ++- playground/src/main.ts | 21 +- playground/src/props.js | 94 +++++++ 5 files changed, 433 insertions(+), 18 deletions(-) create mode 100644 packages/runtime-vapor/src/componentProps.ts create mode 100644 playground/src/props.js diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index c6195f36e..da30df343 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -1,6 +1,18 @@ import { EffectScope } from '@vue/reactivity' +import { EMPTY_OBJ } from '@vue/shared' import { Block, BlockFn } from './render' import { DirectiveBinding } from './directives' +import { + ComponentPropsOptions, + NormalizedPropsOptions, + normalizePropsOptions, +} from './componentProps' + +// Conventional ConcreteComponent +export interface Component

{ + props?: ComponentPropsOptions

+ blockFn: BlockFn +} export interface ComponentInternalInstance { uid: number @@ -8,11 +20,17 @@ export interface ComponentInternalInstance { block: Block | null scope: EffectScope - component: BlockFn - isMounted: boolean + blockFn: BlockFn + propsOptions: NormalizedPropsOptions + + // state + props: Data /** directives */ dirs: Map + + // lifecycle + isMounted: boolean // TODO: registory of provides, appContext, lifecycles, ... } @@ -36,18 +54,26 @@ export interface ComponentPublicInstance {} let uid = 0 export const createComponentInstance = ( - component: BlockFn, + component: Component, ): ComponentInternalInstance => { const instance: ComponentInternalInstance = { uid: uid++, block: null, container: null!, // set on mount scope: new EffectScope(true /* detached */)!, + blockFn: component.blockFn, - component, - isMounted: false, + // resolved props and emits options + propsOptions: normalizePropsOptions(component), + // emitsOptions: normalizeEmitsOptions(type, appContext), // TODO: + + // state + props: EMPTY_OBJ, dirs: new Map(), + + // lifecycle hooks + isMounted: false, // TODO: registory of provides, appContext, lifecycles, ... } return instance diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts new file mode 100644 index 000000000..eb4c21bf8 --- /dev/null +++ b/packages/runtime-vapor/src/componentProps.ts @@ -0,0 +1,270 @@ +// NOTE: runtime-core/src/componentProps.ts + +import { + EMPTY_ARR, + EMPTY_OBJ, + camelize, + extend, + hasOwn, + hyphenate, + isArray, + isFunction, + isReservedProp, +} from '@vue/shared' +import { shallowReactive, toRaw } from '@vue/reactivity' +import { Component, ComponentInternalInstance, Data } from './component' + +export type ComponentPropsOptions

= + | ComponentObjectPropsOptions

+ | string[] + +export type ComponentObjectPropsOptions

= { + [K in keyof P]: Prop | null +} + +export type Prop = PropOptions | PropType + +type DefaultFactory = (props: Data) => T | null | undefined + +export interface PropOptions { + type?: PropType | true | null + required?: boolean + default?: D | DefaultFactory | null | undefined | object + validator?(value: unknown): boolean + /** + * @internal + */ + skipCheck?: boolean + /** + * @internal + */ + skipFactory?: boolean +} + +export type PropType = PropConstructor | PropConstructor[] + +type PropConstructor = + | { new (...args: any[]): T & {} } + | { (): T } + | PropMethod + +type PropMethod = [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 +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): string { + const match = ctor && ctor.toString().match(/^\s*(function|class) (\w+)/) + return match ? match[2] : ctor === null ? 'null' : '' +} + +function isSameType(a: Prop, b: Prop): boolean { + return getType(a) === getType(b) +} + +function getTypeIndex( + type: Prop, + expectedTypes: PropType | 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 +} diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index 82a3bfde7..c033d5518 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -6,10 +6,13 @@ import { } from '@vue/shared' import { + Component, ComponentInternalInstance, createComponentInstance, setCurrentInstance, + unsetCurrentInstance, } from './component' +import { initProps } from './componentProps' export type Block = Node | Fragment | Block[] export type ParentBlock = ParentNode | Node[] @@ -17,13 +20,15 @@ export type Fragment = { nodes: Block; anchor: Node } export type BlockFn = (props?: any) => Block export function render( - comp: BlockFn, + comp: Component, + props: any, container: string | ParentNode, ): ComponentInternalInstance { - const instance = createComponentInstance(comp) - setCurrentInstance(instance) - mountComponent(instance, (container = normalizeContainer(container))) - return instance + return mountComponent( + comp, + props, + (container = normalizeContainer(container)), + ) } export function normalizeContainer(container: string | ParentNode): ParentNode { @@ -33,18 +38,27 @@ export function normalizeContainer(container: string | ParentNode): ParentNode { } export const mountComponent = ( - instance: ComponentInternalInstance, + comp: Component, + props: any, container: ParentNode, -) => { +): ComponentInternalInstance => { + const instance = createComponentInstance(comp) + initProps(instance, props) + + setCurrentInstance(instance) instance.container = container const block = instance.scope.run( - () => (instance.block = instance.component()), + () => (instance.block = instance.blockFn(instance.props)), )! insert(block, instance.container) instance.isMounted = true + unsetCurrentInstance() + // TODO: lifecycle hooks (mounted, ...) // const { m } = instance // m && invoke(m) + + return instance } export const unmountComponent = (instance: ComponentInternalInstance) => { diff --git a/playground/src/main.ts b/playground/src/main.ts index 06b2c60ad..0c7a34b16 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -1,11 +1,22 @@ +import { extend } from '@vue/shared' import { render } from 'vue/vapor' -const modules = import.meta.glob('./*.vue') +const modules = import.meta.glob('./*.(vue|js)') const mod = (modules['.' + location.pathname] || modules['./App.vue'])() mod.then(({ default: m }) => { - render(() => { - const returned = m.setup?.({}, { expose() {} }) - return m.render(returned) - }, '#app') + render( + { + props: m.props, + blockFn: props => { + const returned = m.setup?.(props, { expose() {} }) + const ctx = extend(props, returned) // TODO: merge + return m.render(ctx) + } + }, + { + /* TODO: raw props */ + }, + '#app' + ) }) diff --git a/playground/src/props.js b/playground/src/props.js new file mode 100644 index 000000000..2aa692f84 --- /dev/null +++ b/playground/src/props.js @@ -0,0 +1,94 @@ +import { extend } from '@vue/shared' +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++ + } + return { count, handleClick } + }, + + render(_ctx) { + const t0 = template('') + const n0 = t0() + const { + 0: [n1] + } = children(n0) + on(n1, 'click', _ctx.handleClick) + effect(() => { + setText(n1, void 0, _ctx.count.value) + }) + + // TODO: create component fn? + // const c0 = createComponent(...) + // insert(n0, c0) + renderComponent( + { + props: child.props, + blockFn: props => { + const returned = child.setup?.(props, { expose() {} }) + const ctx = extend(props, returned) // TODO: merge + return child.render(ctx) + } + }, + // TODO: proxy?? + { + /* */ + get count() { + return _ctx.count.value + }, + + /* */ + get inlineDouble() { + return _ctx.count.value * 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) + ) + }, + + render(_ctx) { + const t0 = template('

') + const n0 = t0() + const { + 0: [n1] + } = children(n0) + effect(() => { + setText(n1, void 0, _ctx.count + ' * 2 = ' + _ctx.inlineDouble) + }) + return n0 + } +} From 79aa1d9d38048390409d68080273888ab4f9800e Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Thu, 7 Dec 2023 23:59:15 +0900 Subject: [PATCH 02/15] refactor(runtime-vapor): remove dead console.log --- packages/runtime-vapor/src/render.ts | 5 ----- playground/src/props.js | 1 - 2 files changed, 6 deletions(-) diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index c1c8c6615..55c09c796 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -68,11 +68,6 @@ export function mountComponent( const setupFn = typeof component === 'function' ? component : component.setup - console.log( - '🚀 ~ file: render.ts:70 ~ block ~ setupFn:', - component, - setupFn, - ) const state = setupFn(props, ctx) diff --git a/playground/src/props.js b/playground/src/props.js index 4ac1750cc..be43c6dd2 100644 --- a/playground/src/props.js +++ b/playground/src/props.js @@ -1,4 +1,3 @@ -import { extend } from '@vue/shared' import { watch } from 'vue' import { children, From b6e699a1d11f6b188701bd3fdbc8058ff52eeff8 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Fri, 8 Dec 2023 00:17:43 +0900 Subject: [PATCH 03/15] feat(runtime-vapor): componentPublicInstance --- packages/runtime-vapor/src/component.ts | 7 ++++ .../src/componentPublicInstance.ts | 22 +++++++++++ packages/runtime-vapor/src/render.ts | 37 ++++--------------- playground/src/props.js | 6 +-- 4 files changed, 40 insertions(+), 32 deletions(-) create mode 100644 packages/runtime-vapor/src/componentPublicInstance.ts diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 972210309..ad9f116e4 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -31,8 +31,12 @@ export interface ComponentInternalInstance { component: FunctionalComponent | ObjectComponent propsOptions: NormalizedPropsOptions + // TODO: type + proxy: Data | null + // state props: Data + setupState: Data /** directives */ dirs: Map @@ -71,8 +75,11 @@ export const createComponentInstance = ( propsOptions: normalizePropsOptions(component), // emitsOptions: normalizeEmitsOptions(type, appContext), // TODO: + proxy: null, + // state props: EMPTY_OBJ, + setupState: EMPTY_OBJ, dirs: new Map(), diff --git a/packages/runtime-vapor/src/componentPublicInstance.ts b/packages/runtime-vapor/src/componentPublicInstance.ts new file mode 100644 index 000000000..8bfacf981 --- /dev/null +++ b/packages/runtime-vapor/src/componentPublicInstance.ts @@ -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 = { + 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] + } + }, +} diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index 55c09c796..9d0a08d8e 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -1,5 +1,5 @@ -import { reactive } from '@vue/reactivity' -import { Data, extend } from '@vue/shared' +import { markRaw, proxyRefs } from '@vue/reactivity' +import { Data } from '@vue/shared' import { type Component, @@ -13,6 +13,7 @@ import { initProps } from './componentProps' import { invokeDirectiveHook } from './directives' import { insert, remove } from './dom' +import { PublicInstanceProxyHandlers } from './componentPublicInstance' export type Block = Node | Fragment | Block[] export type ParentBlock = ParentNode | Node[] @@ -35,26 +36,6 @@ export function normalizeContainer(container: string | ParentNode): ParentNode { : container } -// export const mountComponent = ( -// comp: Component, -// props: any, -// container: ParentNode, -// ): ComponentInternalInstance => { -// const instance = createComponentInstance(comp) -// initProps(instance, props) - -// setCurrentInstance(instance) -// instance.container = container -// const block = instance.scope.run( -// () => (instance.block = instance.blockFn(instance.props)), -// )! -// insert(block, instance.container) -// instance.isMounted = true -// unsetCurrentInstance() - -// return instance -// } - export function mountComponent( instance: ComponentInternalInstance, container: ParentNode, @@ -70,14 +51,12 @@ export function mountComponent( 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( - // TODO: merge - extend(props, state), - ), - )) + instance.setupState = proxyRefs(state) + return (instance.block = component.render(instance.proxy)) } else { return (instance.block = state as Block) } diff --git a/playground/src/props.js b/playground/src/props.js index be43c6dd2..b80768dcc 100644 --- a/playground/src/props.js +++ b/playground/src/props.js @@ -36,7 +36,7 @@ export default { } = children(n0) on(n1, 'click', _ctx.handleClick) effect(() => { - setText(n1, void 0, _ctx.count.value) + setText(n1, void 0, _ctx.count) }) // TODO: create component fn? @@ -49,12 +49,12 @@ export default { { /* */ get count() { - return _ctx.count.value + return _ctx.count }, /* */ get inlineDouble() { - return _ctx.count.value * 2 + return _ctx.count * 2 } }, n0 From 2460435eaedbb16ebc9bbbf7739e2324e29a08ad Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Fri, 8 Dec 2023 00:36:23 +0900 Subject: [PATCH 04/15] chore(runtime-vapor): fix type import --- packages/runtime-vapor/src/component.ts | 6 +++--- packages/runtime-vapor/src/componentProps.ts | 2 +- packages/runtime-vapor/src/render.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index ad9f116e4..608acc6a1 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -2,10 +2,10 @@ import { EffectScope } from '@vue/reactivity' import { EMPTY_OBJ } from '@vue/shared' import { Block } from './render' -import { DirectiveBinding } from './directives' +import { type DirectiveBinding } from './directives' import { - ComponentPropsOptions, - NormalizedPropsOptions, + type ComponentPropsOptions, + type NormalizedPropsOptions, normalizePropsOptions, } from './componentProps' import type { Data } from '@vue/shared' diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index 693de1378..c12ce1f13 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -13,7 +13,7 @@ import { isReservedProp, } from '@vue/shared' import { shallowReactive, toRaw } from '@vue/reactivity' -import { ComponentInternalInstance, Component } from './component' +import { type ComponentInternalInstance, type Component } from './component' export type ComponentPropsOptions

= | ComponentObjectPropsOptions

diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index 9d0a08d8e..bc1756363 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -1,5 +1,5 @@ import { markRaw, proxyRefs } from '@vue/reactivity' -import { Data } from '@vue/shared' +import { type Data } from '@vue/shared' import { type Component, From 551e5c10301822736e017ab18cb6637fc7407665 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Fri, 8 Dec 2023 19:43:30 +0900 Subject: [PATCH 05/15] chore(runtime-vapor): remove props type check option --- packages/runtime-vapor/src/componentProps.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index c12ce1f13..5cd0f1d21 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -32,10 +32,6 @@ export interface PropOptions { required?: boolean default?: D | DefaultFactory | null | undefined | object validator?(value: unknown): boolean - /** - * @internal - */ - skipCheck?: boolean /** * @internal */ From 7bcb160c0253c4464a87896b96590e592cc10a69 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Fri, 8 Dec 2023 19:45:36 +0900 Subject: [PATCH 06/15] chore(runtime-vapor): remove dead props --- packages/runtime-vapor/src/component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index ac8746f4d..5ff3b86b8 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -45,7 +45,6 @@ export interface ComponentInternalInstance { // lifecycle get isMounted(): boolean isMountedRef: Ref - isMounted: boolean // TODO: registory of provides, appContext, lifecycles, ... } From 4b55722c0c5fb689459f16900dcaebfb7a39c2f9 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Sat, 9 Dec 2023 00:49:28 +0900 Subject: [PATCH 07/15] feat(runtime-vapor): component emits --- packages/runtime-vapor/src/component.ts | 39 +++++++- packages/runtime-vapor/src/componentEmits.ts | 95 +++++++++++++++++++ .../src/componentPublicInstance.ts | 2 + packages/runtime-vapor/src/render.ts | 7 +- playground/src/emits.js | 85 +++++++++++++++++ 5 files changed, 222 insertions(+), 6 deletions(-) create mode 100644 packages/runtime-vapor/src/componentEmits.ts create mode 100644 playground/src/emits.js diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 5ff3b86b8..fb068539e 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -1,6 +1,7 @@ import { EffectScope, Ref, ref } from '@vue/reactivity' - +import type { Data } from '@vue/shared' import { EMPTY_OBJ } from '@vue/shared' + import { Block } from './render' import { type DirectiveBinding } from './directive' import { @@ -9,17 +10,25 @@ import { normalizePropsOptions, } from './componentProps' -import type { Data } from '@vue/shared' +import { + type EmitFn, + type EmitsOptions, + type ObjectEmitsOptions, + emit, + normalizeEmitsOptions, +} from './componentEmits' export type Component = FunctionalComponent | ObjectComponent export type SetupFn = (props: any, ctx: any) => Block | Data export type FunctionalComponent = SetupFn & { props: ComponentPropsOptions + emits: EmitsOptions render(ctx: any): Block } export interface ObjectComponent { props: ComponentPropsOptions + emits: EmitsOptions setup: SetupFn render(ctx: any): Block } @@ -29,8 +38,14 @@ export interface ComponentInternalInstance { container: ParentNode block: Block | null scope: EffectScope + + // conventional vnode.type component: FunctionalComponent | ObjectComponent + rawProps: Data + + // normalized options propsOptions: NormalizedPropsOptions + emitsOptions: ObjectEmitsOptions | null // TODO: type proxy: Data | null @@ -38,6 +53,8 @@ export interface ComponentInternalInstance { // state props: Data setupState: Data + emit: EmitFn + emitted: Record | null /** directives */ dirs: Map @@ -45,6 +62,8 @@ export interface ComponentInternalInstance { // lifecycle get isMounted(): boolean isMountedRef: Ref + get isUnmounted(): boolean + isUnmountedRef: Ref // TODO: registory of provides, appContext, lifecycles, ... } @@ -65,18 +84,25 @@ export const unsetCurrentInstance = () => { let uid = 0 export const createComponentInstance = ( component: ObjectComponent | FunctionalComponent, + rawProps: Data, ): ComponentInternalInstance => { const isMountedRef = ref(false) + const isUnmountedRef = ref(false) const instance: ComponentInternalInstance = { uid: uid++, block: null, container: null!, // set on mount scope: new EffectScope(true /* detached */)!, component, + rawProps, // resolved props and emits options propsOptions: normalizePropsOptions(component), - // emitsOptions: normalizeEmitsOptions(type, appContext), // TODO: + emitsOptions: normalizeEmitsOptions(component), + + // emit + emit: null!, // to be set immediately + emitted: null, proxy: null, @@ -91,7 +117,14 @@ export const createComponentInstance = ( return isMountedRef.value }, isMountedRef, + get isUnmounted() { + return isUnmountedRef.value + }, + isUnmountedRef, // TODO: registory of provides, appContext, lifecycles, ... } + + instance.emit = emit.bind(null, instance) + return instance } diff --git a/packages/runtime-vapor/src/componentEmits.ts b/packages/runtime-vapor/src/componentEmits.ts new file mode 100644 index 000000000..fcd863e2f --- /dev/null +++ b/packages/runtime-vapor/src/componentEmits.ts @@ -0,0 +1,95 @@ +// NOTE: runtime-core/src/componentEmits.ts + +import { + type UnionToIntersection, + camelize, + extend, + hyphenate, + isArray, + isFunction, + toHandlerKey, +} from '@vue/shared' +import { type Component, type ComponentInternalInstance } from './component' + +export type ObjectEmitsOptions = Record< + string, + ((...args: any[]) => any) | null +> + +export type EmitsOptions = ObjectEmitsOptions | string[] + +export type EmitFn< + Options = ObjectEmitsOptions, + Event extends keyof Options = keyof Options, +> = Options extends Array + ? (event: V, ...args: any[]) => void + : {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function + ? (event: string, ...args: any[]) => void + : UnionToIntersection< + { + [key in Event]: Options[key] extends (...args: infer Args) => any + ? (event: key, ...args: Args) => void + : (event: key, ...args: any[]) => void + }[Event] + > + +export function emit( + instance: ComponentInternalInstance, + event: string, + ...rawArgs: any[] +) { + if (instance.isUnmounted) return + const props = instance.rawProps // TODO: raw props + + let args = rawArgs + + // TODO: modelListener + + let handlerName + let handler = + props[(handlerName = toHandlerKey(event))] || + // also try camelCase event handler (#2249) + props[(handlerName = toHandlerKey(camelize(event)))] + // for v-model update:xxx events, also trigger kebab-case equivalent + // for props passed via kebab-case + if (!handler) { + handler = props[(handlerName = toHandlerKey(hyphenate(event)))] + } + + if (handler && isFunction(handler)) { + // TODO: callWithAsyncErrorHandling + handler(...args) + } + + const onceHandler = props[handlerName + `Once`] + if (onceHandler) { + if (!instance.emitted) { + instance.emitted = {} + } else if (instance.emitted[handlerName]) { + return + } + + if (isFunction(onceHandler)) { + instance.emitted[handlerName] = true + // TODO: callWithAsyncErrorHandling + onceHandler(...args) + } + } +} + +export function normalizeEmitsOptions( + comp: Component, +): ObjectEmitsOptions | null { + // TODO: caching? + + const raw = comp.emits + let normalized: ObjectEmitsOptions = {} + + if (isArray(raw)) { + raw.forEach((key) => (normalized[key] = null)) + } else { + extend(normalized, raw) + } + + return normalized +} diff --git a/packages/runtime-vapor/src/componentPublicInstance.ts b/packages/runtime-vapor/src/componentPublicInstance.ts index 8bfacf981..2adcc3402 100644 --- a/packages/runtime-vapor/src/componentPublicInstance.ts +++ b/packages/runtime-vapor/src/componentPublicInstance.ts @@ -20,3 +20,5 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { } }, } + +// TODO: publicPropertiesMap diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index 422d5e689..62fb595aa 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -26,7 +26,7 @@ export function render( props: Data, container: string | ParentNode, ): ComponentInternalInstance { - const instance = createComponentInstance(comp) + const instance = createComponentInstance(comp, props) initProps(instance, props) return mountComponent(instance, (container = normalizeContainer(container))) } @@ -45,8 +45,8 @@ export function mountComponent( setCurrentInstance(instance) const block = instance.scope.run(() => { - const { component, props } = instance - const ctx = { expose: () => {} } + const { component, props, emit } = instance + const ctx = { emit, expose: () => {} } const setupFn = typeof component === 'function' ? component : component.setup @@ -82,6 +82,7 @@ export function unmountComponent(instance: ComponentInternalInstance) { scope.stop() block && remove(block, container) instance.isMountedRef.value = false + instance.isUnmountedRef.value = true invokeDirectiveHook(instance, 'unmounted') unsetCurrentInstance() diff --git a/playground/src/emits.js b/playground/src/emits.js new file mode 100644 index 000000000..8f4b389cc --- /dev/null +++ b/playground/src/emits.js @@ -0,0 +1,85 @@ +import { + children, + on, + ref, + template, + effect, + setText, + render as renderComponent // TODO: +} from '@vue/vapor' + +export default { + props: undefined, + + setup(_, {}) { + const count = ref(1) + const setCount = v => { + count.value = v + } + + const __returned__ = { count, setCount } + + Object.defineProperty(__returned__, '__isScriptSetup', { + enumerable: false, + value: true + }) + + return __returned__ + }, + + render(_ctx) { + const t0 = template('

') + const n0 = t0() + const { + 0: [n1] + } = children(n0) + + effect(() => { + setText(n1, void 0, _ctx.count) + }) + + renderComponent( + child, + { + get count() { + return _ctx.count + }, + 'onClick:child': _ctx.setCount + }, + n0 + ) + + return n0 + } +} + +const child = { + props: { + count: { type: Number, default: 1 } + }, + + setup(props, { emit }) { + const handleClick = () => { + emit('click:child', props.count * 2) + } + + const __returned__ = { handleClick } + + Object.defineProperty(__returned__, '__isScriptSetup', { + enumerable: false, + value: true + }) + + return __returned__ + }, + + render(_ctx) { + const t0 = template('') + const n0 = t0() + const { + 0: [n1] + } = children(n0) + on(n1, 'click', _ctx.handleClick) + return n0 + } +} From 123108f54ff2b7719c6365d54343c9fd046533c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Sat, 9 Dec 2023 03:24:25 +0800 Subject: [PATCH 08/15] refactor --- packages/runtime-vapor/src/component.ts | 4 +--- packages/runtime-vapor/src/componentProps.ts | 5 +++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 5ff3b86b8..3cdffd04c 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -1,14 +1,12 @@ 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 { DirectiveBinding } from './directive' import type { Data } from '@vue/shared' export type Component = FunctionalComponent | ObjectComponent diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index 5cd0f1d21..0b7e42a6e 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -1,7 +1,7 @@ // NOTE: runtime-core/src/componentProps.ts import { - Data, + type Data, EMPTY_ARR, EMPTY_OBJ, camelize, @@ -13,7 +13,7 @@ import { isReservedProp, } from '@vue/shared' import { shallowReactive, toRaw } from '@vue/reactivity' -import { type ComponentInternalInstance, type Component } from './component' +import type { ComponentInternalInstance, Component } from './component' export type ComponentPropsOptions

= | ComponentObjectPropsOptions

@@ -77,6 +77,7 @@ export function initProps( if (rawProps) { for (let key in rawProps) { // key, ref are reserved and never passed down + // TODO: remove vnode if (isReservedProp(key)) { continue } From 461c283f16ad56effd19a1c664e352f036ca21e2 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Sun, 10 Dec 2023 13:16:33 +0900 Subject: [PATCH 09/15] feat(vapor-playground): emit once --- playground/src/emits.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/playground/src/emits.js b/playground/src/emits.js index 8f4b389cc..bdecb9de1 100644 --- a/playground/src/emits.js +++ b/playground/src/emits.js @@ -49,6 +49,17 @@ export default { n0 ) + renderComponent( + child, + { + get count() { + return _ctx.count + }, + 'onClick:childOnce': _ctx.setCount + }, + n0 + ) + return n0 } } From 095c3db2f017a3922b3043293fa48131cd44c836 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Sun, 10 Dec 2023 13:26:16 +0900 Subject: [PATCH 10/15] chore(runtime-vapor): add todo comment --- packages/runtime-vapor/src/componentEmits.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-vapor/src/componentEmits.ts b/packages/runtime-vapor/src/componentEmits.ts index fcd863e2f..d8afcd825 100644 --- a/packages/runtime-vapor/src/componentEmits.ts +++ b/packages/runtime-vapor/src/componentEmits.ts @@ -13,7 +13,7 @@ import { type Component, type ComponentInternalInstance } from './component' export type ObjectEmitsOptions = Record< string, - ((...args: any[]) => any) | null + ((...args: any[]) => any) | null // TODO: call validation? > export type EmitsOptions = ObjectEmitsOptions | string[] From 64a50c0a714d93135b0e7880af346b2b3eca7f27 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Sun, 10 Dec 2023 13:29:27 +0900 Subject: [PATCH 11/15] feat(vapor-playground): props default --- playground/src/props.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/playground/src/props.js b/playground/src/props.js index b80768dcc..6b7c4ab3c 100644 --- a/playground/src/props.js +++ b/playground/src/props.js @@ -60,14 +60,28 @@ export default { n0 ) + // test default value + renderComponent( + child, + { + get count() { + return _ctx.count % 2 === 0 ? _ctx.count : undefined + }, + get inlineDouble() { + return _ctx.count % 2 === 0 ? undefined : _ctx.count * 2 + } + }, + n0 + ) + return n0 } } const child = { props: { - count: { type: Number, default: 1 }, - inlineDouble: { type: Number, default: 2 } + count: { type: Number, default: 'opps!' }, + inlineDouble: { type: Number, default: 'opps!' } }, setup(props) { From d8bd68a2fd49f8633f4cb995d0d9d3041646d12f Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Wed, 13 Dec 2023 23:45:44 +0900 Subject: [PATCH 12/15] fix: define getter of emit handler --- playground/src/emits.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/playground/src/emits.js b/playground/src/emits.js index bdecb9de1..bc8e8022b 100644 --- a/playground/src/emits.js +++ b/playground/src/emits.js @@ -44,7 +44,9 @@ export default { get count() { return _ctx.count }, - 'onClick:child': _ctx.setCount + get ['onClick:child']() { + return _ctx.setCount + } }, n0 ) @@ -55,7 +57,9 @@ export default { get count() { return _ctx.count }, - 'onClick:childOnce': _ctx.setCount + get ['onClick:childOnce']() { + return _ctx.setCount + } }, n0 ) From 7931cf65c5f3b29e9bb731b82f94d0042d8f6833 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Tue, 26 Dec 2023 00:51:47 +0900 Subject: [PATCH 13/15] fix: conflict --- playground/src/emits.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playground/src/emits.js b/playground/src/emits.js index bc8e8022b..ca001bd13 100644 --- a/playground/src/emits.js +++ b/playground/src/emits.js @@ -3,7 +3,7 @@ import { on, ref, template, - effect, + watchEffect, setText, render as renderComponent // TODO: } from '@vue/vapor' @@ -34,7 +34,7 @@ export default { 0: [n1] } = children(n0) - effect(() => { + watchEffect(() => { setText(n1, void 0, _ctx.count) }) From 76418ca03e1f33032d71e47bfbb254fdfe9f1115 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Sun, 31 Dec 2023 13:27:58 +0900 Subject: [PATCH 14/15] fix: lint --- packages/runtime-vapor/src/componentEmits.ts | 2 +- .../src/componentPublicInstance.ts | 2 +- playground/src/emits.js | 26 +++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/runtime-vapor/src/componentEmits.ts b/packages/runtime-vapor/src/componentEmits.ts index d8afcd825..c1e55b06e 100644 --- a/packages/runtime-vapor/src/componentEmits.ts +++ b/packages/runtime-vapor/src/componentEmits.ts @@ -9,7 +9,7 @@ import { isFunction, toHandlerKey, } from '@vue/shared' -import { type Component, type ComponentInternalInstance } from './component' +import type { Component, ComponentInternalInstance } from './component' export type ObjectEmitsOptions = Record< string, diff --git a/packages/runtime-vapor/src/componentPublicInstance.ts b/packages/runtime-vapor/src/componentPublicInstance.ts index 2adcc3402..5589dbbb2 100644 --- a/packages/runtime-vapor/src/componentPublicInstance.ts +++ b/packages/runtime-vapor/src/componentPublicInstance.ts @@ -1,5 +1,5 @@ import { hasOwn } from '@vue/shared' -import { type ComponentInternalInstance } from './component' +import type { ComponentInternalInstance } from './component' export interface ComponentRenderContext { [key: string]: any diff --git a/playground/src/emits.js b/playground/src/emits.js index ca001bd13..4d019fc1a 100644 --- a/playground/src/emits.js +++ b/playground/src/emits.js @@ -2,10 +2,10 @@ import { children, on, ref, + render as renderComponent, // TODO: + setText, template, watchEffect, - setText, - render as renderComponent // TODO: } from '@vue/vapor' export default { @@ -21,7 +21,7 @@ export default { Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, - value: true + value: true, }) return __returned__ @@ -31,7 +31,7 @@ export default { const t0 = template('

') const n0 = t0() const { - 0: [n1] + 0: [n1], } = children(n0) watchEffect(() => { @@ -46,9 +46,9 @@ export default { }, get ['onClick:child']() { return _ctx.setCount - } + }, }, - n0 + n0, ) renderComponent( @@ -59,18 +59,18 @@ export default { }, get ['onClick:childOnce']() { return _ctx.setCount - } + }, }, - n0 + n0, ) return n0 - } + }, } const child = { props: { - count: { type: Number, default: 1 } + count: { type: Number, default: 1 }, }, setup(props, { emit }) { @@ -82,7 +82,7 @@ const child = { Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, - value: true + value: true, }) return __returned__ @@ -92,9 +92,9 @@ const child = { const t0 = template('') const n0 = t0() const { - 0: [n1] + 0: [n1], } = children(n0) on(n1, 'click', _ctx.handleClick) return n0 - } + }, } From 610450d82404b0e814d2263c81b473ec6d24a7f8 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Fri, 5 Jan 2024 02:22:16 +0900 Subject: [PATCH 15/15] chore: update readme (Codes Copied From `runtime-core`) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d58a74bb2..c314d6db7 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,9 @@ The code provided here is a duplicate from `runtime-core` as Vapor cannot import - packages/runtime-vapor/src/apiWatch.ts - packages/runtime-vapor/src/component.ts +- packages/runtime-vapor/src/componentEmits.ts - packages/runtime-vapor/src/componentProps.ts +- packages/runtime-vapor/src/componentPublicInstance.ts - packages/runtime-vapor/src/enums.ts - packages/runtime-vapor/src/errorHandling.ts - packages/runtime-vapor/src/scheduler.ts