From 5f90f8bef0bb7c03c91d355983ab7107d08839fc 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: Fri, 8 Dec 2023 17:34:33 +0800 Subject: [PATCH] feat: v-show --- .../__snapshots__/compile.test.ts.snap | 10 +-- packages/compiler-vapor/src/compile.ts | 2 + packages/compiler-vapor/src/generate.ts | 25 ++++--- .../src/transforms/transformElement.ts | 2 +- .../compiler-vapor/src/transforms/vShow.ts | 19 +++++ packages/runtime-vapor/src/component.ts | 15 ++-- .../src/{directives.ts => directive.ts} | 71 ++++++++++++------- .../runtime-vapor/src/directives/vShow.ts | 23 ++++++ packages/runtime-vapor/src/index.ts | 3 +- packages/runtime-vapor/src/render.ts | 6 +- playground/src/show.vue | 13 ++++ 11 files changed, 141 insertions(+), 48 deletions(-) create mode 100644 packages/compiler-vapor/src/transforms/vShow.ts rename packages/runtime-vapor/src/{directives.ts => directive.ts} (65%) create mode 100644 packages/runtime-vapor/src/directives/vShow.ts create mode 100644 playground/src/show.vue diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap index 11119db64..5e3b020fa 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap @@ -35,7 +35,7 @@ export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _withDirectives(n1, [[_ctx.vExample, _ctx.msg]]) + _withDirectives(n1, [[_ctx.vExample, () => _ctx.msg]]) return n0 }" `; @@ -47,7 +47,7 @@ export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _withDirectives(n1, [[_ctx.vExample, _ctx.msg, _ctx.foo]]) + _withDirectives(n1, [[_ctx.vExample, () => _ctx.msg, _ctx.foo]]) return n0 }" `; @@ -59,7 +59,7 @@ export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _withDirectives(n1, [[_ctx.vExample, _ctx.msg, void 0, { bar: true }]]) + _withDirectives(n1, [[_ctx.vExample, () => _ctx.msg, void 0, { bar: true }]]) return n0 }" `; @@ -83,7 +83,7 @@ export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _withDirectives(n1, [[_ctx.vExample, _ctx.msg, "foo"]]) + _withDirectives(n1, [[_ctx.vExample, () => _ctx.msg, "foo"]]) return n0 }" `; @@ -95,7 +95,7 @@ export function render(_ctx) { const t0 = _template("
") const n0 = t0() const { 0: [n1],} = _children(n0) - _withDirectives(n1, [[_ctx.vExample, _ctx.msg, "foo", { bar: true }]]) + _withDirectives(n1, [[_ctx.vExample, () => _ctx.msg, "foo", { bar: true }]]) return n0 }" `; diff --git a/packages/compiler-vapor/src/compile.ts b/packages/compiler-vapor/src/compile.ts index e386de3b3..0d37278d2 100644 --- a/packages/compiler-vapor/src/compile.ts +++ b/packages/compiler-vapor/src/compile.ts @@ -20,6 +20,7 @@ import { transformVHtml } from './transforms/vHtml' import { transformVText } from './transforms/vText' import { transformVBind } from './transforms/vBind' import { transformVOn } from './transforms/vOn' +import { transformVShow } from './transforms/vShow' import { transformInterpolation } from './transforms/transformInterpolation' import type { HackOptions } from './ir' @@ -97,6 +98,7 @@ export function getBaseTransformPreset( on: transformVOn, html: transformVHtml, text: transformVText, + show: transformVShow, }, ] } diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index fee38022b..bdb4f8820 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 } from '@vue/shared' +import { camelize, isString, makeMap } from '@vue/shared' import type { Identifier } from '@babel/types' // remove when stable @@ -475,16 +475,20 @@ function genWithDirective(oper: WithDirectiveIRNode, context: CodegenContext) { // TODO merge directive for the same node pushWithNewline(`${vaporHelper('withDirectives')}(n${oper.element}, [[`) - // TODO resolve directive - const directiveReference = camelize(`v-${dir.name}`) - if (bindingMetadata[directiveReference]) { - const directiveExpression = createSimpleExpression(directiveReference) - directiveExpression.ast = null - genExpression(directiveExpression, context) + 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(', ') + push(', () => ') genExpression(dir.exp, context) } else if (dir.arg || dir.modifiers.length) { push(', void 0') @@ -512,6 +516,8 @@ function genArrayExpression(elements: string[]) { return `[${elements.map((it) => JSON.stringify(it)).join(', ')}]` } +const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this') + function genExpression(node: IRExpression, context: CodegenContext): void { const { push } = context if (isString(node)) return push(node) @@ -525,7 +531,8 @@ function genExpression(node: IRExpression, context: CodegenContext): void { !context.prefixIdentifiers || !node.content.trim() || // there was a parsing error - ast === false + ast === false || + isLiteralWhitelisted(rawExpr) ) { return push(rawExpr, NewlineType.None, loc) } diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index ea468d29b..4bca80478 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -74,7 +74,7 @@ function transformProp( type: IRNodeTypes.WITH_DIRECTIVE, element: context.reference(), dir: prop, - loc: loc, + loc, }) } } diff --git a/packages/compiler-vapor/src/transforms/vShow.ts b/packages/compiler-vapor/src/transforms/vShow.ts new file mode 100644 index 000000000..1604b27e7 --- /dev/null +++ b/packages/compiler-vapor/src/transforms/vShow.ts @@ -0,0 +1,19 @@ +import { DOMErrorCodes, createDOMCompilerError } from '@vue/compiler-dom' +import type { DirectiveTransform } from '../transform' +import { IRNodeTypes } from '../ir' + +export const transformVShow: DirectiveTransform = (dir, node, context) => { + const { exp, loc } = dir + if (!exp) { + context.options.onError( + createDOMCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION, loc), + ) + } + + context.registerOperation({ + type: IRNodeTypes.WITH_DIRECTIVE, + element: context.reference(), + dir, + loc, + }) +} diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 421b2bdfb..26518598d 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -1,6 +1,6 @@ -import { EffectScope } from '@vue/reactivity' -import { Block } from './render' -import { DirectiveBinding } from './directives' +import { type Ref, EffectScope, ref } from '@vue/reactivity' +import type { Block } from './render' +import type { DirectiveBinding } from './directive' import type { Data } from '@vue/shared' export type SetupFn = (props: any, ctx: any) => Block | Data @@ -19,7 +19,8 @@ export interface ComponentInternalInstance { scope: EffectScope component: FunctionalComponent | ObjectComponent - isMounted: boolean + get isMounted(): boolean + isMountedRef: Ref /** directives */ dirs: Map @@ -44,6 +45,7 @@ let uid = 0 export const createComponentInstance = ( component: ObjectComponent | FunctionalComponent, ): ComponentInternalInstance => { + const isMountedRef = ref(false) const instance: ComponentInternalInstance = { uid: uid++, block: null, @@ -51,7 +53,10 @@ export const createComponentInstance = ( scope: new EffectScope(true /* detached */)!, component, - isMounted: false, + get isMounted() { + return isMountedRef.value + }, + isMountedRef, dirs: new Map(), // TODO: registory of provides, appContext, lifecycles, ... diff --git a/packages/runtime-vapor/src/directives.ts b/packages/runtime-vapor/src/directive.ts similarity index 65% rename from packages/runtime-vapor/src/directives.ts rename to packages/runtime-vapor/src/directive.ts index 70833e973..2eeb9277c 100644 --- a/packages/runtime-vapor/src/directives.ts +++ b/packages/runtime-vapor/src/directive.ts @@ -1,5 +1,6 @@ import { isFunction } from '@vue/shared' import { currentInstance, type ComponentInternalInstance } from './component' +import { effect } from './scheduler' export type DirectiveModifiers = Record @@ -9,6 +10,7 @@ export interface DirectiveBinding< M extends string = string, > { instance: ComponentInternalInstance | null + source?: () => V value: V oldValue: V | null arg?: A @@ -26,23 +28,24 @@ export type DirectiveHook< // create node -> `created` -> node operation -> `beforeMount` -> node mounted -> `mounted` // effect update -> `beforeUpdate` -> node updated -> `updated` // `beforeUnmount`-> node unmount -> `unmounted` -export interface ObjectDirective< +export type DirectiveHookName = + | 'created' + | 'beforeMount' + | 'mounted' + // | 'beforeUpdate' + | 'updated' + | 'beforeUnmount' + | 'unmounted' +export type ObjectDirective< T = any, V = any, A = string, M extends string = string, -> { - created?: DirectiveHook - beforeMount?: DirectiveHook - mounted?: DirectiveHook - // beforeUpdate?: DirectiveHook - // updated?: DirectiveHook - beforeUnmount?: DirectiveHook - unmounted?: DirectiveHook - // getSSRProps?: SSRDirectiveHook - // deep?: boolean +> = { + [K in DirectiveHookName]?: DirectiveHook | undefined +} & { + deep?: boolean } -export type DirectiveHookName = Exclude export type FunctionDirective< T = any, @@ -60,11 +63,11 @@ export type Directive< export type DirectiveArguments = Array< | [Directive | undefined] - | [Directive | undefined, value: any] - | [Directive | undefined, value: any, argument: string] + | [Directive | undefined, () => any] + | [Directive | undefined, () => any, argument: string] | [ Directive | undefined, - value: any, + value: () => any, argument: string, modifiers: DirectiveModifiers, ] @@ -83,7 +86,7 @@ export function withDirectives( const bindings = currentInstance.dirs.get(node)! for (const directive of directives) { - let [dir, value, arg, modifiers] = directive + let [dir, source, arg, modifiers] = directive if (!dir) continue if (isFunction(dir)) { // TODO function directive @@ -95,13 +98,19 @@ export function withDirectives( const binding: DirectiveBinding = { dir, instance: currentInstance, - value, - oldValue: void 0, + source, + value: null, // set later + oldValue: null, arg, modifiers, } - if (dir.created) dir.created(node, binding) bindings.push(binding) + + callDirectiveHook(node, binding, 'created') + effect(() => { + if (!currentInstance!.isMountedRef.value) return + callDirectiveHook(node, binding, 'updated') + }) } return node @@ -113,14 +122,28 @@ export function invokeDirectiveHook( nodes?: IterableIterator, ) { if (!instance) return - if (!nodes) { - nodes = instance.dirs.keys() - } + nodes = nodes || instance.dirs.keys() for (const node of nodes) { const directives = instance.dirs.get(node) || [] for (const binding of directives) { - const hook = binding.dir[name] - hook && hook(node, binding) + callDirectiveHook(node, binding, name) } } } + +function callDirectiveHook( + node: Node, + binding: DirectiveBinding, + name: DirectiveHookName, +) { + const { dir } = binding + const hook = dir[name] + if (!hook) return + + const newValue = binding.source ? binding.source() : undefined + if (name === 'updated' && binding.value === newValue) return + + binding.oldValue = binding.value + binding.value = newValue + hook(node, binding) +} diff --git a/packages/runtime-vapor/src/directives/vShow.ts b/packages/runtime-vapor/src/directives/vShow.ts new file mode 100644 index 000000000..f419053ec --- /dev/null +++ b/packages/runtime-vapor/src/directives/vShow.ts @@ -0,0 +1,23 @@ +import type { ObjectDirective } from '../directive' + +const vShowMap = new WeakMap() + +export const vShow: ObjectDirective = { + beforeMount(node, { source: value }) { + vShowMap.set(node, node.style.display === 'none' ? '' : node.style.display) + setDisplay(node, value) + }, + + updated(node, { value, oldValue }) { + if (!value === !oldValue) return + setDisplay(node, value) + }, + + beforeUnmount(node, { source: value }) { + setDisplay(node, value) + }, +} + +function setDisplay(el: HTMLElement, value: unknown): void { + el.style.display = value ? vShowMap.get(el)! : 'none' +} diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 63985f7ec..0c56476ad 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -41,5 +41,6 @@ export * from './on' export * from './render' export * from './template' export * from './scheduler' -export * from './directives' +export * from './directive' export * from './dom' +export * from './directives/vShow' diff --git a/packages/runtime-vapor/src/render.ts b/packages/runtime-vapor/src/render.ts index 74e88eace..b03e56ae8 100644 --- a/packages/runtime-vapor/src/render.ts +++ b/packages/runtime-vapor/src/render.ts @@ -7,7 +7,7 @@ import { setCurrentInstance, unsetCurrentInstance, } from './component' -import { invokeDirectiveHook } from './directives' +import { invokeDirectiveHook } from './directive' import { insert, remove } from './dom' export type Block = Node | Fragment | Block[] @@ -56,7 +56,7 @@ export function mountComponent( invokeDirectiveHook(instance, 'beforeMount') insert(block, instance.container) - instance.isMounted = true + instance.isMountedRef.value = true invokeDirectiveHook(instance, 'mounted') // TODO: lifecycle hooks (mounted, ...) @@ -70,7 +70,7 @@ export function unmountComponent(instance: ComponentInternalInstance) { invokeDirectiveHook(instance, 'beforeUnmount') scope.stop() block && remove(block, container) - instance.isMounted = false + instance.isMountedRef.value = false invokeDirectiveHook(instance, 'unmounted') unsetCurrentInstance() diff --git a/playground/src/show.vue b/playground/src/show.vue new file mode 100644 index 000000000..23b599121 --- /dev/null +++ b/playground/src/show.vue @@ -0,0 +1,13 @@ + + +