From 093fbd9236cd3eaa976ddb3c7db5ad2f05538f2e Mon Sep 17 00:00:00 2001 From: GaoNeng-wWw Date: Sat, 9 Dec 2023 11:05:50 +0800 Subject: [PATCH 01/20] feat(runtime-vapor): onMounted and onUnMounted hook --- packages/runtime-vapor/src/apiLifecycle.ts | 65 +++++++++++ packages/runtime-vapor/src/component.ts | 124 ++++++++++++++++++++- packages/runtime-vapor/src/index.ts | 1 + packages/runtime-vapor/src/render.ts | 12 +- 4 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 packages/runtime-vapor/src/apiLifecycle.ts diff --git a/packages/runtime-vapor/src/apiLifecycle.ts b/packages/runtime-vapor/src/apiLifecycle.ts new file mode 100644 index 000000000..299f41342 --- /dev/null +++ b/packages/runtime-vapor/src/apiLifecycle.ts @@ -0,0 +1,65 @@ +import { pauseTracking, resetTracking } from '@vue/reactivity' +import { + ComponentInternalInstance, + currentInstance, + setCurrentInstance, +} from './component' + +export enum LifecycleHooks { + BEFORE_CREATE = 'bc', + CREATED = 'c', + BEFORE_MOUNT = 'bm', + MOUNTED = 'm', + BEFORE_UPDATE = 'bu', + UPDATED = 'u', + BEFORE_UNMOUNT = 'bum', + UNMOUNTED = 'um', + DEACTIVATED = 'da', + ACTIVATED = 'a', + RENDER_TRIGGERED = 'rtg', + RENDER_TRACKED = 'rtc', + ERROR_CAPTURED = 'ec', + SERVER_PREFETCH = 'sp', +} +export const injectHook = ( + type: LifecycleHooks, + hook: Function & { __weh?: Function }, + target: ComponentInternalInstance | null = currentInstance, + prepend: boolean = false, +) => { + if (target) { + const hooks = target[type] || (target[type] = []) + const wrappedHook = + hook.__weh || + (hook.__weh = (...args: unknown[]) => { + if (target.isUnMounted) { + return + } + pauseTracking() + setCurrentInstance(target) + // TODO: call error handling + const res = hook(...args) + resetTracking() + return res + }) + if (prepend) { + hooks.unshift(wrappedHook) + } else { + hooks.push(wrappedHook) + } + return wrappedHook + } else if (__DEV__) { + // TODO: warn need + } +} +export const createHook = + any>(lifecycle: LifecycleHooks) => + (hook: T, target: ComponentInternalInstance | null = currentInstance) => + injectHook(lifecycle, (...args: unknown[]) => hook(...args), target) + +export const onMounted = createHook(LifecycleHooks.MOUNTED) +export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT) +export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE) +export const onUpdated = createHook(LifecycleHooks.UPDATED) +export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT) +export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 26518598d..e427d8aa0 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -2,6 +2,7 @@ import { type Ref, EffectScope, ref } from '@vue/reactivity' import type { Block } from './render' import type { DirectiveBinding } from './directive' import type { Data } from '@vue/shared' +import { LifecycleHooks } from './apiLifecycle' export type SetupFn = (props: any, ctx: any) => Block | Data export type FunctionalComponent = SetupFn & { @@ -11,7 +12,7 @@ export interface ObjectComponent { setup: SetupFn render(ctx: any): Block } - +type LifecycleHook = TFn[] | null export interface ComponentInternalInstance { uid: number container: ParentNode @@ -20,11 +21,69 @@ export interface ComponentInternalInstance { component: FunctionalComponent | ObjectComponent get isMounted(): boolean + get isUnMounted(): boolean isMountedRef: Ref + isUnMountedRef: Ref /** directives */ dirs: Map // TODO: registory of provides, appContext, lifecycles, ... + /** + * @internal + */ + [LifecycleHooks.BEFORE_CREATE]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.CREATED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.BEFORE_MOUNT]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.MOUNTED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.BEFORE_UPDATE]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.UPDATED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.UNMOUNTED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.RENDER_TRACKED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.RENDER_TRIGGERED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.ACTIVATED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.DEACTIVATED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.ERROR_CAPTURED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise> } // TODO @@ -46,20 +105,81 @@ export const createComponentInstance = ( component: ObjectComponent | FunctionalComponent, ): ComponentInternalInstance => { const isMountedRef = ref(false) + const isUnMountedRef = ref(false) const instance: ComponentInternalInstance = { uid: uid++, block: null, - container: null!, // set on mount + container: null!, scope: new EffectScope(true /* detached */)!, component, get isMounted() { return isMountedRef.value }, + get isUnMounted() { + return isUnMountedRef.value + }, isMountedRef, + isUnMountedRef, dirs: new Map(), // TODO: registory of provides, appContext, lifecycles, ... + /** + * @internal + */ + [LifecycleHooks.BEFORE_CREATE]: null, + /** + * @internal + */ + [LifecycleHooks.CREATED]: null, + /** + * @internal + */ + [LifecycleHooks.BEFORE_MOUNT]: null, + /** + * @internal + */ + [LifecycleHooks.MOUNTED]: null, + /** + * @internal + */ + [LifecycleHooks.BEFORE_UPDATE]: null, + /** + * @internal + */ + [LifecycleHooks.UPDATED]: null, + /** + * @internal + */ + [LifecycleHooks.BEFORE_UNMOUNT]: null, + /** + * @internal + */ + [LifecycleHooks.UNMOUNTED]: null, + /** + * @internal + */ + [LifecycleHooks.RENDER_TRACKED]: null, + /** + * @internal + */ + [LifecycleHooks.RENDER_TRIGGERED]: null, + /** + * @internal + */ + [LifecycleHooks.ACTIVATED]: null, + /** + * @internal + */ + [LifecycleHooks.DEACTIVATED]: null, + /** + * @internal + */ + [LifecycleHooks.ERROR_CAPTURED]: null, + /** + * @internal + */ + [LifecycleHooks.SERVER_PREFETCH]: null, } return instance } diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 0c56476ad..69cca595a 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -44,3 +44,4 @@ export * from './scheduler' export * from './directive' export * from './dom' export * from './directives/vShow' +export * from './apiLifecycle' diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index b03e56ae8..defef7a1e 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -9,6 +9,7 @@ import { } from './component' import { invokeDirectiveHook } from './directive' import { insert, remove } from './dom' +import { invokeArrayFns } from '@vue/shared' export type Block = Node | Fragment | Block[] export type ParentBlock = ParentNode | Node[] @@ -30,7 +31,6 @@ export function normalizeContainer(container: string | ParentNode): ParentNode { ? (document.querySelector(container) as ParentNode) : container } - export function mountComponent( instance: ComponentInternalInstance, container: ParentNode, @@ -58,7 +58,10 @@ export function mountComponent( insert(block, instance.container) instance.isMountedRef.value = true invokeDirectiveHook(instance, 'mounted') - + const { m } = instance + if (m) { + invokeArrayFns(m) + } // TODO: lifecycle hooks (mounted, ...) // const { m } = instance // m && invoke(m) @@ -73,7 +76,10 @@ export function unmountComponent(instance: ComponentInternalInstance) { instance.isMountedRef.value = false invokeDirectiveHook(instance, 'unmounted') unsetCurrentInstance() - + const { um } = instance + if (um) { + invokeArrayFns(um) + } // TODO: lifecycle hooks (unmounted, ...) // const { um } = instance // um && invoke(um) From 719601bb52530b1ca4dcbbb8db7f25d84d4809fd Mon Sep 17 00:00:00 2001 From: GaoNeng-wWw Date: Sat, 9 Dec 2023 12:49:28 +0800 Subject: [PATCH 02/20] styles: Revision review comments --- packages/runtime-vapor/src/apiLifecycle.ts | 2 +- packages/runtime-vapor/src/render.ts | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/runtime-vapor/src/apiLifecycle.ts b/packages/runtime-vapor/src/apiLifecycle.ts index 299f41342..a92ab8162 100644 --- a/packages/runtime-vapor/src/apiLifecycle.ts +++ b/packages/runtime-vapor/src/apiLifecycle.ts @@ -1,6 +1,6 @@ import { pauseTracking, resetTracking } from '@vue/reactivity' import { - ComponentInternalInstance, + type ComponentInternalInstance, currentInstance, setCurrentInstance, } from './component' diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index defef7a1e..43ce89dca 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -62,9 +62,6 @@ export function mountComponent( if (m) { invokeArrayFns(m) } - // TODO: lifecycle hooks (mounted, ...) - // const { m } = instance - // m && invoke(m) } export function unmountComponent(instance: ComponentInternalInstance) { @@ -80,7 +77,4 @@ export function unmountComponent(instance: ComponentInternalInstance) { if (um) { invokeArrayFns(um) } - // TODO: lifecycle hooks (unmounted, ...) - // const { um } = instance - // um && invoke(um) } From 4bc650bb74eaa420a6a534999438c8278c675d21 Mon Sep 17 00:00:00 2001 From: GaoNeng-wWw Date: Sat, 9 Dec 2023 12:53:23 +0800 Subject: [PATCH 03/20] fix: UnMounted -> Unmounted --- packages/runtime-vapor/src/apiLifecycle.ts | 2 +- packages/runtime-vapor/src/component.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/runtime-vapor/src/apiLifecycle.ts b/packages/runtime-vapor/src/apiLifecycle.ts index a92ab8162..f03ae6f93 100644 --- a/packages/runtime-vapor/src/apiLifecycle.ts +++ b/packages/runtime-vapor/src/apiLifecycle.ts @@ -32,7 +32,7 @@ export const injectHook = ( const wrappedHook = hook.__weh || (hook.__weh = (...args: unknown[]) => { - if (target.isUnMounted) { + if (target.isUnmounted) { return } pauseTracking() diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index e427d8aa0..31387ad34 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -21,9 +21,9 @@ export interface ComponentInternalInstance { component: FunctionalComponent | ObjectComponent get isMounted(): boolean - get isUnMounted(): boolean + get isUnmounted(): boolean isMountedRef: Ref - isUnMountedRef: Ref + isUnmountedRef: Ref /** directives */ dirs: Map @@ -105,7 +105,7 @@ export const createComponentInstance = ( component: ObjectComponent | FunctionalComponent, ): ComponentInternalInstance => { const isMountedRef = ref(false) - const isUnMountedRef = ref(false) + const isUnmountedRef = ref(false) const instance: ComponentInternalInstance = { uid: uid++, block: null, @@ -116,11 +116,11 @@ export const createComponentInstance = ( get isMounted() { return isMountedRef.value }, - get isUnMounted() { - return isUnMountedRef.value + get isUnmounted() { + return isUnmountedRef.value }, isMountedRef, - isUnMountedRef, + isUnmountedRef, dirs: new Map(), // TODO: registory of provides, appContext, lifecycles, ... From c30102012681a435b757ce191613f5015d616ce9 Mon Sep 17 00:00:00 2001 From: GaoNeng-wWw Date: Sat, 9 Dec 2023 13:37:02 +0800 Subject: [PATCH 04/20] fix(runtime-vapor): name conflict for enum LifecycleHooks error --- packages/runtime-vapor/src/apiLifecycle.ts | 19 +++---- packages/runtime-vapor/src/component.ts | 58 +++++++++++----------- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/packages/runtime-vapor/src/apiLifecycle.ts b/packages/runtime-vapor/src/apiLifecycle.ts index f03ae6f93..732884ace 100644 --- a/packages/runtime-vapor/src/apiLifecycle.ts +++ b/packages/runtime-vapor/src/apiLifecycle.ts @@ -5,7 +5,7 @@ import { setCurrentInstance, } from './component' -export enum LifecycleHooks { +export enum VaporLifecycleHooks { BEFORE_CREATE = 'bc', CREATED = 'c', BEFORE_MOUNT = 'bm', @@ -21,8 +21,9 @@ export enum LifecycleHooks { ERROR_CAPTURED = 'ec', SERVER_PREFETCH = 'sp', } + export const injectHook = ( - type: LifecycleHooks, + type: VaporLifecycleHooks, hook: Function & { __weh?: Function }, target: ComponentInternalInstance | null = currentInstance, prepend: boolean = false, @@ -53,13 +54,13 @@ export const injectHook = ( } } export const createHook = - any>(lifecycle: LifecycleHooks) => + any>(lifecycle: VaporLifecycleHooks) => (hook: T, target: ComponentInternalInstance | null = currentInstance) => injectHook(lifecycle, (...args: unknown[]) => hook(...args), target) -export const onMounted = createHook(LifecycleHooks.MOUNTED) -export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT) -export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE) -export const onUpdated = createHook(LifecycleHooks.UPDATED) -export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT) -export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED) +export const onMounted = createHook(VaporLifecycleHooks.MOUNTED) +export const onBeforeMount = createHook(VaporLifecycleHooks.BEFORE_MOUNT) +export const onBeforeUpdate = createHook(VaporLifecycleHooks.BEFORE_UPDATE) +export const onUpdated = createHook(VaporLifecycleHooks.UPDATED) +export const onBeforeUnmount = createHook(VaporLifecycleHooks.BEFORE_UNMOUNT) +export const onUnmounted = createHook(VaporLifecycleHooks.UNMOUNTED) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 31387ad34..85f542885 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -2,7 +2,7 @@ import { type Ref, EffectScope, ref } from '@vue/reactivity' import type { Block } from './render' import type { DirectiveBinding } from './directive' import type { Data } from '@vue/shared' -import { LifecycleHooks } from './apiLifecycle' +import { VaporLifecycleHooks } from './apiLifecycle' export type SetupFn = (props: any, ctx: any) => Block | Data export type FunctionalComponent = SetupFn & { @@ -31,59 +31,59 @@ export interface ComponentInternalInstance { /** * @internal */ - [LifecycleHooks.BEFORE_CREATE]: LifecycleHook + [VaporLifecycleHooks.BEFORE_CREATE]: LifecycleHook /** * @internal */ - [LifecycleHooks.CREATED]: LifecycleHook + [VaporLifecycleHooks.CREATED]: LifecycleHook /** * @internal */ - [LifecycleHooks.BEFORE_MOUNT]: LifecycleHook + [VaporLifecycleHooks.BEFORE_MOUNT]: LifecycleHook /** * @internal */ - [LifecycleHooks.MOUNTED]: LifecycleHook + [VaporLifecycleHooks.MOUNTED]: LifecycleHook /** * @internal */ - [LifecycleHooks.BEFORE_UPDATE]: LifecycleHook + [VaporLifecycleHooks.BEFORE_UPDATE]: LifecycleHook /** * @internal */ - [LifecycleHooks.UPDATED]: LifecycleHook + [VaporLifecycleHooks.UPDATED]: LifecycleHook /** * @internal */ - [LifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook + [VaporLifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook /** * @internal */ - [LifecycleHooks.UNMOUNTED]: LifecycleHook + [VaporLifecycleHooks.UNMOUNTED]: LifecycleHook /** * @internal */ - [LifecycleHooks.RENDER_TRACKED]: LifecycleHook + [VaporLifecycleHooks.RENDER_TRACKED]: LifecycleHook /** * @internal */ - [LifecycleHooks.RENDER_TRIGGERED]: LifecycleHook + [VaporLifecycleHooks.RENDER_TRIGGERED]: LifecycleHook /** * @internal */ - [LifecycleHooks.ACTIVATED]: LifecycleHook + [VaporLifecycleHooks.ACTIVATED]: LifecycleHook /** * @internal */ - [LifecycleHooks.DEACTIVATED]: LifecycleHook + [VaporLifecycleHooks.DEACTIVATED]: LifecycleHook /** * @internal */ - [LifecycleHooks.ERROR_CAPTURED]: LifecycleHook + [VaporLifecycleHooks.ERROR_CAPTURED]: LifecycleHook /** * @internal */ - [LifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise> + [VaporLifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise> } // TODO @@ -127,59 +127,59 @@ export const createComponentInstance = ( /** * @internal */ - [LifecycleHooks.BEFORE_CREATE]: null, + [VaporLifecycleHooks.BEFORE_CREATE]: null, /** * @internal */ - [LifecycleHooks.CREATED]: null, + [VaporLifecycleHooks.CREATED]: null, /** * @internal */ - [LifecycleHooks.BEFORE_MOUNT]: null, + [VaporLifecycleHooks.BEFORE_MOUNT]: null, /** * @internal */ - [LifecycleHooks.MOUNTED]: null, + [VaporLifecycleHooks.MOUNTED]: null, /** * @internal */ - [LifecycleHooks.BEFORE_UPDATE]: null, + [VaporLifecycleHooks.BEFORE_UPDATE]: null, /** * @internal */ - [LifecycleHooks.UPDATED]: null, + [VaporLifecycleHooks.UPDATED]: null, /** * @internal */ - [LifecycleHooks.BEFORE_UNMOUNT]: null, + [VaporLifecycleHooks.BEFORE_UNMOUNT]: null, /** * @internal */ - [LifecycleHooks.UNMOUNTED]: null, + [VaporLifecycleHooks.UNMOUNTED]: null, /** * @internal */ - [LifecycleHooks.RENDER_TRACKED]: null, + [VaporLifecycleHooks.RENDER_TRACKED]: null, /** * @internal */ - [LifecycleHooks.RENDER_TRIGGERED]: null, + [VaporLifecycleHooks.RENDER_TRIGGERED]: null, /** * @internal */ - [LifecycleHooks.ACTIVATED]: null, + [VaporLifecycleHooks.ACTIVATED]: null, /** * @internal */ - [LifecycleHooks.DEACTIVATED]: null, + [VaporLifecycleHooks.DEACTIVATED]: null, /** * @internal */ - [LifecycleHooks.ERROR_CAPTURED]: null, + [VaporLifecycleHooks.ERROR_CAPTURED]: null, /** * @internal */ - [LifecycleHooks.SERVER_PREFETCH]: null, + [VaporLifecycleHooks.SERVER_PREFETCH]: null, } return instance } From 26308c51ebf28e339d3ab1fb0e91c54eea7b976d Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Sat, 9 Dec 2023 18:41:59 +0800 Subject: [PATCH 05/20] feat: camel modifier for `v-bind` (#39) --- .../transforms/__snapshots__/vBind.spec.ts.snap | 2 +- .../__tests__/transforms/vBind.spec.ts | 11 +++++------ packages/compiler-vapor/src/generate.ts | 4 +++- packages/compiler-vapor/src/ir.ts | 1 + packages/compiler-vapor/src/transforms/vBind.ts | 12 +++++++++++- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap index 335280313..0c115735c 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap @@ -8,7 +8,7 @@ export function render(_ctx) { const n0 = t0() const { 0: [n1],} = _children(n0) _effect(() => { - _setAttr(n1, "foo-bar", undefined, _ctx.id) + _setAttr(n1, "fooBar", undefined, _ctx.id) }) return n0 }" diff --git a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts index fbaf2b089..416f65c4e 100644 --- a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts @@ -165,7 +165,7 @@ describe('compiler: transform v-bind', () => { }) }) - test.fails('.camel modifier', () => { + test('.camel modifier', () => { const node = parseWithVBind(`
`) expect(node.effect[0].operations[0]).toMatchObject({ key: { @@ -179,7 +179,7 @@ describe('compiler: transform v-bind', () => { }) }) - test.fails('.camel modifier w/ no expression', () => { + test('.camel modifier w/ no expression', () => { const node = parseWithVBind(`
`) expect(node.effect[0].operations[0]).toMatchObject({ key: { @@ -193,13 +193,13 @@ describe('compiler: transform v-bind', () => { }) }) - test.fails('.camel modifier w/ dynamic arg', () => { + test('.camel modifier w/ dynamic arg', () => { const node = parseWithVBind(`
`) expect(node.effect[0].operations[0]).toMatchObject({ + runtimeCamelize: true, key: { content: `foo`, isStatic: false, - somethingShouldBeTrue: true, }, value: { content: `id`, @@ -289,8 +289,7 @@ describe('compiler: codegen v-bind', () => { expect(code).contains('_setAttr(n1, _ctx.id, undefined, _ctx.id)') }) - // TODO: camel modifier for v-bind - test.fails('.camel modifier', () => { + test('.camel modifier', () => { const code = compile(`
`) expect(code).matchSnapshot() diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index d92def7fe..4bce56044 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -370,9 +370,11 @@ function genOperation(oper: OperationNode, context: CodegenContext) { } function genSetProp(oper: SetPropIRNode, context: CodegenContext) { - const { push, pushWithNewline, vaporHelper } = context + const { push, pushWithNewline, vaporHelper, helper } = context pushWithNewline(`${vaporHelper('setAttr')}(n${oper.element}, `) + if (oper.runtimeCamelize) push(`${helper('camelize')}(`) genExpression(oper.key, context) + if (oper.runtimeCamelize) push(`)`) push(`, undefined, `) genExpression(oper.value, context) push(')') diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index 172f32fd7..979536af8 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -60,6 +60,7 @@ export interface SetPropIRNode extends BaseIRNode { element: number key: IRExpression value: IRExpression + runtimeCamelize: boolean } export interface SetTextIRNode extends BaseIRNode { diff --git a/packages/compiler-vapor/src/transforms/vBind.ts b/packages/compiler-vapor/src/transforms/vBind.ts index 6bd4cc939..84fd1ac43 100644 --- a/packages/compiler-vapor/src/transforms/vBind.ts +++ b/packages/compiler-vapor/src/transforms/vBind.ts @@ -8,7 +8,7 @@ import { IRNodeTypes } from '../ir' import type { DirectiveTransform } from '../transform' export const transformVBind: DirectiveTransform = (dir, node, context) => { - let { arg, exp, loc } = dir + let { arg, exp, loc, modifiers } = dir if (!arg) { // TODO support v-bind="{}" @@ -21,6 +21,15 @@ export const transformVBind: DirectiveTransform = (dir, node, context) => { exp.ast = null } + let camel = false + if (modifiers.includes('camel')) { + if (arg.isStatic) { + arg.content = camelize(arg.content) + } else { + camel = true + } + } + if (!exp.content.trim()) { context.options.onError( createCompilerError(ErrorCodes.X_V_BIND_NO_EXPRESSION, loc), @@ -38,6 +47,7 @@ export const transformVBind: DirectiveTransform = (dir, node, context) => { element: context.reference(), key: arg, value: exp, + runtimeCamelize: camel, }, ], ) From 0521be9abc25b0127c07891c108bf7a3cd80e12a Mon Sep 17 00:00:00 2001 From: GaoNeng-wWw Date: Sat, 9 Dec 2023 19:45:31 +0800 Subject: [PATCH 06/20] fix(runtime-vapor): set isUnMountedRef to true when unmount component --- packages/runtime-vapor/src/render.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index 43ce89dca..5d1032a41 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -77,4 +77,5 @@ export function unmountComponent(instance: ComponentInternalInstance) { if (um) { invokeArrayFns(um) } + instance.isUnmountedRef.value = true } From 341ddf019076231c590b9784d902c0daee5eaabb 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 23:25:01 +0800 Subject: [PATCH 07/20] feat(compiler-vapor/v-bind): globally allowed --- packages/compiler-vapor/src/generate.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index 4bce56044..b7ac40de7 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -30,7 +30,7 @@ import { IRNodeTypes, } from './ir' import { SourceMapGenerator } from 'source-map-js' -import { camelize, isString, makeMap } from '@vue/shared' +import { camelize, isGloballyAllowed, isString, makeMap } from '@vue/shared' import type { Identifier } from '@babel/types' // remove when stable @@ -534,6 +534,7 @@ function genExpression(node: IRExpression, context: CodegenContext): void { !node.content.trim() || // there was a parsing error ast === false || + isGloballyAllowed(rawExpr) || isLiteralWhitelisted(rawExpr) ) { return push(rawExpr, NewlineType.None, loc) From 45e86e36d7fdba1f47bc31f11b181c0f2f5b4e6e 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 23:29:10 +0800 Subject: [PATCH 08/20] fix(compiler-vapor): generate static expression --- packages/compiler-vapor/src/generate.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index b7ac40de7..0a71f074d 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -525,11 +525,11 @@ function genExpression(node: IRExpression, context: CodegenContext): void { if (isString(node)) return push(node) const { content: rawExpr, ast, isStatic, loc } = node - if (__BROWSER__) { - return push(rawExpr) + if (isStatic) { + return push(JSON.stringify(rawExpr), NewlineType.None, loc) } - if ( + __BROWSER__ || !context.prefixIdentifiers || !node.content.trim() || // there was a parsing error @@ -539,9 +539,6 @@ function genExpression(node: IRExpression, context: CodegenContext): void { ) { return push(rawExpr, NewlineType.None, loc) } - if (isStatic) { - return push(JSON.stringify(rawExpr), NewlineType.None, loc) - } if (ast === null) { // the expression is a simple identifier From da8e196ca5ccc52d8908a10ad093c2804118def7 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: Sun, 10 Dec 2023 00:06:20 +0800 Subject: [PATCH 09/20] refactor(compiler-vapor): v-on --- packages/compiler-core/src/utils.ts | 12 +++- packages/compiler-vapor/src/generate.ts | 57 +++++++++++------ packages/compiler-vapor/src/ir.ts | 4 +- packages/compiler-vapor/src/transforms/vOn.ts | 64 +++++++++---------- 4 files changed, 78 insertions(+), 59 deletions(-) diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts index a1e084eb9..93b990dbb 100644 --- a/packages/compiler-core/src/utils.ts +++ b/packages/compiler-core/src/utils.ts @@ -152,11 +152,17 @@ export const isMemberExpressionBrowser = (path: string): boolean => { } export const isMemberExpressionNode = __BROWSER__ - ? (NOOP as any as (path: string, context: TransformContext) => boolean) - : (path: string, context: TransformContext): boolean => { + ? (NOOP as any as ( + path: string, + options: Pick + ) => boolean) + : ( + path: string, + options: Pick + ): boolean => { try { let ret: Expression = parseExpression(path, { - plugins: context.expressionPlugins + plugins: options.expressionPlugins }) if (ret.type === 'TSAsExpression' || ret.type === 'TSTypeAssertion') { ret = ret.expression diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index 0a71f074d..e9427f474 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -438,31 +438,48 @@ function genSetEvent(oper: SetEventIRNode, context: CodegenContext) { const { vaporHelper, push, pushWithNewline } = context pushWithNewline(`${vaporHelper('on')}(n${oper.element}, `) - // second arg: event name - genExpression(oper.key, context) - push(', ') - const { keys, nonKeys, options } = oper.modifiers - if (keys.length) { - push(`${vaporHelper('withKeys')}(`) - } - if (nonKeys.length) { - push(`${vaporHelper('withModifiers')}(`) + // 2nd arg: event name + if (oper.keyOverride) { + const find = JSON.stringify(oper.keyOverride[0]) + const replacement = JSON.stringify(oper.keyOverride[1]) + push('(') + genExpression(oper.key, context) + push(`) === ${find} ? ${replacement} : (`) + genExpression(oper.key, context) + push(')') + } else { + genExpression(oper.key, context) } + push(', ') - // gen event handler - push('(...args) => (') - genExpression(oper.value, context) - push(' && ') - genExpression(oper.value, context) - push('(...args))') + const { keys, nonKeys, options } = oper.modifiers - if (nonKeys.length) { - push(`, ${genArrayExpression(nonKeys)})`) - } - if (keys.length) { - push(`, ${genArrayExpression(keys)})`) + // 3rd arg: event handler + if (oper.value && oper.value.content.trim()) { + if (keys.length) { + push(`${vaporHelper('withKeys')}(`) + } + if (nonKeys.length) { + push(`${vaporHelper('withModifiers')}(`) + } + push('(...args) => (') + genExpression(oper.value, context) + push(' && ') + genExpression(oper.value, context) + push('(...args))') + + if (nonKeys.length) { + push(`, ${genArrayExpression(nonKeys)})`) + } + if (keys.length) { + push(`, ${genArrayExpression(keys)})`) + } + } else { + push('() => {}') } + + // 4th arg, gen options if (options.length) { push(`, { ${options.map((v) => `${v}: true`).join(', ')} }`) } diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index 979536af8..2af5455d6 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -69,11 +69,12 @@ export interface SetTextIRNode extends BaseIRNode { value: IRExpression } +export type KeyOverride = [find: string, replacement: string] export interface SetEventIRNode extends BaseIRNode { type: IRNodeTypes.SET_EVENT element: number key: IRExpression - value: IRExpression + value?: SimpleExpressionNode modifiers: { // modifiers for addEventListener() options, e.g. .passive & .capture options: string[] @@ -82,6 +83,7 @@ export interface SetEventIRNode extends BaseIRNode { // modifiers that needs runtime guards, withModifiers nonKeys: string[] } + keyOverride?: KeyOverride } export interface SetHtmlIRNode extends BaseIRNode { diff --git a/packages/compiler-vapor/src/transforms/vOn.ts b/packages/compiler-vapor/src/transforms/vOn.ts index f1764ffb0..de5fc38a9 100644 --- a/packages/compiler-vapor/src/transforms/vOn.ts +++ b/packages/compiler-vapor/src/transforms/vOn.ts @@ -1,70 +1,64 @@ -import { - createCompilerError, - createSimpleExpression, - ErrorCodes, - ExpressionNode, - isStaticExp, - NodeTypes, -} from '@vue/compiler-core' +import { createCompilerError, ErrorCodes } from '@vue/compiler-core' import type { DirectiveTransform } from '../transform' -import { IRNodeTypes } from '../ir' +import { IRNodeTypes, KeyOverride } from '../ir' import { resolveModifiers } from '@vue/compiler-dom' export const transformVOn: DirectiveTransform = (dir, node, context) => { - const { arg, exp, loc, modifiers } = dir + let { arg, exp, loc, modifiers } = dir if (!exp && !modifiers.length) { context.options.onError( createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc), ) - return } if (!arg) { // TODO support v-on="{}" return - } else if (exp === undefined) { - // TODO X_V_ON_NO_EXPRESSION error - return } - const handlerKey = `on${arg.content}` const { keyModifiers, nonKeyModifiers, eventOptionModifiers } = - resolveModifiers(handlerKey, modifiers, null, loc) + resolveModifiers( + arg.isStatic ? `on${arg.content}` : arg, + modifiers, + null, + loc, + ) + + let keyOverride: KeyOverride | undefined // normalize click.right and click.middle since they don't actually fire - let name = arg.content + + const isStaticClick = arg.isStatic && arg.content.toLowerCase() === 'click' + if (nonKeyModifiers.includes('right')) { - name = transformClick(arg, 'contextmenu') + if (isStaticClick) { + arg = { ...arg, content: 'contextmenu' } + } else if (!arg.isStatic) { + keyOverride = ['click', 'contextmenu'] + } } if (nonKeyModifiers.includes('middle')) { - name = transformClick(arg, 'mouseup') + if (keyOverride) { + // TODO error here + } + if (isStaticClick) { + arg = { ...arg, content: 'mouseup' } + } else if (!arg.isStatic) { + keyOverride = ['click', 'mouseup'] + } } - // TODO reactive context.registerOperation({ type: IRNodeTypes.SET_EVENT, loc, element: context.reference(), - key: createSimpleExpression(name, true, arg.loc), + key: arg, value: exp, modifiers: { keys: keyModifiers, nonKeys: nonKeyModifiers, options: eventOptionModifiers, }, + keyOverride, }) } - -function transformClick(key: ExpressionNode, event: string) { - const isStaticClick = - isStaticExp(key) && key.content.toLowerCase() === 'click' - - if (isStaticClick) { - return event - } else if (key.type !== NodeTypes.SIMPLE_EXPRESSION) { - // TODO: handle CompoundExpression - return 'TODO' - } else { - return key.content.toLowerCase() - } -} From 4b4cb055a4044b362a91aa02edcc75eeb0445bfa 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: Sun, 10 Dec 2023 01:01:57 +0800 Subject: [PATCH 10/20] refactor: pushMulti --- packages/compiler-vapor/src/generate.ts | 111 ++++++++++++++---------- 1 file changed, 63 insertions(+), 48 deletions(-) diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index e9427f474..9dc5eb1ad 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -58,6 +58,10 @@ export interface CodegenContext extends Required { loc?: SourceLocation, name?: string, ): void + pushMulti( + codes: [left: string, right: string, segment?: string], + ...fn: Array void)> + ): void indent(): void deindent(): void newline(): void @@ -170,6 +174,15 @@ function createCodegenContext( context.newline() context.push(code, newlineIndex, node) }, + pushMulti([left, right, seg], ...fns) { + fns = fns.filter(Boolean) + context.push(left) + for (let i = 0; i < fns.length; i++) { + ;(fns[i] as () => void)() + if (seg && i < fns.length - 1) context.push(seg) + } + context.push(right) + }, indent() { ++context.indentLevel }, @@ -435,56 +448,58 @@ function genAppendNode(oper: AppendNodeIRNode, context: CodegenContext) { } function genSetEvent(oper: SetEventIRNode, context: CodegenContext) { - const { vaporHelper, push, pushWithNewline } = context - - pushWithNewline(`${vaporHelper('on')}(n${oper.element}, `) - - // 2nd arg: event name - if (oper.keyOverride) { - const find = JSON.stringify(oper.keyOverride[0]) - const replacement = JSON.stringify(oper.keyOverride[1]) - push('(') - genExpression(oper.key, context) - push(`) === ${find} ? ${replacement} : (`) - genExpression(oper.key, context) - push(')') - } else { - genExpression(oper.key, context) - } - push(', ') - + const { vaporHelper, push, pushWithNewline, pushMulti: pushMulti } = context const { keys, nonKeys, options } = oper.modifiers - // 3rd arg: event handler - if (oper.value && oper.value.content.trim()) { - if (keys.length) { - push(`${vaporHelper('withKeys')}(`) - } - if (nonKeys.length) { - push(`${vaporHelper('withModifiers')}(`) - } - push('(...args) => (') - genExpression(oper.value, context) - push(' && ') - genExpression(oper.value, context) - push('(...args))') - - if (nonKeys.length) { - push(`, ${genArrayExpression(nonKeys)})`) - } - if (keys.length) { - push(`, ${genArrayExpression(keys)})`) - } - } else { - push('() => {}') - } - - // 4th arg, gen options - if (options.length) { - push(`, { ${options.map((v) => `${v}: true`).join(', ')} }`) - } - - push(')') + pushWithNewline(vaporHelper('on')) + pushMulti( + ['(', ')', ', '], + // 1st arg: event name + () => push(`n${oper.element}`), + // 2nd arg: event name + () => { + if (oper.keyOverride) { + const find = JSON.stringify(oper.keyOverride[0]) + const replacement = JSON.stringify(oper.keyOverride[1]) + pushMulti(['(', ')'], () => genExpression(oper.key, context)) + push(` === ${find} ? ${replacement} : `) + pushMulti(['(', ')'], () => genExpression(oper.key, context)) + } else { + genExpression(oper.key, context) + } + }, + // 3rd arg: event handler + () => { + if (oper.value && oper.value.content.trim()) { + const pushWithKeys = (fn: () => void) => { + push(`${vaporHelper('withKeys')}(`) + fn() + push(`, ${genArrayExpression(keys)})`) + } + const pushWithModifiers = (fn: () => void) => { + push(`${vaporHelper('withModifiers')}(`) + fn() + push(`, ${genArrayExpression(nonKeys)})`) + } + const pushNoop = (fn: () => void) => fn() + + ;(keys.length ? pushWithKeys : pushNoop)(() => + (nonKeys.length ? pushWithModifiers : pushNoop)(() => { + push('(...args) => (') + genExpression(oper.value!, context) + push(' && ') + genExpression(oper.value!, context) + push('(...args))') + }), + ) + } else { + push('() => {}') + } + }, + // 4th arg, gen options + !!options.length && + (() => push(`{ ${options.map((v) => `${v}: true`).join(', ')} }`)), + ) } function genWithDirective(oper: WithDirectiveIRNode, context: CodegenContext) { From 0c26b0d4edbe3c52b917e30312a215a2b0c9f80f 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: Sun, 10 Dec 2023 01:05:26 +0800 Subject: [PATCH 11/20] feat: withIndent --- packages/compiler-vapor/src/generate.ts | 113 +++++++++++++----------- 1 file changed, 59 insertions(+), 54 deletions(-) diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index 9dc5eb1ad..ff41cad18 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -62,8 +62,7 @@ export interface CodegenContext extends Required { codes: [left: string, right: string, segment?: string], ...fn: Array void)> ): void - indent(): void - deindent(): void + withIndent(fn: () => void): void newline(): void helpers: Set @@ -183,10 +182,9 @@ function createCodegenContext( } context.push(right) }, - indent() { + withIndent(fn) { ++context.indentLevel - }, - deindent() { + fn() --context.indentLevel }, newline() { @@ -231,8 +229,15 @@ export function generate( options: CodegenOptions = {}, ): CodegenResult { const ctx = createCodegenContext(ir, options) - const { push, pushWithNewline, indent, deindent, newline } = ctx - const { vaporHelper, helpers, vaporHelpers } = ctx + const { + push, + pushWithNewline, + withIndent, + newline, + helpers, + vaporHelper, + vaporHelpers, + } = ctx const functionName = 'render' const isSetupInlined = !!options.inline @@ -243,62 +248,62 @@ export function generate( newline() pushWithNewline(`export function ${functionName}(_ctx) {`) } - indent() - - ir.template.forEach((template, i) => { - if (template.type === IRNodeTypes.TEMPLATE_FACTORY) { - // TODO source map? - pushWithNewline( - `const t${i} = ${vaporHelper('template')}(${JSON.stringify( - template.template, - )})`, - ) - } else { - // fragment - pushWithNewline( - `const t0 = ${vaporHelper('fragment')}()\n`, - NewlineType.End, - ) - } - }) - { - pushWithNewline(`const n${ir.dynamic.id} = t0()`) + withIndent(() => { + ir.template.forEach((template, i) => { + if (template.type === IRNodeTypes.TEMPLATE_FACTORY) { + // TODO source map? + pushWithNewline( + `const t${i} = ${vaporHelper('template')}(${JSON.stringify( + template.template, + )})`, + ) + } else { + // fragment + pushWithNewline( + `const t0 = ${vaporHelper('fragment')}()\n`, + NewlineType.End, + ) + } + }) - const children = genChildren(ir.dynamic.children) - if (children) { - pushWithNewline( - `const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`, - ) - } + { + pushWithNewline(`const n${ir.dynamic.id} = t0()`) - for (const oper of ir.operation.filter( - (oper): oper is WithDirectiveIRNode => - oper.type === IRNodeTypes.WITH_DIRECTIVE, - )) { - genWithDirective(oper, ctx) - } + const children = genChildren(ir.dynamic.children) + if (children) { + pushWithNewline( + `const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`, + ) + } - for (const operation of ir.operation) { - genOperation(operation, ctx) - } + for (const oper of ir.operation.filter( + (oper): oper is WithDirectiveIRNode => + oper.type === IRNodeTypes.WITH_DIRECTIVE, + )) { + genWithDirective(oper, ctx) + } - for (const { operations } of ir.effect) { - pushWithNewline(`${vaporHelper('effect')}(() => {`) - indent() - for (const operation of operations) { + for (const operation of ir.operation) { genOperation(operation, ctx) } - deindent() - pushWithNewline('})') - } - // TODO multiple-template - // TODO return statement in IR - pushWithNewline(`return n${ir.dynamic.id}`) - } + for (const { operations } of ir.effect) { + pushWithNewline(`${vaporHelper('effect')}(() => {`) + withIndent(() => { + for (const operation of operations) { + genOperation(operation, ctx) + } + }) + pushWithNewline('})') + } + + // TODO multiple-template + // TODO return statement in IR + pushWithNewline(`return n${ir.dynamic.id}`) + } + }) - deindent() newline() if (isSetupInlined) { push('})()') From ecf7da98d7d86da278c1a3a9b2a6c144727d62d6 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: Sun, 10 Dec 2023 01:26:19 +0800 Subject: [PATCH 12/20] feat: pushFnCall --- packages/compiler-vapor/src/generate.ts | 215 ++++++++++++++---------- 1 file changed, 123 insertions(+), 92 deletions(-) diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index ff41cad18..ba2a3aeb5 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -52,7 +52,7 @@ export interface CodegenContext extends Required { loc?: SourceLocation, name?: string, ): void - pushWithNewline( + pushNewline( code: string, newlineIndex?: number, loc?: SourceLocation, @@ -60,8 +60,9 @@ export interface CodegenContext extends Required { ): void pushMulti( codes: [left: string, right: string, segment?: string], - ...fn: Array void)> + ...fn: Array void)> ): void + pushFnCall(name: string, ...args: Array void)>): void withIndent(fn: () => void): void newline(): void @@ -169,7 +170,7 @@ function createCodegenContext( } } }, - pushWithNewline(code, newlineIndex, node) { + pushNewline(code, newlineIndex, node) { context.newline() context.push(code, newlineIndex, node) }, @@ -177,25 +178,28 @@ function createCodegenContext( fns = fns.filter(Boolean) context.push(left) for (let i = 0; i < fns.length; i++) { - ;(fns[i] as () => void)() + const fn = fns[i] as string | (() => void) + + if (isString(fn)) context.push(fn) + else fn() if (seg && i < fns.length - 1) context.push(seg) } context.push(right) }, + pushFnCall(name, ...args) { + context.push(name) + context.pushMulti(['(', ')', ', '], ...args) + }, withIndent(fn) { ++context.indentLevel fn() --context.indentLevel }, newline() { - newline(context.indentLevel) + context.push(`\n${` `.repeat(context.indentLevel)}`, NewlineType.Start) }, } - function newline(n: number) { - context.push(`\n${` `.repeat(n)}`, NewlineType.Start) - } - function addMapping(loc: Position, name: string | null = null) { // we use the private property to directly add the mapping // because the addMapping() implementation in source-map-js has a bunch of @@ -231,7 +235,7 @@ export function generate( const ctx = createCodegenContext(ir, options) const { push, - pushWithNewline, + pushNewline, withIndent, newline, helpers, @@ -246,21 +250,21 @@ export function generate( } else { // placeholder for preamble newline() - pushWithNewline(`export function ${functionName}(_ctx) {`) + pushNewline(`export function ${functionName}(_ctx) {`) } withIndent(() => { ir.template.forEach((template, i) => { if (template.type === IRNodeTypes.TEMPLATE_FACTORY) { // TODO source map? - pushWithNewline( + pushNewline( `const t${i} = ${vaporHelper('template')}(${JSON.stringify( template.template, )})`, ) } else { // fragment - pushWithNewline( + pushNewline( `const t0 = ${vaporHelper('fragment')}()\n`, NewlineType.End, ) @@ -268,11 +272,11 @@ export function generate( }) { - pushWithNewline(`const n${ir.dynamic.id} = t0()`) + pushNewline(`const n${ir.dynamic.id} = t0()`) const children = genChildren(ir.dynamic.children) if (children) { - pushWithNewline( + pushNewline( `const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`, ) } @@ -289,18 +293,18 @@ export function generate( } for (const { operations } of ir.effect) { - pushWithNewline(`${vaporHelper('effect')}(() => {`) + pushNewline(`${vaporHelper('effect')}(() => {`) withIndent(() => { for (const operation of operations) { genOperation(operation, ctx) } }) - pushWithNewline('})') + pushNewline('})') } // TODO multiple-template // TODO return statement in IR - pushWithNewline(`return n${ir.dynamic.id}`) + pushNewline(`return n${ir.dynamic.id}`) } }) @@ -336,7 +340,6 @@ export function generate( function genChildren(children: IRDynamicChildren) { let code = '' - // TODO let offset = 0 for (const [index, child] of Object.entries(children)) { const childrenLength = Object.keys(child.children).length @@ -388,77 +391,93 @@ function genOperation(oper: OperationNode, context: CodegenContext) { } function genSetProp(oper: SetPropIRNode, context: CodegenContext) { - const { push, pushWithNewline, vaporHelper, helper } = context - pushWithNewline(`${vaporHelper('setAttr')}(n${oper.element}, `) - if (oper.runtimeCamelize) push(`${helper('camelize')}(`) - genExpression(oper.key, context) - if (oper.runtimeCamelize) push(`)`) - push(`, undefined, `) - genExpression(oper.value, context) - push(')') + const { pushFnCall, newline, vaporHelper, helper } = context + + newline() + pushFnCall( + vaporHelper('setAttr'), + `n${oper.element}`, + // 2. key name + () => { + if (oper.runtimeCamelize) { + pushFnCall(helper('camelize'), () => genExpression(oper.key, context)) + } else { + genExpression(oper.key, context) + } + }, + 'undefined', + () => genExpression(oper.value, context), + ) } function genSetText(oper: SetTextIRNode, context: CodegenContext) { - const { push, pushWithNewline, vaporHelper } = context - pushWithNewline(`${vaporHelper('setText')}(n${oper.element}, undefined, `) - genExpression(oper.value, context) - push(')') + const { pushFnCall, newline, vaporHelper } = context + newline() + pushFnCall(vaporHelper('setText'), `n${oper.element}`, 'undefined', () => + genExpression(oper.value, context), + ) } function genSetHtml(oper: SetHtmlIRNode, context: CodegenContext) { - const { push, pushWithNewline, vaporHelper } = context - pushWithNewline(`${vaporHelper('setHtml')}(n${oper.element}, undefined, `) - genExpression(oper.value, context) - push(')') + const { newline, pushFnCall, vaporHelper } = context + newline() + pushFnCall(vaporHelper('setHtml'), `n${oper.element}`, 'undefined', () => + genExpression(oper.value, context), + ) } function genCreateTextNode( oper: CreateTextNodeIRNode, context: CodegenContext, ) { - const { push, pushWithNewline, vaporHelper } = context - pushWithNewline(`const n${oper.id} = ${vaporHelper('createTextNode')}(`) - genExpression(oper.value, context) - push(')') + const { pushNewline, pushFnCall, vaporHelper } = context + pushNewline(`const n${oper.id} = `) + pushFnCall(vaporHelper('createTextNode'), () => + genExpression(oper.value, context), + ) } function genInsertNode(oper: InsertNodeIRNode, context: CodegenContext) { - const { pushWithNewline, vaporHelper } = context + const { newline, pushFnCall, vaporHelper } = context const elements = ([] as number[]).concat(oper.element) let element = elements.map((el) => `n${el}`).join(', ') if (elements.length > 1) element = `[${element}]` - pushWithNewline( - `${vaporHelper('insert')}(${element}, n${ - oper.parent - }${`, n${oper.anchor}`})`, + newline() + pushFnCall( + vaporHelper('insert'), + element, + `n${oper.parent}`, + `n${oper.anchor}`, ) } function genPrependNode(oper: PrependNodeIRNode, context: CodegenContext) { - const { pushWithNewline, vaporHelper } = context - pushWithNewline( - `${vaporHelper('prepend')}(n${oper.parent}, ${oper.elements - .map((el) => `n${el}`) - .join(', ')})`, + const { newline, pushFnCall, vaporHelper } = context + newline() + pushFnCall( + vaporHelper('prepend'), + `n${oper.parent}`, + oper.elements.map((el) => `n${el}`).join(', '), ) } function genAppendNode(oper: AppendNodeIRNode, context: CodegenContext) { - const { pushWithNewline, vaporHelper } = context - pushWithNewline( - `${vaporHelper('append')}(n${oper.parent}, ${oper.elements - .map((el) => `n${el}`) - .join(', ')})`, + const { newline, pushFnCall, vaporHelper } = context + newline() + pushFnCall( + vaporHelper('append'), + `n${oper.parent}`, + oper.elements.map((el) => `n${el}`).join(', '), ) } function genSetEvent(oper: SetEventIRNode, context: CodegenContext) { - const { vaporHelper, push, pushWithNewline, pushMulti: pushMulti } = context + const { vaporHelper, push, newline, pushMulti, pushFnCall } = context const { keys, nonKeys, options } = oper.modifiers - pushWithNewline(vaporHelper('on')) - pushMulti( - ['(', ')', ', '], + newline() + pushFnCall( + vaporHelper('on'), // 1st arg: event name () => push(`n${oper.element}`), // 2nd arg: event name @@ -508,46 +527,58 @@ function genSetEvent(oper: SetEventIRNode, context: CodegenContext) { } function genWithDirective(oper: WithDirectiveIRNode, context: CodegenContext) { - const { push, pushWithNewline, vaporHelper, bindingMetadata } = context + const { push, newline, pushFnCall, pushMulti, vaporHelper, bindingMetadata } = + context const { dir } = oper // TODO merge directive for the same node - pushWithNewline(`${vaporHelper('withDirectives')}(n${oper.element}, [[`) - - if (dir.name === 'show') { - push(vaporHelper('vShow')) - } else { - const directiveReference = camelize(`v-${dir.name}`) - // TODO resolve directive - if (bindingMetadata[directiveReference]) { - const directiveExpression = createSimpleExpression(directiveReference) - directiveExpression.ast = null - genExpression(directiveExpression, context) - } - } + newline() + pushFnCall( + vaporHelper('withDirectives'), + // 1st arg: node + `n${oper.element}`, + // 2nd arg: directives + () => { + push('[') + // directive + pushMulti(['[', ']', ', '], () => { + if (dir.name === 'show') { + push(vaporHelper('vShow')) + } else { + const directiveReference = camelize(`v-${dir.name}`) + // TODO resolve directive + if (bindingMetadata[directiveReference]) { + const directiveExpression = + createSimpleExpression(directiveReference) + directiveExpression.ast = null + genExpression(directiveExpression, context) + } + } - if (dir.exp) { - push(', () => ') - genExpression(dir.exp, context) - } else if (dir.arg || dir.modifiers.length) { - push(', void 0') - } + if (dir.exp) { + push(', () => ') + genExpression(dir.exp, context) + } else if (dir.arg || dir.modifiers.length) { + push(', void 0') + } - if (dir.arg) { - push(', ') - genExpression(dir.arg, context) - } else if (dir.modifiers.length) { - push(', void 0') - } + if (dir.arg) { + push(', ') + genExpression(dir.arg, context) + } else if (dir.modifiers.length) { + push(', void 0') + } - if (dir.modifiers.length) { - push(', ') - push('{ ') - push(genDirectiveModifiers(dir.modifiers)) - push(' }') - } - push(']])') - return + if (dir.modifiers.length) { + push(', ') + push('{ ') + push(genDirectiveModifiers(dir.modifiers)) + push(' }') + } + }) + push(']') + }, + ) } // TODO: other types (not only string) From 12250a85b92551af95e0bc60b21b5997f74640ce Mon Sep 17 00:00:00 2001 From: ubugeeei <71201308+Ubugeeei@users.noreply.github.com> Date: Sun, 10 Dec 2023 02:33:18 +0900 Subject: [PATCH 13/20] feat(runtime-vapor): component props (#40) --- packages/runtime-vapor/src/component.ts | 50 +++- packages/runtime-vapor/src/componentProps.ts | 267 ++++++++++++++++++ .../src/componentPublicInstance.ts | 22 ++ packages/runtime-vapor/src/render.ts | 33 ++- playground/src/main.ts | 4 +- playground/src/props.js | 104 +++++++ 6 files changed, 458 insertions(+), 22 deletions(-) create mode 100644 packages/runtime-vapor/src/componentProps.ts create mode 100644 packages/runtime-vapor/src/componentPublicInstance.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 26518598d..5ff3b86b8 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -1,13 +1,25 @@ -import { type Ref, EffectScope, ref } from '@vue/reactivity' -import type { Block } from './render' -import type { DirectiveBinding } from './directive' +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 { Data } from '@vue/shared' +export type Component = FunctionalComponent | ObjectComponent + export type SetupFn = (props: any, ctx: any) => Block | Data export type FunctionalComponent = SetupFn & { + props: ComponentPropsOptions render(ctx: any): Block } export interface ObjectComponent { + props: ComponentPropsOptions setup: SetupFn render(ctx: any): Block } @@ -17,13 +29,22 @@ export interface ComponentInternalInstance { container: ParentNode block: Block | null scope: EffectScope - component: FunctionalComponent | ObjectComponent - get isMounted(): boolean - isMountedRef: Ref + propsOptions: NormalizedPropsOptions + + // TODO: type + proxy: Data | null + + // state + props: Data + setupState: Data /** directives */ dirs: Map + + // lifecycle + get isMounted(): boolean + isMountedRef: Ref // TODO: registory of provides, appContext, lifecycles, ... } @@ -51,14 +72,25 @@ export const createComponentInstance = ( block: null, container: null!, // set on mount scope: new EffectScope(true /* detached */)!, - component, + + // resolved props and emits options + propsOptions: normalizePropsOptions(component), + // emitsOptions: normalizeEmitsOptions(type, appContext), // TODO: + + proxy: null, + + // state + props: EMPTY_OBJ, + setupState: EMPTY_OBJ, + + dirs: new Map(), + + // lifecycle get isMounted() { return isMountedRef.value }, isMountedRef, - - dirs: new Map(), // 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..5cd0f1d21 --- /dev/null +++ b/packages/runtime-vapor/src/componentProps.ts @@ -0,0 +1,267 @@ +// NOTE: runtime-core/src/componentProps.ts + +import { + Data, + EMPTY_ARR, + EMPTY_OBJ, + camelize, + extend, + hasOwn, + hyphenate, + isArray, + isFunction, + isReservedProp, +} from '@vue/shared' +import { shallowReactive, toRaw } from '@vue/reactivity' +import { type ComponentInternalInstance, type Component } 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 + */ + 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/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 b03e56ae8..422d5e689 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -1,14 +1,20 @@ -import { reactive } from '@vue/reactivity' +import { markRaw, proxyRefs } from '@vue/reactivity' +import { type Data } from '@vue/shared' + import { + type Component, type ComponentInternalInstance, - type FunctionalComponent, - type ObjectComponent, createComponentInstance, setCurrentInstance, unsetCurrentInstance, } from './component' + +import { initProps } from './componentProps' + import { invokeDirectiveHook } from './directive' + import { insert, remove } from './dom' +import { PublicInstanceProxyHandlers } from './componentPublicInstance' export type Block = Node | Fragment | Block[] export type ParentBlock = ParentNode | Node[] @@ -16,13 +22,13 @@ export type Fragment = { nodes: Block; anchor: Node } export type BlockFn = (props: any, ctx: any) => Block export function render( - comp: ObjectComponent | FunctionalComponent, + comp: Component, + props: Data, container: string | ParentNode, ): ComponentInternalInstance { const instance = createComponentInstance(comp) - setCurrentInstance(instance) - mountComponent(instance, (container = normalizeContainer(container))) - return instance + initProps(instance, props) + return mountComponent(instance, (container = normalizeContainer(container))) } export function normalizeContainer(container: string | ParentNode): ParentNode { @@ -39,29 +45,34 @@ export function mountComponent( setCurrentInstance(instance) const block = instance.scope.run(() => { - const { component } = instance - const props = {} + const { component, props } = instance const ctx = { expose: () => {} } const setupFn = 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(state))) + instance.setupState = proxyRefs(state) + return (instance.block = component.render(instance.proxy)) } else { return (instance.block = state as Block) } })! - invokeDirectiveHook(instance, 'beforeMount') insert(block, instance.container) instance.isMountedRef.value = true invokeDirectiveHook(instance, 'mounted') + unsetCurrentInstance() // TODO: lifecycle hooks (mounted, ...) // const { m } = instance // m && invoke(m) + + return instance } export function unmountComponent(instance: ComponentInternalInstance) { diff --git a/playground/src/main.ts b/playground/src/main.ts index 43565bc94..717629057 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -1,6 +1,6 @@ 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: mod }) => render(mod, '#app')) +mod.then(({ default: mod }) => render(mod, {}, '#app')) diff --git a/playground/src/props.js b/playground/src/props.js new file mode 100644 index 000000000..b80768dcc --- /dev/null +++ b/playground/src/props.js @@ -0,0 +1,104 @@ +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++ + } + + const __returned__ = { count, 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) + effect(() => { + setText(n1, void 0, _ctx.count) + }) + + // TODO: create component fn? + // const c0 = createComponent(...) + // insert(n0, c0) + renderComponent( + child, + + // TODO: proxy?? + { + /* */ + get count() { + return _ctx.count + }, + + /* */ + get inlineDouble() { + return _ctx.count * 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) + ) + + const __returned__ = {} + + 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 + ' * 2 = ' + _ctx.inlineDouble) + }) + return n0 + } +} From b421aa91a2b09f4b9f9b995a65dc18affd12d3ac Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Sun, 10 Dec 2023 01:33:29 +0800 Subject: [PATCH 14/20] test: combine with transform and codegen tests for `v-bind` (#45) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 三咲智子 Kevin Deng --- .../__snapshots__/vBind.spec.ts.snap | 50 ++++- .../__tests__/transforms/vBind.spec.ts | 191 +++++++----------- 2 files changed, 110 insertions(+), 131 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap index 0c115735c..910d019cd 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`compiler: codegen v-bind > .camel modifier 1`] = ` +exports[`compiler v-bind > .camel modifier 1`] = ` "import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { @@ -14,21 +14,21 @@ export function render(_ctx) { }" `; -exports[`compiler: codegen v-bind > dynamic arg 1`] = ` -"import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor'; +exports[`compiler v-bind > .camel modifier w/ dynamic arg 1`] = ` +"import { camelize as _camelize } from 'vue'; export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) _effect(() => { - _setAttr(n1, _ctx.id, undefined, _ctx.id) + _setAttr(n1, _camelize(_ctx.foo), undefined, _ctx.id) }) return n0 }" `; -exports[`compiler: codegen v-bind > no expression (shorthand) 1`] = ` +exports[`compiler v-bind > .camel modifier w/ no expression 1`] = ` "import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { @@ -36,13 +36,13 @@ export function render(_ctx) { const n0 = t0() const { 0: [n1],} = _children(n0) _effect(() => { - _setAttr(n1, "camel-case", undefined, _ctx.camelCase) + _setAttr(n1, "fooBar", undefined, _ctx.fooBar) }) return n0 }" `; -exports[`compiler: codegen v-bind > no expression 1`] = ` +exports[`compiler v-bind > basic 1`] = ` "import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { @@ -56,17 +56,35 @@ export function render(_ctx) { }" `; -exports[`compiler: codegen v-bind > should error if no expression 1`] = ` -"import { template as _template } from 'vue/vapor'; +exports[`compiler v-bind > dynamic arg 1`] = ` +"import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { - const t0 = _template("
") + const t0 = _template("
") const n0 = t0() + const { 0: [n1],} = _children(n0) + _effect(() => { + _setAttr(n1, _ctx.id, undefined, _ctx.id) + }) return n0 }" `; -exports[`compiler: codegen v-bind > simple expression 1`] = ` +exports[`compiler v-bind > no expression (shorthand) 1`] = ` +"import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("
") + const n0 = t0() + const { 0: [n1],} = _children(n0) + _effect(() => { + _setAttr(n1, "camel-case", undefined, _ctx.camelCase) + }) + return n0 +}" +`; + +exports[`compiler v-bind > no expression 1`] = ` "import { template as _template, children as _children, effect as _effect, setAttr as _setAttr } from 'vue/vapor'; export function render(_ctx) { @@ -79,3 +97,13 @@ export function render(_ctx) { return n0 }" `; + +exports[`compiler v-bind > should error if empty expression 1`] = ` +"import { template as _template } from 'vue/vapor'; + +export function render(_ctx) { + const t0 = _template("
") + const n0 = t0() + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts index 416f65c4e..c81f2e51a 100644 --- a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts @@ -1,9 +1,4 @@ -import { - type RootNode, - ErrorCodes, - NodeTypes, - BindingTypes, -} from '@vue/compiler-dom' +import { ErrorCodes, NodeTypes } from '@vue/compiler-dom' import { type RootIRNode, type CompilerOptions, @@ -13,48 +8,45 @@ import { transformElement, IRNodeTypes, compile as _compile, + generate, } from '../../src' -function parseWithVBind( +function compileWithVBind( template: string, options: CompilerOptions = {}, -): RootIRNode { - const ast = parse(template) +): { + ir: RootIRNode + code: string +} { + const ast = parse(template, { prefixIdentifiers: true, ...options }) const ir = transform(ast, { nodeTransforms: [transformElement], directiveTransforms: { bind: transformVBind, }, - ...options, - }) - return ir -} - -function compile(template: string | RootNode, options: CompilerOptions = {}) { - let { code } = _compile(template, { - ...options, - mode: 'module', prefixIdentifiers: true, + ...options, }) - return code + const { code } = generate(ir, { prefixIdentifiers: true, ...options }) + return { ir, code } } -describe('compiler: transform v-bind', () => { +describe('compiler v-bind', () => { test('basic', () => { - const node = parseWithVBind(`
`) + const { ir, code } = compileWithVBind(`
`) - expect(node.dynamic.children[0]).toMatchObject({ + expect(ir.dynamic.children[0]).toMatchObject({ id: 1, referenced: true, }) - expect(node.template[0]).toMatchObject({ + expect(ir.template[0]).toMatchObject({ type: IRNodeTypes.TEMPLATE_FACTORY, template: '
', }) - expect(node.effect).lengthOf(1) - expect(node.effect[0].expressions).lengthOf(1) - expect(node.effect[0].operations).lengthOf(1) - expect(node.effect[0]).toMatchObject({ + expect(ir.effect).lengthOf(1) + expect(ir.effect[0].expressions).lengthOf(1) + expect(ir.effect[0].operations).lengthOf(1) + expect(ir.effect[0]).toMatchObject({ expressions: [ { type: NodeTypes.SIMPLE_EXPRESSION, @@ -89,12 +81,15 @@ describe('compiler: transform v-bind', () => { }, ], }) + + expect(code).matchSnapshot() + expect(code).contains('_setAttr(n1, "id", undefined, _ctx.id)') }) test('no expression', () => { - const node = parseWithVBind(`
`) + const { ir, code } = compileWithVBind(`
`) - expect(node.effect[0].operations[0]).toMatchObject({ + expect(ir.effect[0].operations[0]).toMatchObject({ type: IRNodeTypes.SET_PROP, key: { content: `id`, @@ -113,27 +108,35 @@ describe('compiler: transform v-bind', () => { }, }, }) + + expect(code).matchSnapshot() + expect(code).contains('_setAttr(n1, "id", undefined, _ctx.id)') }) test('no expression (shorthand)', () => { - const node = parseWithVBind(`
`) + const { ir, code } = compileWithVBind(`
`) - expect(node.effect[0].operations[0]).toMatchObject({ + expect(ir.effect[0].operations[0]).toMatchObject({ type: IRNodeTypes.SET_PROP, key: { - content: `id`, + content: `camel-case`, isStatic: true, }, value: { - content: `id`, + content: `camelCase`, isStatic: false, }, }) + + expect(code).matchSnapshot() + expect(code).contains( + '_setAttr(n1, "camel-case", undefined, _ctx.camelCase)', + ) }) test('dynamic arg', () => { - const node = parseWithVBind(`
`) - expect(node.effect[0].operations[0]).toMatchObject({ + const { ir, code } = compileWithVBind(`
`) + expect(ir.effect[0].operations[0]).toMatchObject({ type: IRNodeTypes.SET_PROP, element: 1, key: { @@ -147,11 +150,17 @@ describe('compiler: transform v-bind', () => { isStatic: false, }, }) + + expect(code).matchSnapshot() + expect(code).contains('_setAttr(n1, _ctx.id, undefined, _ctx.id)') }) test('should error if empty expression', () => { const onError = vi.fn() - const node = parseWithVBind(`
`, { onError }) + const { ir, code } = compileWithVBind(`
`, { + onError, + }) + expect(onError.mock.calls[0][0]).toMatchObject({ code: ErrorCodes.X_V_BIND_NO_EXPRESSION, loc: { @@ -159,15 +168,19 @@ describe('compiler: transform v-bind', () => { end: { line: 1, column: 19 }, }, }) - expect(node.template[0]).toMatchObject({ + expect(ir.template[0]).toMatchObject({ type: IRNodeTypes.TEMPLATE_FACTORY, template: '
', }) + + expect(code).matchSnapshot() + expect(code).contains(JSON.stringify('
')) }) test('.camel modifier', () => { - const node = parseWithVBind(`
`) - expect(node.effect[0].operations[0]).toMatchObject({ + const { ir, code } = compileWithVBind(`
`) + + expect(ir.effect[0].operations[0]).toMatchObject({ key: { content: `fooBar`, isStatic: true, @@ -177,11 +190,15 @@ describe('compiler: transform v-bind', () => { isStatic: false, }, }) + + expect(code).matchSnapshot() + expect(code).contains('_setAttr(n1, "fooBar", undefined, _ctx.id)') }) test('.camel modifier w/ no expression', () => { - const node = parseWithVBind(`
`) - expect(node.effect[0].operations[0]).toMatchObject({ + const { ir, code } = compileWithVBind(`
`) + + expect(ir.effect[0].operations[0]).toMatchObject({ key: { content: `fooBar`, isStatic: true, @@ -191,11 +208,16 @@ describe('compiler: transform v-bind', () => { isStatic: false, }, }) + + expect(code).matchSnapshot() + expect(code).contains('effect') + expect(code).contains('_setAttr(n1, "fooBar", undefined, _ctx.fooBar)') }) test('.camel modifier w/ dynamic arg', () => { - const node = parseWithVBind(`
`) - expect(node.effect[0].operations[0]).toMatchObject({ + const { ir, code } = compileWithVBind(`
`) + + expect(ir.effect[0].operations[0]).toMatchObject({ runtimeCamelize: true, key: { content: `foo`, @@ -206,6 +228,12 @@ describe('compiler: transform v-bind', () => { isStatic: false, }, }) + + expect(code).matchSnapshot() + expect(code).contains('effect') + expect(code).contains( + `_setAttr(n1, _camelize(_ctx.foo), undefined, _ctx.id)`, + ) }) test.todo('.camel modifier w/ dynamic arg + prefixIdentifiers') @@ -219,80 +247,3 @@ describe('compiler: transform v-bind', () => { test.todo('.attr modifier') test.todo('.attr modifier w/ no expression') }) - -// TODO: combine with above -describe('compiler: codegen v-bind', () => { - test('simple expression', () => { - const code = compile(`
`, { - bindingMetadata: { - id: BindingTypes.SETUP_REF, - }, - }) - expect(code).matchSnapshot() - }) - - test('should error if no expression', () => { - const onError = vi.fn() - const code = compile(`
`, { onError }) - - expect(onError.mock.calls[0][0]).toMatchObject({ - code: ErrorCodes.X_V_BIND_NO_EXPRESSION, - loc: { - start: { - line: 1, - column: 6, - }, - end: { - line: 1, - column: 19, - }, - }, - }) - - expect(code).matchSnapshot() - // the arg is static - expect(code).contains(JSON.stringify('
')) - }) - - test('no expression', () => { - const code = compile('
', { - bindingMetadata: { - id: BindingTypes.SETUP_REF, - }, - }) - - expect(code).matchSnapshot() - expect(code).contains('_setAttr(n1, "id", undefined, _ctx.id)') - }) - - test('no expression (shorthand)', () => { - const code = compile('
', { - bindingMetadata: { - camelCase: BindingTypes.SETUP_REF, - }, - }) - - expect(code).matchSnapshot() - expect(code).contains( - '_setAttr(n1, "camel-case", undefined, _ctx.camelCase)', - ) - }) - - test('dynamic arg', () => { - const code = compile('
', { - bindingMetadata: { - id: BindingTypes.SETUP_REF, - }, - }) - - expect(code).matchSnapshot() - expect(code).contains('_setAttr(n1, _ctx.id, undefined, _ctx.id)') - }) - - test('.camel modifier', () => { - const code = compile(`
`) - - expect(code).matchSnapshot() - expect(code).contains('fooBar') - }) -}) From d1dd1e110a1efcb6ed13ba8db024c1c0d588b9b8 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: Sun, 10 Dec 2023 01:36:34 +0800 Subject: [PATCH 15/20] fix(compiler-vapor): add modifier for empty v-on --- packages/compiler-vapor/src/generate.ts | 42 ++++++++++++------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index ba2a3aeb5..b79ac2901 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -494,31 +494,31 @@ function genSetEvent(oper: SetEventIRNode, context: CodegenContext) { }, // 3rd arg: event handler () => { - if (oper.value && oper.value.content.trim()) { - const pushWithKeys = (fn: () => void) => { - push(`${vaporHelper('withKeys')}(`) - fn() - push(`, ${genArrayExpression(keys)})`) - } - const pushWithModifiers = (fn: () => void) => { - push(`${vaporHelper('withModifiers')}(`) - fn() - push(`, ${genArrayExpression(nonKeys)})`) - } - const pushNoop = (fn: () => void) => fn() + const pushWithKeys = (fn: () => void) => { + push(`${vaporHelper('withKeys')}(`) + fn() + push(`, ${genArrayExpression(keys)})`) + } + const pushWithModifiers = (fn: () => void) => { + push(`${vaporHelper('withModifiers')}(`) + fn() + push(`, ${genArrayExpression(nonKeys)})`) + } + const pushNoop = (fn: () => void) => fn() - ;(keys.length ? pushWithKeys : pushNoop)(() => - (nonKeys.length ? pushWithModifiers : pushNoop)(() => { + ;(keys.length ? pushWithKeys : pushNoop)(() => + (nonKeys.length ? pushWithModifiers : pushNoop)(() => { + if (oper.value && oper.value.content.trim()) { push('(...args) => (') - genExpression(oper.value!, context) + genExpression(oper.value, context) push(' && ') - genExpression(oper.value!, context) + genExpression(oper.value, context) push('(...args))') - }), - ) - } else { - push('() => {}') - } + } else { + push('() => {}') + } + }), + ) }, // 4th arg, gen options !!options.length && From 6852ade678099607acace171f7158a4c2226db4f Mon Sep 17 00:00:00 2001 From: GaoNeng-wWw Date: Sat, 9 Dec 2023 11:05:50 +0800 Subject: [PATCH 16/20] feat(runtime-vapor): onMounted and onUnMounted hook --- packages/runtime-vapor/src/apiLifecycle.ts | 65 +++++++++++ packages/runtime-vapor/src/component.ts | 124 ++++++++++++++++++++- packages/runtime-vapor/src/index.ts | 1 + packages/runtime-vapor/src/render.ts | 12 +- 4 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 packages/runtime-vapor/src/apiLifecycle.ts diff --git a/packages/runtime-vapor/src/apiLifecycle.ts b/packages/runtime-vapor/src/apiLifecycle.ts new file mode 100644 index 000000000..299f41342 --- /dev/null +++ b/packages/runtime-vapor/src/apiLifecycle.ts @@ -0,0 +1,65 @@ +import { pauseTracking, resetTracking } from '@vue/reactivity' +import { + ComponentInternalInstance, + currentInstance, + setCurrentInstance, +} from './component' + +export enum LifecycleHooks { + BEFORE_CREATE = 'bc', + CREATED = 'c', + BEFORE_MOUNT = 'bm', + MOUNTED = 'm', + BEFORE_UPDATE = 'bu', + UPDATED = 'u', + BEFORE_UNMOUNT = 'bum', + UNMOUNTED = 'um', + DEACTIVATED = 'da', + ACTIVATED = 'a', + RENDER_TRIGGERED = 'rtg', + RENDER_TRACKED = 'rtc', + ERROR_CAPTURED = 'ec', + SERVER_PREFETCH = 'sp', +} +export const injectHook = ( + type: LifecycleHooks, + hook: Function & { __weh?: Function }, + target: ComponentInternalInstance | null = currentInstance, + prepend: boolean = false, +) => { + if (target) { + const hooks = target[type] || (target[type] = []) + const wrappedHook = + hook.__weh || + (hook.__weh = (...args: unknown[]) => { + if (target.isUnMounted) { + return + } + pauseTracking() + setCurrentInstance(target) + // TODO: call error handling + const res = hook(...args) + resetTracking() + return res + }) + if (prepend) { + hooks.unshift(wrappedHook) + } else { + hooks.push(wrappedHook) + } + return wrappedHook + } else if (__DEV__) { + // TODO: warn need + } +} +export const createHook = + any>(lifecycle: LifecycleHooks) => + (hook: T, target: ComponentInternalInstance | null = currentInstance) => + injectHook(lifecycle, (...args: unknown[]) => hook(...args), target) + +export const onMounted = createHook(LifecycleHooks.MOUNTED) +export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT) +export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE) +export const onUpdated = createHook(LifecycleHooks.UPDATED) +export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT) +export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 5ff3b86b8..7364d90ef 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -12,6 +12,7 @@ import { import type { Data } from '@vue/shared' export type Component = FunctionalComponent | ObjectComponent +import { LifecycleHooks } from './apiLifecycle' export type SetupFn = (props: any, ctx: any) => Block | Data export type FunctionalComponent = SetupFn & { @@ -23,7 +24,7 @@ export interface ObjectComponent { setup: SetupFn render(ctx: any): Block } - +type LifecycleHook = TFn[] | null export interface ComponentInternalInstance { uid: number container: ParentNode @@ -37,7 +38,9 @@ export interface ComponentInternalInstance { // state props: Data + get isUnMounted(): boolean setupState: Data + isUnMountedRef: Ref /** directives */ dirs: Map @@ -46,6 +49,62 @@ export interface ComponentInternalInstance { get isMounted(): boolean isMountedRef: Ref // TODO: registory of provides, appContext, lifecycles, ... + /** + * @internal + */ + [LifecycleHooks.BEFORE_CREATE]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.CREATED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.BEFORE_MOUNT]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.MOUNTED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.BEFORE_UPDATE]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.UPDATED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.UNMOUNTED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.RENDER_TRACKED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.RENDER_TRIGGERED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.ACTIVATED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.DEACTIVATED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.ERROR_CAPTURED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise> } // TODO @@ -67,10 +126,11 @@ export const createComponentInstance = ( component: ObjectComponent | FunctionalComponent, ): ComponentInternalInstance => { const isMountedRef = ref(false) + const isUnMountedRef = ref(false) const instance: ComponentInternalInstance = { uid: uid++, block: null, - container: null!, // set on mount + container: null!, scope: new EffectScope(true /* detached */)!, component, @@ -90,8 +150,68 @@ export const createComponentInstance = ( get isMounted() { return isMountedRef.value }, + get isUnMounted() { + return isUnMountedRef.value + }, isMountedRef, + isUnMountedRef, // TODO: registory of provides, appContext, lifecycles, ... + /** + * @internal + */ + [LifecycleHooks.BEFORE_CREATE]: null, + /** + * @internal + */ + [LifecycleHooks.CREATED]: null, + /** + * @internal + */ + [LifecycleHooks.BEFORE_MOUNT]: null, + /** + * @internal + */ + [LifecycleHooks.MOUNTED]: null, + /** + * @internal + */ + [LifecycleHooks.BEFORE_UPDATE]: null, + /** + * @internal + */ + [LifecycleHooks.UPDATED]: null, + /** + * @internal + */ + [LifecycleHooks.BEFORE_UNMOUNT]: null, + /** + * @internal + */ + [LifecycleHooks.UNMOUNTED]: null, + /** + * @internal + */ + [LifecycleHooks.RENDER_TRACKED]: null, + /** + * @internal + */ + [LifecycleHooks.RENDER_TRIGGERED]: null, + /** + * @internal + */ + [LifecycleHooks.ACTIVATED]: null, + /** + * @internal + */ + [LifecycleHooks.DEACTIVATED]: null, + /** + * @internal + */ + [LifecycleHooks.ERROR_CAPTURED]: null, + /** + * @internal + */ + [LifecycleHooks.SERVER_PREFETCH]: null, } return instance } diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 0c56476ad..69cca595a 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -44,3 +44,4 @@ export * from './scheduler' export * from './directive' export * from './dom' export * from './directives/vShow' +export * from './apiLifecycle' diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index 422d5e689..9cd16b12f 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -15,6 +15,7 @@ import { invokeDirectiveHook } from './directive' import { insert, remove } from './dom' import { PublicInstanceProxyHandlers } from './componentPublicInstance' +import { invokeArrayFns } from '@vue/shared' export type Block = Node | Fragment | Block[] export type ParentBlock = ParentNode | Node[] @@ -36,7 +37,6 @@ export function normalizeContainer(container: string | ParentNode): ParentNode { ? (document.querySelector(container) as ParentNode) : container } - export function mountComponent( instance: ComponentInternalInstance, container: ParentNode, @@ -67,7 +67,10 @@ export function mountComponent( instance.isMountedRef.value = true invokeDirectiveHook(instance, 'mounted') unsetCurrentInstance() - + const { m } = instance + if (m) { + invokeArrayFns(m) + } // TODO: lifecycle hooks (mounted, ...) // const { m } = instance // m && invoke(m) @@ -84,7 +87,10 @@ export function unmountComponent(instance: ComponentInternalInstance) { instance.isMountedRef.value = false invokeDirectiveHook(instance, 'unmounted') unsetCurrentInstance() - + const { um } = instance + if (um) { + invokeArrayFns(um) + } // TODO: lifecycle hooks (unmounted, ...) // const { um } = instance // um && invoke(um) From 98d48e737f06df5108e960b8d14ab05a952cc85d Mon Sep 17 00:00:00 2001 From: GaoNeng-wWw Date: Sat, 9 Dec 2023 12:49:28 +0800 Subject: [PATCH 17/20] styles: Revision review comments --- packages/runtime-vapor/src/apiLifecycle.ts | 2 +- packages/runtime-vapor/src/render.ts | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/runtime-vapor/src/apiLifecycle.ts b/packages/runtime-vapor/src/apiLifecycle.ts index 299f41342..a92ab8162 100644 --- a/packages/runtime-vapor/src/apiLifecycle.ts +++ b/packages/runtime-vapor/src/apiLifecycle.ts @@ -1,6 +1,6 @@ import { pauseTracking, resetTracking } from '@vue/reactivity' import { - ComponentInternalInstance, + type ComponentInternalInstance, currentInstance, setCurrentInstance, } from './component' diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index 9cd16b12f..8b8c9c755 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -71,9 +71,6 @@ export function mountComponent( if (m) { invokeArrayFns(m) } - // TODO: lifecycle hooks (mounted, ...) - // const { m } = instance - // m && invoke(m) return instance } @@ -91,7 +88,4 @@ export function unmountComponent(instance: ComponentInternalInstance) { if (um) { invokeArrayFns(um) } - // TODO: lifecycle hooks (unmounted, ...) - // const { um } = instance - // um && invoke(um) } From 485528439856083dc01f0528af5dacd86ef07d57 Mon Sep 17 00:00:00 2001 From: GaoNeng-wWw Date: Sat, 9 Dec 2023 12:53:23 +0800 Subject: [PATCH 18/20] fix: UnMounted -> Unmounted --- packages/runtime-vapor/src/apiLifecycle.ts | 2 +- packages/runtime-vapor/src/component.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/runtime-vapor/src/apiLifecycle.ts b/packages/runtime-vapor/src/apiLifecycle.ts index a92ab8162..f03ae6f93 100644 --- a/packages/runtime-vapor/src/apiLifecycle.ts +++ b/packages/runtime-vapor/src/apiLifecycle.ts @@ -32,7 +32,7 @@ export const injectHook = ( const wrappedHook = hook.__weh || (hook.__weh = (...args: unknown[]) => { - if (target.isUnMounted) { + if (target.isUnmounted) { return } pauseTracking() diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 7364d90ef..78d7fdde8 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -38,9 +38,9 @@ export interface ComponentInternalInstance { // state props: Data - get isUnMounted(): boolean + get isUnmounted(): boolean setupState: Data - isUnMountedRef: Ref + isUnmountedRef: Ref /** directives */ dirs: Map @@ -126,7 +126,7 @@ export const createComponentInstance = ( component: ObjectComponent | FunctionalComponent, ): ComponentInternalInstance => { const isMountedRef = ref(false) - const isUnMountedRef = ref(false) + const isUnmountedRef = ref(false) const instance: ComponentInternalInstance = { uid: uid++, block: null, @@ -150,11 +150,11 @@ export const createComponentInstance = ( get isMounted() { return isMountedRef.value }, - get isUnMounted() { - return isUnMountedRef.value + get isUnmounted() { + return isUnmountedRef.value }, isMountedRef, - isUnMountedRef, + isUnmountedRef, // TODO: registory of provides, appContext, lifecycles, ... /** * @internal From cef4d28ce0e6f8bb4bedb953b644f7004452b24a Mon Sep 17 00:00:00 2001 From: GaoNeng-wWw Date: Sat, 9 Dec 2023 13:37:02 +0800 Subject: [PATCH 19/20] fix(runtime-vapor): name conflict for enum LifecycleHooks error --- packages/runtime-vapor/src/apiLifecycle.ts | 19 +++---- packages/runtime-vapor/src/component.ts | 58 +++++++++++----------- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/packages/runtime-vapor/src/apiLifecycle.ts b/packages/runtime-vapor/src/apiLifecycle.ts index f03ae6f93..732884ace 100644 --- a/packages/runtime-vapor/src/apiLifecycle.ts +++ b/packages/runtime-vapor/src/apiLifecycle.ts @@ -5,7 +5,7 @@ import { setCurrentInstance, } from './component' -export enum LifecycleHooks { +export enum VaporLifecycleHooks { BEFORE_CREATE = 'bc', CREATED = 'c', BEFORE_MOUNT = 'bm', @@ -21,8 +21,9 @@ export enum LifecycleHooks { ERROR_CAPTURED = 'ec', SERVER_PREFETCH = 'sp', } + export const injectHook = ( - type: LifecycleHooks, + type: VaporLifecycleHooks, hook: Function & { __weh?: Function }, target: ComponentInternalInstance | null = currentInstance, prepend: boolean = false, @@ -53,13 +54,13 @@ export const injectHook = ( } } export const createHook = - any>(lifecycle: LifecycleHooks) => + any>(lifecycle: VaporLifecycleHooks) => (hook: T, target: ComponentInternalInstance | null = currentInstance) => injectHook(lifecycle, (...args: unknown[]) => hook(...args), target) -export const onMounted = createHook(LifecycleHooks.MOUNTED) -export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT) -export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE) -export const onUpdated = createHook(LifecycleHooks.UPDATED) -export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT) -export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED) +export const onMounted = createHook(VaporLifecycleHooks.MOUNTED) +export const onBeforeMount = createHook(VaporLifecycleHooks.BEFORE_MOUNT) +export const onBeforeUpdate = createHook(VaporLifecycleHooks.BEFORE_UPDATE) +export const onUpdated = createHook(VaporLifecycleHooks.UPDATED) +export const onBeforeUnmount = createHook(VaporLifecycleHooks.BEFORE_UNMOUNT) +export const onUnmounted = createHook(VaporLifecycleHooks.UNMOUNTED) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 78d7fdde8..d16694338 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -12,7 +12,7 @@ import { import type { Data } from '@vue/shared' export type Component = FunctionalComponent | ObjectComponent -import { LifecycleHooks } from './apiLifecycle' +import { VaporLifecycleHooks } from './apiLifecycle' export type SetupFn = (props: any, ctx: any) => Block | Data export type FunctionalComponent = SetupFn & { @@ -52,59 +52,59 @@ export interface ComponentInternalInstance { /** * @internal */ - [LifecycleHooks.BEFORE_CREATE]: LifecycleHook + [VaporLifecycleHooks.BEFORE_CREATE]: LifecycleHook /** * @internal */ - [LifecycleHooks.CREATED]: LifecycleHook + [VaporLifecycleHooks.CREATED]: LifecycleHook /** * @internal */ - [LifecycleHooks.BEFORE_MOUNT]: LifecycleHook + [VaporLifecycleHooks.BEFORE_MOUNT]: LifecycleHook /** * @internal */ - [LifecycleHooks.MOUNTED]: LifecycleHook + [VaporLifecycleHooks.MOUNTED]: LifecycleHook /** * @internal */ - [LifecycleHooks.BEFORE_UPDATE]: LifecycleHook + [VaporLifecycleHooks.BEFORE_UPDATE]: LifecycleHook /** * @internal */ - [LifecycleHooks.UPDATED]: LifecycleHook + [VaporLifecycleHooks.UPDATED]: LifecycleHook /** * @internal */ - [LifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook + [VaporLifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook /** * @internal */ - [LifecycleHooks.UNMOUNTED]: LifecycleHook + [VaporLifecycleHooks.UNMOUNTED]: LifecycleHook /** * @internal */ - [LifecycleHooks.RENDER_TRACKED]: LifecycleHook + [VaporLifecycleHooks.RENDER_TRACKED]: LifecycleHook /** * @internal */ - [LifecycleHooks.RENDER_TRIGGERED]: LifecycleHook + [VaporLifecycleHooks.RENDER_TRIGGERED]: LifecycleHook /** * @internal */ - [LifecycleHooks.ACTIVATED]: LifecycleHook + [VaporLifecycleHooks.ACTIVATED]: LifecycleHook /** * @internal */ - [LifecycleHooks.DEACTIVATED]: LifecycleHook + [VaporLifecycleHooks.DEACTIVATED]: LifecycleHook /** * @internal */ - [LifecycleHooks.ERROR_CAPTURED]: LifecycleHook + [VaporLifecycleHooks.ERROR_CAPTURED]: LifecycleHook /** * @internal */ - [LifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise> + [VaporLifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise> } // TODO @@ -159,59 +159,59 @@ export const createComponentInstance = ( /** * @internal */ - [LifecycleHooks.BEFORE_CREATE]: null, + [VaporLifecycleHooks.BEFORE_CREATE]: null, /** * @internal */ - [LifecycleHooks.CREATED]: null, + [VaporLifecycleHooks.CREATED]: null, /** * @internal */ - [LifecycleHooks.BEFORE_MOUNT]: null, + [VaporLifecycleHooks.BEFORE_MOUNT]: null, /** * @internal */ - [LifecycleHooks.MOUNTED]: null, + [VaporLifecycleHooks.MOUNTED]: null, /** * @internal */ - [LifecycleHooks.BEFORE_UPDATE]: null, + [VaporLifecycleHooks.BEFORE_UPDATE]: null, /** * @internal */ - [LifecycleHooks.UPDATED]: null, + [VaporLifecycleHooks.UPDATED]: null, /** * @internal */ - [LifecycleHooks.BEFORE_UNMOUNT]: null, + [VaporLifecycleHooks.BEFORE_UNMOUNT]: null, /** * @internal */ - [LifecycleHooks.UNMOUNTED]: null, + [VaporLifecycleHooks.UNMOUNTED]: null, /** * @internal */ - [LifecycleHooks.RENDER_TRACKED]: null, + [VaporLifecycleHooks.RENDER_TRACKED]: null, /** * @internal */ - [LifecycleHooks.RENDER_TRIGGERED]: null, + [VaporLifecycleHooks.RENDER_TRIGGERED]: null, /** * @internal */ - [LifecycleHooks.ACTIVATED]: null, + [VaporLifecycleHooks.ACTIVATED]: null, /** * @internal */ - [LifecycleHooks.DEACTIVATED]: null, + [VaporLifecycleHooks.DEACTIVATED]: null, /** * @internal */ - [LifecycleHooks.ERROR_CAPTURED]: null, + [VaporLifecycleHooks.ERROR_CAPTURED]: null, /** * @internal */ - [LifecycleHooks.SERVER_PREFETCH]: null, + [VaporLifecycleHooks.SERVER_PREFETCH]: null, } return instance } From 25bdc244d12b8f308641135d086cf0b3d874e38a Mon Sep 17 00:00:00 2001 From: GaoNeng-wWw Date: Sat, 9 Dec 2023 19:45:31 +0800 Subject: [PATCH 20/20] fix(runtime-vapor): set isUnMountedRef to true when unmount component --- packages/runtime-vapor/src/render.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index 8b8c9c755..ed4aa32da 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -88,4 +88,5 @@ export function unmountComponent(instance: ComponentInternalInstance) { if (um) { invokeArrayFns(um) } + instance.isUnmountedRef.value = true }