From 464c0cfe761e591f7c3974ee20f22a74f42df9e4 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: Thu, 14 Mar 2024 15:44:29 +0800 Subject: [PATCH 01/13] wip: createComponent --- .../__snapshots__/compile.spec.ts.snap | 18 +-- .../src/generators/component.ts | 20 +++ .../src/generators/operation.ts | 3 + packages/compiler-vapor/src/ir.ts | 12 ++ .../src/transforms/transformElement.ts | 128 +++++++++++------- .../runtime-vapor/src/apiCreateComponent.ts | 12 ++ packages/runtime-vapor/src/directives.ts | 4 - .../src/helpers/resolveAssets.ts | 7 + packages/runtime-vapor/src/index.ts | 4 +- 9 files changed, 147 insertions(+), 61 deletions(-) create mode 100644 packages/compiler-vapor/src/generators/component.ts create mode 100644 packages/runtime-vapor/src/apiCreateComponent.ts create mode 100644 packages/runtime-vapor/src/helpers/resolveAssets.ts diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap index 8eb9a09ea..407b7690c 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap @@ -120,18 +120,20 @@ export function render(_ctx) { `; exports[`compile > directives > v-pre > should not affect siblings after it 1`] = ` -"import { createTextNode as _createTextNode, insert as _insert, renderEffect as _renderEffect, setText as _setText, setDynamicProp as _setDynamicProp, template as _template } from 'vue/vapor'; +"import { resolveComponent as _resolveComponent, createComponent as _createComponent, createTextNode as _createTextNode, insert as _insert, renderEffect as _renderEffect, setText as _setText, setDynamicProp as _setDynamicProp, template as _template } from 'vue/vapor'; const t0 = _template("
{{ bar }}
") -const t1 = _template("
") +const t1 = _template("
") export function render(_ctx) { const n0 = t0() - const n2 = t1() - const n1 = _createTextNode() - _insert(n1, n2) - _renderEffect(() => _setText(n1, _ctx.bar)) - _renderEffect(() => _setDynamicProp(n2, "id", _ctx.foo)) - return [n0, n2] + const n3 = t1() + const n1 = n3.firstChild + const n1 = _createComponent(_resolveComponent("Comp")) + const n2 = _createTextNode() + _insert(n2, n3) + _renderEffect(() => _setText(n2, _ctx.bar)) + _renderEffect(() => _setDynamicProp(n3, "id", _ctx.foo)) + return [n0, n3] }" `; diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts new file mode 100644 index 000000000..7a76d50ee --- /dev/null +++ b/packages/compiler-vapor/src/generators/component.ts @@ -0,0 +1,20 @@ +import type { CodegenContext } from '../generate' +import type { CreateComponentIRNode } from '../ir' +import { type CodeFragment, NEWLINE, genCall } from './utils' + +export function genCreateComponent( + oper: CreateComponentIRNode, + context: CodegenContext, +): CodeFragment[] { + const { vaporHelper } = context + + const tag = oper.resolve + ? genCall(vaporHelper('resolveComponent'), JSON.stringify(oper.tag)) + : [oper.tag] + + return [ + NEWLINE, + `const n${oper.id} = `, + ...genCall(vaporHelper('createComponent'), tag), + ] +} diff --git a/packages/compiler-vapor/src/generators/operation.ts b/packages/compiler-vapor/src/generators/operation.ts index e08022a7d..59b0816d4 100644 --- a/packages/compiler-vapor/src/generators/operation.ts +++ b/packages/compiler-vapor/src/generators/operation.ts @@ -16,6 +16,7 @@ import { NEWLINE, buildCodeFragment, } from './utils' +import { genCreateComponent } from './component' export function genOperations(opers: OperationNode[], context: CodegenContext) { const [frag, push] = buildCodeFragment() @@ -56,6 +57,8 @@ export function genOperation( return genIf(oper, context) case IRNodeTypes.FOR: return genFor(oper, context) + case IRNodeTypes.CREATE_COMPONENT_NODE: + return genCreateComponent(oper, context) } return [] diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index 6b6de7026..4caf8f8d6 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -29,6 +29,7 @@ export enum IRNodeTypes { INSERT_NODE, PREPEND_NODE, CREATE_TEXT_NODE, + CREATE_COMPONENT_NODE, WITH_DIRECTIVE, @@ -173,6 +174,16 @@ export interface WithDirectiveIRNode extends BaseIRNode { builtin?: VaporHelper } +export interface CreateComponentIRNode extends BaseIRNode { + type: IRNodeTypes.CREATE_COMPONENT_NODE + id: number + tag: string + props: IRProps + // TODO slots + + resolve: boolean +} + export type IRNode = OperationNode | RootIRNode export type OperationNode = | SetPropIRNode @@ -189,6 +200,7 @@ export type OperationNode = | WithDirectiveIRNode | IfIRNode | ForIRNode + | CreateComponentIRNode export enum DynamicFlag { NONE = 0, diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index a0e6146c6..8711f271c 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -29,8 +29,7 @@ export const isReservedProp = /*#__PURE__*/ makeMap( export const transformElement: NodeTransform = (node, context) => { return function postTransformElement() { - node = context.node - + ;({ node } = context) if ( !( node.type === NodeTypes.ELEMENT && @@ -41,37 +40,90 @@ export const transformElement: NodeTransform = (node, context) => { return } - const { tag, props } = node - const isComponent = node.tagType === ElementTypes.COMPONENT + const { tag, tagType } = node + const isComponent = tagType === ElementTypes.COMPONENT + const propsResult = buildProps( + node, + context as TransformContext, + ) + + ;(isComponent ? transformComponentElement : transformNativeElement)( + tag, + propsResult, + context, + ) + } +} - context.template += `<${tag}` - if (props.length) { - buildProps( - node, - context as TransformContext, - undefined, - isComponent, - ) - } - const { scopeId } = context.options - if (scopeId) { - context.template += ` ${scopeId}` - } - context.template += `>` + context.childrenTemplate.join('') +function transformComponentElement( + tag: string, + propsResult: ReturnType, + context: TransformContext, +) { + const { bindingMetadata } = context.options + const resolve = !bindingMetadata[tag] + context.registerOperation({ + type: IRNodeTypes.CREATE_COMPONENT_NODE, + id: context.reference(), + tag, + props: [], + resolve, + }) +} - // TODO remove unnecessary close tag, e.g. if it's the last element of the template - if (!isVoidTag(tag)) { - context.template += `` +function transformNativeElement( + tag: string, + propsResult: ReturnType, + context: TransformContext, +) { + const { scopeId } = context.options + + context.template += `<${tag}` + if (scopeId) context.template += ` ${scopeId}` + + if (propsResult[0] /* dynamic props */) { + const [, dynamicArgs, expressions] = propsResult + context.registerEffect(expressions, [ + { + type: IRNodeTypes.SET_DYNAMIC_PROPS, + element: context.reference(), + props: dynamicArgs, + }, + ]) + } else { + for (const prop of propsResult[1]) { + const { key, values } = prop + if (key.isStatic && values.length === 1 && values[0].isStatic) { + context.template += ` ${key.content}` + if (values[0].content) context.template += `="${values[0].content}"` + } else { + context.registerEffect(values, [ + { + type: IRNodeTypes.SET_PROP, + element: context.reference(), + prop, + }, + ]) + } } } + + context.template += `>` + context.childrenTemplate.join('') + // TODO remove unnecessary close tag, e.g. if it's the last element of the template + if (!isVoidTag(tag)) { + context.template += `` + } } function buildProps( node: ElementNode, context: TransformContext, - props: (VaporDirectiveNode | AttributeNode)[] = node.props as any, - isComponent: boolean, -) { +): + | [dynamic: true, props: IRProps[], expressions: SimpleExpressionNode[]] + | [dynamic: false, props: IRProp[]] { + const props = node.props as (VaporDirectiveNode | AttributeNode)[] + if (props.length === 0) return [false, []] + const dynamicArgs: IRProps[] = [] const dynamicExpr: SimpleExpressionNode[] = [] let results: DirectiveTransformResult[] = [] @@ -112,31 +164,11 @@ function buildProps( if (dynamicArgs.length || results.some(({ key }) => !key.isStatic)) { // take rest of props as dynamic props pushMergeArg() - context.registerEffect(dynamicExpr, [ - { - type: IRNodeTypes.SET_DYNAMIC_PROPS, - element: context.reference(), - props: dynamicArgs, - }, - ]) - } else { - const irProps = dedupeProperties(results) - for (const prop of irProps) { - const { key, values } = prop - if (key.isStatic && values.length === 1 && values[0].isStatic) { - context.template += ` ${key.content}` - if (values[0].content) context.template += `="${values[0].content}"` - } else { - context.registerEffect(values, [ - { - type: IRNodeTypes.SET_PROP, - element: context.reference(), - prop, - }, - ]) - } - } + return [true, dynamicArgs, dynamicExpr] } + + const irProps = dedupeProperties(results) + return [false, irProps] } function transformProp( diff --git a/packages/runtime-vapor/src/apiCreateComponent.ts b/packages/runtime-vapor/src/apiCreateComponent.ts new file mode 100644 index 000000000..0a657f539 --- /dev/null +++ b/packages/runtime-vapor/src/apiCreateComponent.ts @@ -0,0 +1,12 @@ +import type { Data } from '@vue/shared' +import type { Component } from './component' +import { render } from './render' + +export function createComponent( + comp: Component, + props: Data = {}, + container: string | ParentNode, +) { + const instance = render(comp, props, container) + return instance.block +} diff --git a/packages/runtime-vapor/src/directives.ts b/packages/runtime-vapor/src/directives.ts index 9bff0a8eb..15d2119c4 100644 --- a/packages/runtime-vapor/src/directives.ts +++ b/packages/runtime-vapor/src/directives.ts @@ -145,7 +145,3 @@ function callDirectiveHook( ]) resetTracking() } - -export function resolveDirective() { - // TODO -} diff --git a/packages/runtime-vapor/src/helpers/resolveAssets.ts b/packages/runtime-vapor/src/helpers/resolveAssets.ts new file mode 100644 index 000000000..35ae0bb9a --- /dev/null +++ b/packages/runtime-vapor/src/helpers/resolveAssets.ts @@ -0,0 +1,7 @@ +export function resolveComponent() { + // TODO +} + +export function resolveDirective() { + // TODO +} diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index f3891c4a4..bae07bf63 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -62,7 +62,6 @@ export { } from './apiWatch' export { withDirectives, - resolveDirective, type Directive, type DirectiveBinding, type DirectiveHook, @@ -105,6 +104,9 @@ export { } from './apiLifecycle' export { createIf } from './apiCreateIf' export { createFor } from './apiCreateFor' +export { createComponent } from './apiCreateComponent' + +export { resolveComponent, resolveDirective } from './helpers/resolveAssets' // **Internal** DOM-only runtime directive helpers export { From c18e994b8855d4e05921fda39b6631ffdf907a0d 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: Thu, 14 Mar 2024 16:19:45 +0800 Subject: [PATCH 02/13] feat: basic component support --- .../__snapshots__/compile.spec.ts.snap | 3 +- .../compiler-vapor/__tests__/compile.spec.ts | 1 - .../src/transforms/transformElement.ts | 3 + packages/runtime-vapor/__tests__/_utils.ts | 4 +- .../runtime-vapor/src/apiCreateComponent.ts | 22 ++-- packages/runtime-vapor/src/component.ts | 103 +++++++++++++----- packages/runtime-vapor/src/index.ts | 4 +- packages/runtime-vapor/src/render.ts | 38 +------ playground/src/components.vue | 10 ++ playground/src/main.ts | 4 +- playground/src/sub-comp.vue | 10 ++ 11 files changed, 120 insertions(+), 82 deletions(-) create mode 100644 playground/src/components.vue create mode 100644 playground/src/sub-comp.vue diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap index 407b7690c..f5058d890 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap @@ -127,10 +127,9 @@ const t1 = _template("
") export function render(_ctx) { const n0 = t0() const n3 = t1() - const n1 = n3.firstChild const n1 = _createComponent(_resolveComponent("Comp")) const n2 = _createTextNode() - _insert(n2, n3) + _insert([n1, n2], n3) _renderEffect(() => _setText(n2, _ctx.bar)) _renderEffect(() => _setDynamicProp(n3, "id", _ctx.foo)) return [n0, n3] diff --git a/packages/compiler-vapor/__tests__/compile.spec.ts b/packages/compiler-vapor/__tests__/compile.spec.ts index da6d96210..b406d9e95 100644 --- a/packages/compiler-vapor/__tests__/compile.spec.ts +++ b/packages/compiler-vapor/__tests__/compile.spec.ts @@ -72,7 +72,6 @@ describe('compile', () => { expect(code).not.contains('effect') }) - // TODO: support multiple root nodes and components test('should not affect siblings after it', () => { const code = compile( `
{{ bar }}
\n` + diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index 8711f271c..97cb4ffba 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -15,6 +15,7 @@ import type { TransformContext, } from '../transform' import { + DynamicFlag, IRNodeTypes, type IRProp, type IRProps, @@ -62,10 +63,12 @@ function transformComponentElement( ) { const { bindingMetadata } = context.options const resolve = !bindingMetadata[tag] + context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT context.registerOperation({ type: IRNodeTypes.CREATE_COMPONENT_NODE, id: context.reference(), tag, + // TODO props: [], resolve, }) diff --git a/packages/runtime-vapor/__tests__/_utils.ts b/packages/runtime-vapor/__tests__/_utils.ts index 9117cc677..5790c12c0 100644 --- a/packages/runtime-vapor/__tests__/_utils.ts +++ b/packages/runtime-vapor/__tests__/_utils.ts @@ -4,8 +4,8 @@ import { type ObjectComponent, type SetupFn, render as _render, - createComponentInstance, defineComponent, + test_createComponent, } from '../src' export function makeRender( @@ -31,7 +31,7 @@ export function makeRender( props: Data = {}, container: string | ParentNode = '#host', ) => { - instance = createComponentInstance(component, props) + instance = test_createComponent(component, props) _render(instance, container) return res() } diff --git a/packages/runtime-vapor/src/apiCreateComponent.ts b/packages/runtime-vapor/src/apiCreateComponent.ts index 0a657f539..801496270 100644 --- a/packages/runtime-vapor/src/apiCreateComponent.ts +++ b/packages/runtime-vapor/src/apiCreateComponent.ts @@ -1,12 +1,18 @@ import type { Data } from '@vue/shared' -import type { Component } from './component' -import { render } from './render' +import { + type Component, + createComponentInstance, + currentInstance, + setupComponent, +} from './component' + +export function createComponent(comp: Component, props: Data = {}) { + const current = currentInstance! + const instance = createComponentInstance(comp, props) + setupComponent(instance) + + // register sub-component with current component for lifecycle management + current.comps.add(instance) -export function createComponent( - comp: Component, - props: Data = {}, - container: string | ParentNode, -) { - const instance = render(comp, props, container) return instance.block } diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 60f9fd298..79f3ce2cf 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -1,7 +1,7 @@ -import { EffectScope } from '@vue/reactivity' +import { EffectScope, proxyRefs } from '@vue/reactivity' -import { EMPTY_OBJ, isFunction } from '@vue/shared' -import type { Block } from './render' +import { EMPTY_OBJ, isArray, isFunction, isObject } from '@vue/shared' +import { type Block, fragmentKey } from './render' import type { DirectiveBinding } from './directives' import { type ComponentPropsOptions, @@ -37,33 +37,30 @@ type LifecycleHook = TFn[] | null export interface ComponentInternalInstance { uid: number - container: ParentNode + vapor: true + block: Block | null + container: ParentNode + parent: ComponentInternalInstance | null + scope: EffectScope component: FunctionalComponent | ObjectComponent + comps: Set + dirs: Map - // TODO: ExtraProps: key, ref, ... rawProps: { [key: string]: any } - // normalized options propsOptions: NormalizedPropsOptions emitsOptions: ObjectEmitsOptions | null - parent: ComponentInternalInstance | null - // state - props: Data - attrs: Data setupState: Data + props: Data emit: EmitFn emitted: Record | null + attrs: Data refs: Data - vapor: true - - /** directives */ - dirs: Map - // lifecycle isMounted: boolean isUnmounted: boolean @@ -141,37 +138,37 @@ export const unsetCurrentInstance = () => { } let uid = 0 -export const createComponentInstance = ( +export function createComponentInstance( component: ObjectComponent | FunctionalComponent, rawProps: Data, -): ComponentInternalInstance => { +): ComponentInternalInstance { const instance: ComponentInternalInstance = { uid: uid++, + vapor: true, + block: null, - container: null!, // set on mountComponent - scope: new EffectScope(true /* detached */)!, - component, - rawProps, + container: null!, - // TODO: registory of parent + // TODO parent: null, + scope: new EffectScope(true /* detached */)!, + component, + comps: new Set(), + dirs: new Map(), + + rawProps, // resolved props and emits options propsOptions: normalizePropsOptions(component), emitsOptions: normalizeEmitsOptions(component), - // emit - emit: null!, // to be set immediately - emitted: null, - // state + setupState: EMPTY_OBJ, props: EMPTY_OBJ, + emit: null!, + emitted: null, attrs: EMPTY_OBJ, - setupState: EMPTY_OBJ, refs: EMPTY_OBJ, - vapor: true, - - dirs: new Map(), // lifecycle isMounted: false, @@ -234,3 +231,49 @@ export const createComponentInstance = ( return instance } + +export function setupComponent(instance: ComponentInternalInstance): void { + const reset = setCurrentInstance(instance) + instance.scope.run(() => { + const { component, props, emit, attrs } = instance + const ctx = { expose: () => {}, emit, attrs } + + const setupFn = isFunction(component) ? component : component.setup + const stateOrNode = setupFn && setupFn(props, ctx) + + let block: Block | undefined + + if ( + stateOrNode && + (stateOrNode instanceof Node || + isArray(stateOrNode) || + (stateOrNode as any)[fragmentKey]) + ) { + block = stateOrNode as Block + } else if (isObject(stateOrNode)) { + instance.setupState = proxyRefs(stateOrNode) + } + if (!block && component.render) { + block = component.render(instance.setupState) + } + + if (block instanceof DocumentFragment) { + block = Array.from(block.childNodes) + } + if (!block) { + // TODO: warn no template + block = [] + } + return (instance.block = block) + }) + reset() +} + +export function test_createComponent( + component: ObjectComponent | FunctionalComponent, + rawProps: Data, +) { + const instance = createComponentInstance(component, rawProps) + setupComponent(instance) + return instance +} diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index bae07bf63..272c8b7f3 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -87,7 +87,6 @@ export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event' export { setRef } from './dom/templateRef' export { defineComponent } from './apiDefineComponent' -export { createComponentInstance } from './component' export { onBeforeMount, onMounted, @@ -106,6 +105,9 @@ export { createIf } from './apiCreateIf' export { createFor } from './apiCreateFor' export { createComponent } from './apiCreateComponent' +// TODO remove +export { test_createComponent } from './component' + export { resolveComponent, resolveDirective } from './helpers/resolveAssets' // **Internal** DOM-only runtime directive helpers diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index 893b046f4..7c34356a1 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -1,5 +1,4 @@ -import { proxyRefs } from '@vue/reactivity' -import { invokeArrayFns, isArray, isFunction, isObject } from '@vue/shared' +import { invokeArrayFns } from '@vue/shared' import { type ComponentInternalInstance, setCurrentInstance, @@ -37,47 +36,14 @@ function mountComponent( container: ParentNode, ) { instance.container = container - const reset = setCurrentInstance(instance) - const block = instance.scope.run(() => { - const { component, props, emit, attrs } = instance - const ctx = { expose: () => {}, emit, attrs } - - const setupFn = isFunction(component) ? component : component.setup - const stateOrNode = setupFn && setupFn(props, ctx) - - let block: Block | undefined - - if ( - stateOrNode && - (stateOrNode instanceof Node || - isArray(stateOrNode) || - (stateOrNode as any)[fragmentKey]) - ) { - block = stateOrNode as Block - } else if (isObject(stateOrNode)) { - instance.setupState = proxyRefs(stateOrNode) - } - if (!block && component.render) { - block = component.render(instance.setupState) - } - - if (block instanceof DocumentFragment) { - block = Array.from(block.childNodes) - } - if (!block) { - // TODO: warn no template - block = [] - } - return (instance.block = block) - })! const { bm, m } = instance // hook: beforeMount bm && invokeArrayFns(bm) invokeDirectiveHook(instance, 'beforeMount') - insert(block, instance.container) + insert(instance.block!, instance.container) instance.isMounted = true // hook: mounted diff --git a/playground/src/components.vue b/playground/src/components.vue new file mode 100644 index 000000000..4f85a0d49 --- /dev/null +++ b/playground/src/components.vue @@ -0,0 +1,10 @@ + + + diff --git a/playground/src/main.ts b/playground/src/main.ts index e8f26d40f..10c4a1c1a 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -1,4 +1,4 @@ -import { createComponentInstance, render, unmountComponent } from 'vue/vapor' +import { render, test_createComponent, unmountComponent } from 'vue/vapor' import { createApp } from 'vue' import './style.css' @@ -7,7 +7,7 @@ const mod = (modules['.' + location.pathname] || modules['./App.vue'])() mod.then(({ default: mod }) => { if (mod.vapor) { - const instance = createComponentInstance(mod, {}) + const instance = test_createComponent(mod, {}) render(instance, '#app') // @ts-expect-error globalThis.unmount = () => { diff --git a/playground/src/sub-comp.vue b/playground/src/sub-comp.vue new file mode 100644 index 000000000..c212a07c8 --- /dev/null +++ b/playground/src/sub-comp.vue @@ -0,0 +1,10 @@ + + + From f6d5f7167e2619feb85c1952598b326c79f7e3b8 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: Thu, 14 Mar 2024 17:22:14 +0800 Subject: [PATCH 03/13] fix: repl --- packages/sfc-playground/src/App.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/sfc-playground/src/App.vue b/packages/sfc-playground/src/App.vue index a3525036c..37ef14c22 100644 --- a/packages/sfc-playground/src/App.vue +++ b/packages/sfc-playground/src/App.vue @@ -120,9 +120,10 @@ watch( files.value['src/index.html'] = new File( 'src/index.html', ` diff --git a/playground/src/sub-comp.vue b/playground/src/sub-comp.vue index c212a07c8..2f050140c 100644 --- a/playground/src/sub-comp.vue +++ b/playground/src/sub-comp.vue @@ -1,10 +1,24 @@ From 52255967c3cbc1ced186dc2a219035d32d924738 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, 16 Mar 2024 16:50:06 +0800 Subject: [PATCH 09/13] refactor: update attrs --- packages/runtime-vapor/src/componentAttrs.ts | 43 ++++++ packages/runtime-vapor/src/componentProps.ts | 138 +++++++------------ packages/runtime-vapor/src/renderWatch.ts | 3 - 3 files changed, 92 insertions(+), 92 deletions(-) create mode 100644 packages/runtime-vapor/src/componentAttrs.ts diff --git a/packages/runtime-vapor/src/componentAttrs.ts b/packages/runtime-vapor/src/componentAttrs.ts new file mode 100644 index 000000000..7162c0281 --- /dev/null +++ b/packages/runtime-vapor/src/componentAttrs.ts @@ -0,0 +1,43 @@ +import { isFunction } from '@vue/shared' +import type { ComponentInternalInstance } from './component' +import { isEmitListener } from './componentEmits' + +export function patchAttrs(instance: ComponentInternalInstance) { + const attrs = instance.attrs + const options = instance.propsOptions[0] + + const keys = new Set() + for (const props of Array.from(instance.rawProps).reverse()) { + if (isFunction(props)) { + const resolved = props() + for (const rawKey in resolved) { + registerAttr(rawKey, () => resolved[rawKey]) + } + } else { + for (const rawKey in props) { + registerAttr(rawKey, props[rawKey]) + } + } + } + + for (const key in attrs) { + if (!keys.has(key)) { + delete attrs[key] + } + } + + function registerAttr(key: string, getter: () => unknown) { + if ( + (!options || !(key in options)) && + !isEmitListener(instance.emitsOptions, key) + ) { + keys.add(key) + if (key in attrs) return + Object.defineProperty(attrs, key, { + get: getter, + enumerable: true, + configurable: true, + }) + } + } +} diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index 6b228acb0..157fdeaa6 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -9,14 +9,16 @@ import { isArray, isFunction, } from '@vue/shared' -import { shallowReactive } from '@vue/reactivity' +import { baseWatch, shallowReactive } from '@vue/reactivity' import { warn } from './warning' import { type Component, type ComponentInternalInstance, setCurrentInstance, } from './component' -import { isEmitListener } from './componentEmits' +import { patchAttrs } from './componentAttrs' +import { watchEffect } from './apiWatch' +import { createVaporPreScheduler } from './scheduler' export type ComponentPropsOptions

= | ComponentObjectPropsOptions

@@ -82,28 +84,34 @@ export function initProps( isStateful: boolean, ) { const props: Data = {} + const attrs = (instance.attrs = shallowReactive({})) if (!rawProps) rawProps = [] else if (!isArray(rawProps)) rawProps = [rawProps] instance.rawProps = rawProps - const [options, needCastKeys] = instance.propsOptions + const [options] = instance.propsOptions if (options) { - const hasDynamicProps = rawProps.length > 1 + const hasDynamicProps = rawProps.some(isFunction) if (hasDynamicProps) { for (const key in options) { const getter = () => - resolveDynamicProp(rawProps as NormalizedRawProps, key) - registerProp(key, getter, true) + getDynamicPropValue(rawProps as NormalizedRawProps, key) + registerProp(instance, props, key, getter, true) } } else { for (const key in options) { const rawKey = getRawKey(rawProps[0] as StaticProps, key) if (rawKey) { - registerProp(key, (rawProps[0] as StaticProps)[rawKey]) + registerProp( + instance, + props, + key, + (rawProps[0] as StaticProps)[rawKey], + ) } else { - registerProp(key, undefined, false, true) + registerProp(instance, props, key, undefined, false, true) } } } @@ -114,8 +122,9 @@ export function initProps( validateProps(rawProps, props, options || {}) } - const attrs: Data = (instance.attrs = shallowReactive({})) - patchAttrs(instance) + baseWatch(() => patchAttrs(instance), undefined, { + scheduler: createVaporPreScheduler(instance), + }) if (isStateful) { instance.props = props @@ -123,88 +132,39 @@ export function initProps( // functional w/ optional props, props === attrs instance.props = instance.propsOptions === EMPTY_ARR ? attrs : props } - - function registerProp( - rawKey: string, - getter: () => DynamicPropResult, - isDynamic: true, - isAbsent?: boolean, - ): void - function registerProp( - rawKey: string, - getter?: () => any, - isDynamic?: false, - isAbsent?: boolean, - ): void - function registerProp( - rawKey: string, - getter?: (() => unknown) | (() => DynamicPropResult), - isDynamic?: boolean, - isAbsent?: boolean, - ) { - const key = camelize(rawKey) - if (key in props) return - - const needCast = needCastKeys && needCastKeys.includes(key) - const withCast = (value: unknown, absent?: boolean) => - resolvePropValue(options!, props, key, value, instance, absent) - - if (isAbsent) { - props[key] = needCast ? withCast(undefined, true) : undefined - } else { - const get: () => unknown = isDynamic - ? needCast - ? () => withCast(...(getter!() as DynamicPropResult)) - : () => (getter!() as DynamicPropResult)[0] - : needCast - ? () => withCast(getter!()) - : getter! - - Object.defineProperty(props, key, { - get, - enumerable: true, - }) - } - } } -export function patchAttrs(instance: ComponentInternalInstance) { - const attrs = instance.attrs - const options = instance.propsOptions[0] - - const keys = new Set() - for (const props of Array.from(instance.rawProps).reverse()) { - if (isFunction(props)) { - const resolved = props() - for (const rawKey in resolved) { - registerAttr(rawKey, () => resolved[rawKey]) - } - } else { - for (const rawKey in props) { - registerAttr(rawKey, props[rawKey]) - } - } - } +function registerProp( + instance: ComponentInternalInstance, + props: Data, + rawKey: string, + getter?: (() => unknown) | (() => DynamicPropResult), + isDynamic?: boolean, + isAbsent?: boolean, +) { + const key = camelize(rawKey) + if (key in props) return - for (const key in attrs) { - if (!keys.has(key)) { - delete attrs[key] - } - } + const [options, needCastKeys] = instance.propsOptions + const needCast = needCastKeys && needCastKeys.includes(key) + const withCast = (value: unknown, absent?: boolean) => + resolvePropValue(options!, props, key, value, instance, absent) - function registerAttr(key: string, getter: () => unknown) { - if ( - (!options || !(key in options)) && - !isEmitListener(instance.emitsOptions, key) - ) { - keys.add(key) - if (key in attrs) return - Object.defineProperty(attrs, key, { - get: getter, - enumerable: true, - configurable: true, - }) - } + if (isAbsent) { + props[key] = needCast ? withCast(undefined, true) : undefined + } else { + const get: () => unknown = isDynamic + ? needCast + ? () => withCast(...(getter!() as DynamicPropResult)) + : () => (getter!() as DynamicPropResult)[0] + : needCast + ? () => withCast(getter!()) + : getter! + + Object.defineProperty(props, key, { + get, + enumerable: true, + }) } } @@ -213,7 +173,7 @@ function getRawKey(obj: Data, key: string) { } type DynamicPropResult = [value: unknown, absent: boolean] -function resolveDynamicProp( +function getDynamicPropValue( rawProps: NormalizedRawProps, key: string, ): DynamicPropResult { diff --git a/packages/runtime-vapor/src/renderWatch.ts b/packages/runtime-vapor/src/renderWatch.ts index 250634906..71364037f 100644 --- a/packages/runtime-vapor/src/renderWatch.ts +++ b/packages/runtime-vapor/src/renderWatch.ts @@ -17,7 +17,6 @@ import { import { handleError as handleErrorWithInstance } from './errorHandling' import { warn } from './warning' import { invokeDirectiveHook } from './directives' -import { patchAttrs } from './componentProps' interface RenderWatchOptions { immediate?: boolean @@ -83,8 +82,6 @@ const createMiddleware = // beforeUpdate hook const isFirstEffect = !instance.isUpdating if (isFirstEffect) { - // TODO - patchAttrs(instance) if (bu) { invokeArrayFns(bu) } From 71c9d27524e11fbf303abe2175cecb1c39e00b2d 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, 16 Mar 2024 17:00:38 +0800 Subject: [PATCH 10/13] chore: wip emits --- packages/runtime-vapor/src/componentEmits.ts | 3 +++ packages/runtime-vapor/src/componentProps.ts | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/runtime-vapor/src/componentEmits.ts b/packages/runtime-vapor/src/componentEmits.ts index 8b075281c..bbca2044c 100644 --- a/packages/runtime-vapor/src/componentEmits.ts +++ b/packages/runtime-vapor/src/componentEmits.ts @@ -1,5 +1,8 @@ // NOTE: runtime-core/src/componentEmits.ts +// TODO WIP +// @ts-nocheck + import { EMPTY_OBJ, type UnionToIntersection, diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index 157fdeaa6..b6734c39a 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -17,7 +17,6 @@ import { setCurrentInstance, } from './component' import { patchAttrs } from './componentAttrs' -import { watchEffect } from './apiWatch' import { createVaporPreScheduler } from './scheduler' export type ComponentPropsOptions

= From 83e4102320903110f88fee39e9c740483ed63723 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, 16 Mar 2024 17:39:23 +0800 Subject: [PATCH 11/13] feat: lifecycle --- packages/runtime-vapor/src/apiRender.ts | 38 +++++-------------- .../runtime-vapor/src/componentLifecycle.ts | 34 +++++++++++++++++ playground/src/components.vue | 13 ++++++- playground/src/sub-comp.vue | 14 ++++++- 4 files changed, 69 insertions(+), 30 deletions(-) create mode 100644 packages/runtime-vapor/src/componentLifecycle.ts diff --git a/packages/runtime-vapor/src/apiRender.ts b/packages/runtime-vapor/src/apiRender.ts index 4ca6cef24..94efce891 100644 --- a/packages/runtime-vapor/src/apiRender.ts +++ b/packages/runtime-vapor/src/apiRender.ts @@ -1,13 +1,10 @@ -import { invokeArrayFns, isArray, isFunction, isObject } from '@vue/shared' -import { - type ComponentInternalInstance, - setCurrentInstance, - unsetCurrentInstance, -} from './component' -import { invokeDirectiveHook } from './directives' +import { isArray, isFunction, isObject } from '@vue/shared' +import { type ComponentInternalInstance, setCurrentInstance } from './component' import { insert, querySelector, remove } from './dom/element' import { flushPostFlushCbs, queuePostRenderEffect } from './scheduler' import { proxyRefs } from '@vue/reactivity' +import { invokeLifecycle } from './componentLifecycle' +import { VaporLifecycleHooks } from './apiLifecycle' export const fragmentKey = Symbol(__DEV__ ? `fragmentKey` : ``) @@ -74,44 +71,29 @@ function mountComponent( container: ParentNode, ) { instance.container = container - const reset = setCurrentInstance(instance) - const { bm, m } = instance // hook: beforeMount - bm && invokeArrayFns(bm) - invokeDirectiveHook(instance, 'beforeMount') + invokeLifecycle(instance, VaporLifecycleHooks.BEFORE_MOUNT, 'beforeMount') insert(instance.block!, instance.container) instance.isMounted = true // hook: mounted - queuePostRenderEffect(() => { - invokeDirectiveHook(instance, 'mounted') - // TODO - for (const com of instance.comps) { - com.isMounted = true - } - m && invokeArrayFns(m) - }) - reset() + invokeLifecycle(instance, VaporLifecycleHooks.MOUNTED, 'mounted', true) return instance } export function unmountComponent(instance: ComponentInternalInstance) { - const { container, block, scope, um, bum } = instance + const { container, block, scope } = instance // hook: beforeUnmount - bum && invokeArrayFns(bum) - invokeDirectiveHook(instance, 'beforeUnmount') + invokeLifecycle(instance, VaporLifecycleHooks.BEFORE_UNMOUNT, 'beforeUnmount') scope.stop() block && remove(block, container) - instance.isMounted = false - instance.isUnmounted = true // hook: unmounted - invokeDirectiveHook(instance, 'unmounted') - um && invokeArrayFns(um) - unsetCurrentInstance() + invokeLifecycle(instance, VaporLifecycleHooks.UNMOUNTED, 'unmounted', true) + queuePostRenderEffect(() => (instance.isUnmounted = true)) } diff --git a/packages/runtime-vapor/src/componentLifecycle.ts b/packages/runtime-vapor/src/componentLifecycle.ts new file mode 100644 index 000000000..2dd94b1d5 --- /dev/null +++ b/packages/runtime-vapor/src/componentLifecycle.ts @@ -0,0 +1,34 @@ +import { invokeArrayFns } from '@vue/shared' +import type { VaporLifecycleHooks } from './apiLifecycle' +import { type ComponentInternalInstance, setCurrentInstance } from './component' +import { queuePostRenderEffect } from './scheduler' +import { type DirectiveHookName, invokeDirectiveHook } from './directives' + +export function invokeLifecycle( + instance: ComponentInternalInstance, + lifecycle: VaporLifecycleHooks, + directive: DirectiveHookName, + post?: boolean, +) { + invokeArrayFns(post ? [invokeSub, invokeCurrent] : [invokeCurrent, invokeSub]) + + function invokeCurrent() { + const hooks = instance[lifecycle] + if (hooks) { + const fn = () => { + const reset = setCurrentInstance(instance) + invokeArrayFns(hooks) + reset() + } + post ? queuePostRenderEffect(fn) : fn() + } + + invokeDirectiveHook(instance, directive) + } + + function invokeSub() { + instance.comps.forEach(comp => + invokeLifecycle(comp, lifecycle, directive, post), + ) + } +} diff --git a/playground/src/components.vue b/playground/src/components.vue index 2dee8bdd3..b8a8f4ae0 100644 --- a/playground/src/components.vue +++ b/playground/src/components.vue @@ -1,6 +1,12 @@