From a645e7aa51006516ba668b3a4365d296eb92ee7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Wed, 30 Aug 2023 15:25:51 +0800 Subject: [PATCH 1/9] feat(runtime-core): add `once` option to watch (#9034) --- .../runtime-core/__tests__/apiWatch.spec.ts | 38 +++++++++++++++++++ packages/runtime-core/src/apiWatch.ts | 31 +++++++++++---- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index f24ce80b9df..bddfc5ff541 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -1205,4 +1205,42 @@ describe('api: watch', () => { expect(countWE).toBe(3) expect(countW).toBe(2) }) + + const options = [ + { name: 'only trigger once watch' }, + { + deep: true, + name: 'only trigger once watch with deep' + }, + { + flush: 'sync', + name: 'only trigger once watch with flush: sync' + }, + { + flush: 'pre', + name: 'only trigger once watch with flush: pre' + }, + { + immediate: true, + name: 'only trigger once watch with immediate' + } + ] as const + test.each(options)('$name', async option => { + const count = ref(0) + const cb = vi.fn() + + watch(count, cb, { once: true, ...option }) + + count.value++ + await nextTick() + + expect(count.value).toBe(1) + expect(cb).toHaveBeenCalledTimes(1) + + count.value++ + await nextTick() + + expect(count.value).toBe(2) + expect(cb).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 1b85ba12d19..c307c4198a3 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -75,6 +75,7 @@ export interface WatchOptionsBase extends DebuggerOptions { export interface WatchOptions extends WatchOptionsBase { immediate?: Immediate deep?: boolean + once?: boolean } export type WatchStopHandle = () => void @@ -172,8 +173,16 @@ export function watch = false>( function doWatch( source: WatchSource | WatchSource[] | WatchEffect | object, cb: WatchCallback | null, - { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ + { immediate, deep, flush, once, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ ): WatchStopHandle { + if (cb && once) { + const _cb = cb + cb = (...args) => { + _cb(...args) + unwatch() + } + } + if (__DEV__ && !cb) { if (immediate !== undefined) { warn( @@ -187,6 +196,12 @@ function doWatch( `watch(source, callback, options?) signature.` ) } + if (once !== undefined) { + warn( + `watch() "once" option is only respected when using the ` + + `watch(source, callback, options?) signature.` + ) + } } const warnInvalidSource = (s: unknown) => { @@ -363,6 +378,13 @@ function doWatch( const effect = new ReactiveEffect(getter, scheduler) + const unwatch = () => { + effect.stop() + if (instance && instance.scope) { + remove(instance.scope.effects!, effect) + } + } + if (__DEV__) { effect.onTrack = onTrack effect.onTrigger = onTrigger @@ -384,13 +406,6 @@ function doWatch( effect.run() } - const unwatch = () => { - effect.stop() - if (instance && instance.scope) { - remove(instance.scope.effects!, effect) - } - } - if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch) return unwatch } From 9e77580c0c2f0d977bd0031a1d43cc334769d433 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: Mon, 4 Sep 2023 03:59:11 -0500 Subject: [PATCH 2/9] feat(compiler-sfc): expose resolve type-based props and emits (#8874) --- packages/compiler-sfc/src/index.ts | 3 +++ .../compiler-sfc/src/script/defineEmits.ts | 10 ++++--- .../compiler-sfc/src/script/defineProps.ts | 22 ++++++++++------ .../compiler-sfc/src/script/resolveType.ts | 26 +++++++++++++++++-- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/packages/compiler-sfc/src/index.ts b/packages/compiler-sfc/src/index.ts index 76b4900d46d..c6ee604146e 100644 --- a/packages/compiler-sfc/src/index.ts +++ b/packages/compiler-sfc/src/index.ts @@ -33,6 +33,8 @@ export { // Internals for type resolution export { invalidateTypeCache, registerTS } from './script/resolveType' +export { extractRuntimeProps } from './script/defineProps' +export { extractRuntimeEmits } from './script/defineEmits' // Types export type { @@ -58,6 +60,7 @@ export type { SFCScriptCompileOptions } from './compileScript' export type { ScriptCompileContext } from './script/context' export type { TypeResolveContext, + SimpleTypeResolveOptions, SimpleTypeResolveContext } from './script/resolveType' export type { diff --git a/packages/compiler-sfc/src/script/defineEmits.ts b/packages/compiler-sfc/src/script/defineEmits.ts index 8bd4cdfe543..b7453076cfe 100644 --- a/packages/compiler-sfc/src/script/defineEmits.ts +++ b/packages/compiler-sfc/src/script/defineEmits.ts @@ -8,7 +8,11 @@ import { } from '@babel/types' import { isCallOf } from './utils' import { ScriptCompileContext } from './context' -import { resolveTypeElements, resolveUnionType } from './resolveType' +import { + TypeResolveContext, + resolveTypeElements, + resolveUnionType +} from './resolveType' export const DEFINE_EMITS = 'defineEmits' @@ -64,7 +68,7 @@ export function genRuntimeEmits(ctx: ScriptCompileContext): string | undefined { return emitsDecl } -function extractRuntimeEmits(ctx: ScriptCompileContext): Set { +export function extractRuntimeEmits(ctx: TypeResolveContext): Set { const emits = new Set() const node = ctx.emitsTypeDecl! @@ -97,7 +101,7 @@ function extractRuntimeEmits(ctx: ScriptCompileContext): Set { } function extractEventNames( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, eventName: ArrayPattern | Identifier | ObjectPattern | RestElement, emits: Set ) { diff --git a/packages/compiler-sfc/src/script/defineProps.ts b/packages/compiler-sfc/src/script/defineProps.ts index 5004e314da1..449ed250d1d 100644 --- a/packages/compiler-sfc/src/script/defineProps.ts +++ b/packages/compiler-sfc/src/script/defineProps.ts @@ -8,7 +8,11 @@ import { } from '@babel/types' import { BindingTypes, isFunctionType } from '@vue/compiler-dom' import { ScriptCompileContext } from './context' -import { inferRuntimeType, resolveTypeElements } from './resolveType' +import { + TypeResolveContext, + inferRuntimeType, + resolveTypeElements +} from './resolveType' import { resolveObjectKey, UNKNOWN_TYPE, @@ -150,7 +154,7 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined { } } } else if (ctx.propsTypeDecl) { - propsDecls = genRuntimePropsFromTypes(ctx) + propsDecls = extractRuntimeProps(ctx) } const modelsDecls = genModelProps(ctx) @@ -162,7 +166,9 @@ export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined { } } -function genRuntimePropsFromTypes(ctx: ScriptCompileContext) { +export function extractRuntimeProps( + ctx: TypeResolveContext +): string | undefined { // this is only called if propsTypeDecl exists const props = resolveRuntimePropsFromType(ctx, ctx.propsTypeDecl!) if (!props.length) { @@ -175,7 +181,7 @@ function genRuntimePropsFromTypes(ctx: ScriptCompileContext) { for (const prop of props) { propStrings.push(genRuntimePropFromType(ctx, prop, hasStaticDefaults)) // register bindings - if (!(prop.key in ctx.bindingMetadata)) { + if ('bindingMetadata' in ctx && !(prop.key in ctx.bindingMetadata)) { ctx.bindingMetadata[prop.key] = BindingTypes.PROPS } } @@ -193,7 +199,7 @@ function genRuntimePropsFromTypes(ctx: ScriptCompileContext) { } function resolveRuntimePropsFromType( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, node: Node ): PropTypeData[] { const props: PropTypeData[] = [] @@ -222,7 +228,7 @@ function resolveRuntimePropsFromType( } function genRuntimePropFromType( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, { key, required, type, skipCheck }: PropTypeData, hasStaticDefaults: boolean ): string { @@ -284,7 +290,7 @@ function genRuntimePropFromType( * static properties, we can directly generate more optimized default * declarations. Otherwise we will have to fallback to runtime merging. */ -function hasStaticWithDefaults(ctx: ScriptCompileContext) { +function hasStaticWithDefaults(ctx: TypeResolveContext) { return !!( ctx.propsRuntimeDefaults && ctx.propsRuntimeDefaults.type === 'ObjectExpression' && @@ -297,7 +303,7 @@ function hasStaticWithDefaults(ctx: ScriptCompileContext) { } function genDestructuredDefaultValue( - ctx: ScriptCompileContext, + ctx: TypeResolveContext, key: string, inferredType?: string[] ): diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index 215081dc0b7..12666341e73 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -42,6 +42,13 @@ import type TS from 'typescript' import { extname, dirname } from 'path' import { minimatch as isMatch } from 'minimatch' +export type SimpleTypeResolveOptions = Partial< + Pick< + SFCScriptCompileOptions, + 'globalTypeFiles' | 'fs' | 'babelParserPlugins' | 'isProd' + > +> + /** * TypeResolveContext is compatible with ScriptCompileContext * but also allows a simpler version of it with minimal required properties @@ -59,13 +66,28 @@ import { minimatch as isMatch } from 'minimatch' */ export type SimpleTypeResolveContext = Pick< ScriptCompileContext, - // required - 'source' | 'filename' | 'error' | 'options' + // file + | 'source' + | 'filename' + + // utils + | 'error' + | 'helper' + | 'getString' + + // props + | 'propsTypeDecl' + | 'propsRuntimeDefaults' + | 'propsDestructuredBindings' + + // emits + | 'emitsTypeDecl' > & Partial< Pick > & { ast: Statement[] + options: SimpleTypeResolveOptions } export type TypeResolveContext = ScriptCompileContext | SimpleTypeResolveContext From f7e80ee4a065a9eaba98720abf415d9e87756cbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8A=B1=E6=9E=9C=E5=B1=B1=E5=A4=A7=E5=9C=A3?= <316783812@qq.com> Date: Tue, 5 Sep 2023 15:55:39 +0800 Subject: [PATCH 3/9] feat(compiler-core): export error message (#8729) --- packages/compiler-core/src/index.ts | 1 + packages/compiler-dom/src/index.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 4898a181dfc..588bb92cc5f 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -24,6 +24,7 @@ export { export { generate, type CodegenContext, type CodegenResult } from './codegen' export { ErrorCodes, + errorMessages, createCompilerError, type CoreCompilerError, type CompilerError diff --git a/packages/compiler-dom/src/index.ts b/packages/compiler-dom/src/index.ts index 2c6f71cefbb..a2f4aff2e4c 100644 --- a/packages/compiler-dom/src/index.ts +++ b/packages/compiler-dom/src/index.ts @@ -68,5 +68,9 @@ export function parse(template: string, options: ParserOptions = {}): RootNode { export * from './runtimeHelpers' export { transformStyle } from './transforms/transformStyle' -export { createDOMCompilerError, DOMErrorCodes } from './errors' +export { + createDOMCompilerError, + DOMErrorCodes, + DOMErrorMessages +} from './errors' export * from '@vue/compiler-core' From feb2f2edce2d91218a5e9a52c81e322e4033296b 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: Wed, 27 Sep 2023 16:40:16 +0800 Subject: [PATCH 4/9] feat: export runtime error strings (#9301) These strings are used for automatically generating error references in the documentation and should be considered internal. The code-to-string mapping is **not** part of the public API and can change between non-major versions. They are also exposed only in dev or the esm-bundler builds. --- packages/runtime-core/src/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 98aee757dab..85bd92e75b0 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -140,6 +140,15 @@ export { } from './components/BaseTransition' export { initCustomFormatter } from './customFormatter' +import { ErrorTypeStrings as _ErrorTypeStrings } from './errorHandling' +/** + * Runtime error messages. Only exposed in dev or esm builds. + * @internal + */ +export const ErrorTypeStrings = ( + __ESM_BUNDLER__ || __DEV__ ? _ErrorTypeStrings : null +) as typeof _ErrorTypeStrings + // For devtools export { devtools, setDevtoolsHook } from './devtools' From 16e06ca08f5a1e2af3fc7fb35de153dbe0c3087d Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 27 Oct 2023 22:25:09 +0800 Subject: [PATCH 5/9] feat(reactivity): more efficient reactivity system (#5912) fix #311, fix #1811, fix #6018, fix #7160, fix #8714, fix #9149, fix #9419, fix #9464 --- .../reactivity/__tests__/computed.spec.ts | 165 +++++++- .../__tests__/deferredComputed.spec.ts | 62 +-- packages/reactivity/__tests__/effect.spec.ts | 107 +++-- packages/reactivity/__tests__/gc.spec.ts | 81 ++++ .../__tests__/reactiveArray.spec.ts | 33 ++ packages/reactivity/src/baseHandlers.ts | 13 +- packages/reactivity/src/collectionHandlers.ts | 11 +- packages/reactivity/src/computed.ts | 28 +- packages/reactivity/src/constants.ts | 30 ++ packages/reactivity/src/deferredComputed.ts | 92 +--- packages/reactivity/src/dep.ts | 64 +-- packages/reactivity/src/effect.ts | 394 ++++++------------ packages/reactivity/src/index.ts | 12 +- packages/reactivity/src/operations.ts | 15 - packages/reactivity/src/reactive.ts | 9 +- packages/reactivity/src/reactiveEffect.ts | 150 +++++++ packages/reactivity/src/ref.ts | 63 +-- .../runtime-core/src/apiAsyncComponent.ts | 1 + packages/runtime-core/src/apiWatch.ts | 4 +- .../src/componentPublicInstance.ts | 7 +- .../src/components/BaseTransition.ts | 1 + packages/runtime-core/src/hmr.ts | 2 + packages/runtime-core/src/renderer.ts | 8 +- 23 files changed, 810 insertions(+), 542 deletions(-) create mode 100644 packages/reactivity/__tests__/gc.spec.ts create mode 100644 packages/reactivity/src/constants.ts delete mode 100644 packages/reactivity/src/operations.ts create mode 100644 packages/reactivity/src/reactiveEffect.ts diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts index c044b5feb35..d9b8f888caf 100644 --- a/packages/reactivity/__tests__/computed.spec.ts +++ b/packages/reactivity/__tests__/computed.spec.ts @@ -184,7 +184,7 @@ describe('reactivity/computed', () => { // mutate n n.value++ // on the 2nd run, plusOne.value should have already updated. - expect(plusOneValues).toMatchObject([1, 2, 2]) + expect(plusOneValues).toMatchObject([1, 2]) }) it('should warn if trying to set a readonly computed', () => { @@ -288,4 +288,167 @@ describe('reactivity/computed', () => { oldValue: 2 }) }) + + // https://github.com/vuejs/core/pull/5912#issuecomment-1497596875 + it('should query deps dirty sequentially', () => { + const cSpy = vi.fn() + + const a = ref({ + v: 1 + }) + const b = computed(() => { + return a.value + }) + const c = computed(() => { + cSpy() + return b.value?.v + }) + const d = computed(() => { + if (b.value) { + return c.value + } + return 0 + }) + + d.value + a.value!.v = 2 + a.value = null + d.value + expect(cSpy).toHaveBeenCalledTimes(1) + }) + + // https://github.com/vuejs/core/pull/5912#issuecomment-1738257692 + it('chained computed dirty reallocation after querying dirty', () => { + let _msg: string | undefined + + const items = ref() + const isLoaded = computed(() => { + return !!items.value + }) + const msg = computed(() => { + if (isLoaded.value) { + return 'The items are loaded' + } else { + return 'The items are not loaded' + } + }) + + effect(() => { + _msg = msg.value + }) + + items.value = [1, 2, 3] + items.value = [1, 2, 3] + items.value = undefined + + expect(_msg).toBe('The items are not loaded') + }) + + it('chained computed dirty reallocation after trigger computed getter', () => { + let _msg: string | undefined + + const items = ref() + const isLoaded = computed(() => { + return !!items.value + }) + const msg = computed(() => { + if (isLoaded.value) { + return 'The items are loaded' + } else { + return 'The items are not loaded' + } + }) + + _msg = msg.value + items.value = [1, 2, 3] + isLoaded.value // <- trigger computed getter + _msg = msg.value + items.value = undefined + _msg = msg.value + + expect(_msg).toBe('The items are not loaded') + }) + + // https://github.com/vuejs/core/pull/5912#issuecomment-1739159832 + it('deps order should be consistent with the last time get value', () => { + const cSpy = vi.fn() + + const a = ref(0) + const b = computed(() => { + return a.value % 3 !== 0 + }) + const c = computed(() => { + cSpy() + if (a.value % 3 === 2) { + return 'expensive' + } + return 'cheap' + }) + const d = computed(() => { + return a.value % 3 === 2 + }) + const e = computed(() => { + if (b.value) { + if (d.value) { + return 'Avoiding expensive calculation' + } + } + return c.value + }) + + e.value + a.value++ + e.value + + expect(e.effect.deps.length).toBe(3) + expect(e.effect.deps.indexOf((b as any).dep)).toBe(0) + expect(e.effect.deps.indexOf((d as any).dep)).toBe(1) + expect(e.effect.deps.indexOf((c as any).dep)).toBe(2) + expect(cSpy).toHaveBeenCalledTimes(2) + + a.value++ + e.value + + expect(cSpy).toHaveBeenCalledTimes(2) + }) + + it('should trigger by the second computed that maybe dirty', () => { + const cSpy = vi.fn() + + const src1 = ref(0) + const src2 = ref(0) + const c1 = computed(() => src1.value) + const c2 = computed(() => (src1.value % 2) + src2.value) + const c3 = computed(() => { + cSpy() + c1.value + c2.value + }) + + c3.value + src1.value = 2 + c3.value + expect(cSpy).toHaveBeenCalledTimes(2) + src2.value = 1 + c3.value + expect(cSpy).toHaveBeenCalledTimes(3) + }) + + it('should trigger the second effect', () => { + const fnSpy = vi.fn() + const v = ref(1) + const c = computed(() => v.value) + + effect(() => { + c.value + }) + effect(() => { + c.value + fnSpy() + }) + + expect(fnSpy).toBeCalledTimes(1) + v.value = 2 + expect(fnSpy).toBeCalledTimes(2) + }) }) diff --git a/packages/reactivity/__tests__/deferredComputed.spec.ts b/packages/reactivity/__tests__/deferredComputed.spec.ts index 100f14ae358..8e78ba959c3 100644 --- a/packages/reactivity/__tests__/deferredComputed.spec.ts +++ b/packages/reactivity/__tests__/deferredComputed.spec.ts @@ -1,57 +1,32 @@ -import { computed, deferredComputed, effect, ref } from '../src' +import { computed, effect, ref } from '../src' describe('deferred computed', () => { - const tick = Promise.resolve() - - test('should only trigger once on multiple mutations', async () => { + test('should not trigger if value did not change', () => { const src = ref(0) - const c = deferredComputed(() => src.value) + const c = computed(() => src.value % 2) const spy = vi.fn() effect(() => { spy(c.value) }) expect(spy).toHaveBeenCalledTimes(1) - src.value = 1 src.value = 2 - src.value = 3 - // not called yet - expect(spy).toHaveBeenCalledTimes(1) - await tick - // should only trigger once - expect(spy).toHaveBeenCalledTimes(2) - expect(spy).toHaveBeenCalledWith(c.value) - }) - test('should not trigger if value did not change', async () => { - const src = ref(0) - const c = deferredComputed(() => src.value % 2) - const spy = vi.fn() - effect(() => { - spy(c.value) - }) - expect(spy).toHaveBeenCalledTimes(1) - src.value = 1 - src.value = 2 - - await tick // should not trigger expect(spy).toHaveBeenCalledTimes(1) src.value = 3 - src.value = 4 src.value = 5 - await tick // should trigger because latest value changes expect(spy).toHaveBeenCalledTimes(2) }) - test('chained computed trigger', async () => { + test('chained computed trigger', () => { const effectSpy = vi.fn() const c1Spy = vi.fn() const c2Spy = vi.fn() const src = ref(0) - const c1 = deferredComputed(() => { + const c1 = computed(() => { c1Spy() return src.value % 2 }) @@ -69,19 +44,18 @@ describe('deferred computed', () => { expect(effectSpy).toHaveBeenCalledTimes(1) src.value = 1 - await tick expect(c1Spy).toHaveBeenCalledTimes(2) expect(c2Spy).toHaveBeenCalledTimes(2) expect(effectSpy).toHaveBeenCalledTimes(2) }) - test('chained computed avoid re-compute', async () => { + test('chained computed avoid re-compute', () => { const effectSpy = vi.fn() const c1Spy = vi.fn() const c2Spy = vi.fn() const src = ref(0) - const c1 = deferredComputed(() => { + const c1 = computed(() => { c1Spy() return src.value % 2 }) @@ -98,26 +72,24 @@ describe('deferred computed', () => { src.value = 2 src.value = 4 src.value = 6 - await tick - // c1 should re-compute once. - expect(c1Spy).toHaveBeenCalledTimes(2) + expect(c1Spy).toHaveBeenCalledTimes(4) // c2 should not have to re-compute because c1 did not change. expect(c2Spy).toHaveBeenCalledTimes(1) // effect should not trigger because c2 did not change. expect(effectSpy).toHaveBeenCalledTimes(1) }) - test('chained computed value invalidation', async () => { + test('chained computed value invalidation', () => { const effectSpy = vi.fn() const c1Spy = vi.fn() const c2Spy = vi.fn() const src = ref(0) - const c1 = deferredComputed(() => { + const c1 = computed(() => { c1Spy() return src.value % 2 }) - const c2 = deferredComputed(() => { + const c2 = computed(() => { c2Spy() return c1.value + 1 }) @@ -139,17 +111,17 @@ describe('deferred computed', () => { expect(c2Spy).toHaveBeenCalledTimes(2) }) - test('sync access of invalidated chained computed should not prevent final effect from running', async () => { + test('sync access of invalidated chained computed should not prevent final effect from running', () => { const effectSpy = vi.fn() const c1Spy = vi.fn() const c2Spy = vi.fn() const src = ref(0) - const c1 = deferredComputed(() => { + const c1 = computed(() => { c1Spy() return src.value % 2 }) - const c2 = deferredComputed(() => { + const c2 = computed(() => { c2Spy() return c1.value + 1 }) @@ -162,14 +134,13 @@ describe('deferred computed', () => { src.value = 1 // sync access c2 c2.value - await tick expect(effectSpy).toHaveBeenCalledTimes(2) }) - test('should not compute if deactivated before scheduler is called', async () => { + test('should not compute if deactivated before scheduler is called', () => { const c1Spy = vi.fn() const src = ref(0) - const c1 = deferredComputed(() => { + const c1 = computed(() => { c1Spy() return src.value % 2 }) @@ -179,7 +150,6 @@ describe('deferred computed', () => { c1.effect.stop() // trigger src.value++ - await tick expect(c1Spy).toHaveBeenCalledTimes(1) }) }) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index e34c7b31e40..2ebb2edea8a 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -1,5 +1,4 @@ import { - ref, reactive, effect, stop, @@ -12,7 +11,8 @@ import { readonly, ReactiveEffectRunner } from '../src/index' -import { ITERATE_KEY } from '../src/effect' +import { pauseScheduling, resetScheduling } from '../src/effect' +import { ITERATE_KEY, getDepFromReactive } from '../src/reactiveEffect' describe('reactivity/effect', () => { it('should run the passed function once (wrapped by a effect)', () => { @@ -574,8 +574,8 @@ describe('reactivity/effect', () => { expect(output.fx2).toBe(1 + 3 + 3) expect(fx1Spy).toHaveBeenCalledTimes(1) - // Invoked twice due to change of fx1. - expect(fx2Spy).toHaveBeenCalledTimes(2) + // Invoked due to change of fx1. + expect(fx2Spy).toHaveBeenCalledTimes(1) fx1Spy.mockClear() fx2Spy.mockClear() @@ -821,26 +821,6 @@ describe('reactivity/effect', () => { expect(dummy).toBe(3) }) - // #5707 - // when an effect completes its run, it should clear the tracking bits of - // its tracked deps. However, if the effect stops itself, the deps list is - // emptied so their bits are never cleared. - it('edge case: self-stopping effect tracking ref', () => { - const c = ref(true) - const runner = effect(() => { - // reference ref - if (!c.value) { - // stop itself while running - stop(runner) - } - }) - // trigger run - c.value = !c.value - // should clear bits - expect((c as any).dep.w).toBe(0) - expect((c as any).dep.n).toBe(0) - }) - it('events: onStop', () => { const onStop = vi.fn() const runner = effect(() => {}, { @@ -1015,4 +995,83 @@ describe('reactivity/effect', () => { expect(has).toBe(false) }) }) + + it('should be triggered once with pauseScheduling', () => { + const counter = reactive({ num: 0 }) + + const counterSpy = vi.fn(() => counter.num) + effect(counterSpy) + + counterSpy.mockClear() + + pauseScheduling() + counter.num++ + counter.num++ + resetScheduling() + expect(counterSpy).toHaveBeenCalledTimes(1) + }) + + describe('empty dep cleanup', () => { + it('should remove the dep when the effect is stopped', () => { + const obj = reactive({ prop: 1 }) + expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined() + const runner = effect(() => obj.prop) + const dep = getDepFromReactive(toRaw(obj), 'prop') + expect(dep).toHaveLength(1) + obj.prop = 2 + expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep) + expect(dep).toHaveLength(1) + stop(runner) + expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined() + obj.prop = 3 + runner() + expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined() + }) + + it('should only remove the dep when the last effect is stopped', () => { + const obj = reactive({ prop: 1 }) + expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined() + const runner1 = effect(() => obj.prop) + const dep = getDepFromReactive(toRaw(obj), 'prop') + expect(dep).toHaveLength(1) + const runner2 = effect(() => obj.prop) + expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep) + expect(dep).toHaveLength(2) + obj.prop = 2 + expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep) + expect(dep).toHaveLength(2) + stop(runner1) + expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep) + expect(dep).toHaveLength(1) + obj.prop = 3 + expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep) + expect(dep).toHaveLength(1) + stop(runner2) + expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined() + obj.prop = 4 + runner1() + runner2() + expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined() + }) + + it('should remove the dep when it is no longer used by the effect', () => { + const obj = reactive<{ a: number; b: number; c: 'a' | 'b' }>({ + a: 1, + b: 2, + c: 'a' + }) + expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined() + effect(() => obj[obj.c]) + const depC = getDepFromReactive(toRaw(obj), 'c') + expect(getDepFromReactive(toRaw(obj), 'a')).toHaveLength(1) + expect(getDepFromReactive(toRaw(obj), 'b')).toBeUndefined() + expect(depC).toHaveLength(1) + obj.c = 'b' + obj.a = 4 + expect(getDepFromReactive(toRaw(obj), 'a')).toBeUndefined() + expect(getDepFromReactive(toRaw(obj), 'b')).toHaveLength(1) + expect(getDepFromReactive(toRaw(obj), 'c')).toBe(depC) + expect(depC).toHaveLength(1) + }) + }) }) diff --git a/packages/reactivity/__tests__/gc.spec.ts b/packages/reactivity/__tests__/gc.spec.ts new file mode 100644 index 00000000000..7676a0e12d0 --- /dev/null +++ b/packages/reactivity/__tests__/gc.spec.ts @@ -0,0 +1,81 @@ +import { + ComputedRef, + computed, + effect, + reactive, + shallowRef as ref, + toRaw +} from '../src/index' +import { getDepFromReactive } from '../src/reactiveEffect' + +describe.skipIf(!global.gc)('reactivity/gc', () => { + const gc = () => { + return new Promise(resolve => { + setTimeout(() => { + global.gc!() + resolve() + }) + }) + } + + // #9233 + it('should release computed cache', async () => { + const src = ref<{} | undefined>({}) + const srcRef = new WeakRef(src.value!) + + let c: ComputedRef | undefined = computed(() => src.value) + + c.value // cache src value + src.value = undefined // release value + c = undefined // release computed + + await gc() + expect(srcRef.deref()).toBeUndefined() + }) + + it('should release reactive property dep', async () => { + const src = reactive({ foo: 1 }) + + let c: ComputedRef | undefined = computed(() => src.foo) + + c.value + expect(getDepFromReactive(toRaw(src), 'foo')).not.toBeUndefined() + + c = undefined + await gc() + await gc() + expect(getDepFromReactive(toRaw(src), 'foo')).toBeUndefined() + }) + + it('should not release effect for ref', async () => { + const spy = vi.fn() + const src = ref(0) + + effect(() => { + spy() + src.value + }) + + expect(spy).toHaveBeenCalledTimes(1) + + await gc() + src.value++ + expect(spy).toHaveBeenCalledTimes(2) + }) + + it('should not release effect for reactive', async () => { + const spy = vi.fn() + const src = reactive({ foo: 1 }) + + effect(() => { + spy() + src.foo + }) + + expect(spy).toHaveBeenCalledTimes(1) + + await gc() + src.foo++ + expect(spy).toHaveBeenCalledTimes(2) + }) +}) diff --git a/packages/reactivity/__tests__/reactiveArray.spec.ts b/packages/reactivity/__tests__/reactiveArray.spec.ts index 808c5aa5529..f4eb7b58384 100644 --- a/packages/reactivity/__tests__/reactiveArray.spec.ts +++ b/packages/reactivity/__tests__/reactiveArray.spec.ts @@ -99,6 +99,39 @@ describe('reactivity/reactive/Array', () => { expect(fn).toHaveBeenCalledTimes(1) }) + test('shift on Array should trigger dependency once', () => { + const arr = reactive([1, 2, 3]) + const fn = vi.fn() + effect(() => { + for (let i = 0; i < arr.length; i++) { + arr[i] + } + fn() + }) + expect(fn).toHaveBeenCalledTimes(1) + arr.shift() + expect(fn).toHaveBeenCalledTimes(2) + }) + + //#6018 + test('edge case: avoid trigger effect in deleteProperty when array length-decrease mutation methods called', () => { + const arr = ref([1]) + const fn1 = vi.fn() + const fn2 = vi.fn() + effect(() => { + fn1() + if (arr.value.length > 0) { + arr.value.slice() + fn2() + } + }) + expect(fn1).toHaveBeenCalledTimes(1) + expect(fn2).toHaveBeenCalledTimes(1) + arr.value.splice(0) + expect(fn1).toHaveBeenCalledTimes(2) + expect(fn2).toHaveBeenCalledTimes(1) + }) + test('add existing index on Array should not trigger length dependency', () => { const array = new Array(3) const observed = reactive(array) diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts index 259b44a1edc..36e4d311b4b 100644 --- a/packages/reactivity/src/baseHandlers.ts +++ b/packages/reactivity/src/baseHandlers.ts @@ -2,7 +2,6 @@ import { reactive, readonly, toRaw, - ReactiveFlags, Target, readonlyMap, reactiveMap, @@ -11,14 +10,14 @@ import { isReadonly, isShallow } from './reactive' -import { TrackOpTypes, TriggerOpTypes } from './operations' +import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants' import { - track, - trigger, - ITERATE_KEY, pauseTracking, - resetTracking + resetTracking, + pauseScheduling, + resetScheduling } from './effect' +import { track, trigger, ITERATE_KEY } from './reactiveEffect' import { isObject, hasOwn, @@ -71,7 +70,9 @@ function createArrayInstrumentations() { ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => { instrumentations[key] = function (this: unknown[], ...args: unknown[]) { pauseTracking() + pauseScheduling() const res = (toRaw(this) as any)[key].apply(this, args) + resetScheduling() resetTracking() return res } diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts index 1d07af3be8c..e8d99840f71 100644 --- a/packages/reactivity/src/collectionHandlers.ts +++ b/packages/reactivity/src/collectionHandlers.ts @@ -1,6 +1,11 @@ -import { toRaw, ReactiveFlags, toReactive, toReadonly } from './reactive' -import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect' -import { TrackOpTypes, TriggerOpTypes } from './operations' +import { toRaw, toReactive, toReadonly } from './reactive' +import { + track, + trigger, + ITERATE_KEY, + MAP_KEY_ITERATE_KEY +} from './reactiveEffect' +import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants' import { capitalize, hasOwn, hasChanged, toRawType, isMap } from '@vue/shared' export type CollectionTypes = IterableCollections | WeakCollections diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index b24484c9e62..09247360d06 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -1,8 +1,9 @@ import { DebuggerOptions, ReactiveEffect } from './effect' import { Ref, trackRefValue, triggerRefValue } from './ref' -import { isFunction, NOOP } from '@vue/shared' -import { ReactiveFlags, toRaw } from './reactive' +import { hasChanged, isFunction, NOOP } from '@vue/shared' +import { toRaw } from './reactive' import { Dep } from './dep' +import { DirtyLevels, ReactiveFlags } from './constants' declare const ComputedRefSymbol: unique symbol @@ -32,7 +33,6 @@ export class ComputedRefImpl { public readonly __v_isRef = true public readonly [ReactiveFlags.IS_READONLY]: boolean = false - public _dirty = true public _cacheable: boolean constructor( @@ -42,10 +42,7 @@ export class ComputedRefImpl { isSSR: boolean ) { this.effect = new ReactiveEffect(getter, () => { - if (!this._dirty) { - this._dirty = true - triggerRefValue(this) - } + triggerRefValue(this, DirtyLevels.ComputedValueMaybeDirty) }) this.effect.computed = this this.effect.active = this._cacheable = !isSSR @@ -56,9 +53,10 @@ export class ComputedRefImpl { // the computed ref may get wrapped by other proxies e.g. readonly() #3376 const self = toRaw(this) trackRefValue(self) - if (self._dirty || !self._cacheable) { - self._dirty = false - self._value = self.effect.run()! + if (!self._cacheable || self.effect.dirty) { + if (hasChanged(self._value, (self._value = self.effect.run()!))) { + triggerRefValue(self, DirtyLevels.ComputedValueDirty) + } } return self._value } @@ -66,6 +64,16 @@ export class ComputedRefImpl { set value(newValue: T) { this._setter(newValue) } + + // #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x + get _dirty() { + return this.effect.dirty + } + + set _dirty(v) { + this.effect.dirty = v + } + // #endregion } /** diff --git a/packages/reactivity/src/constants.ts b/packages/reactivity/src/constants.ts new file mode 100644 index 00000000000..4ad2ec3c7da --- /dev/null +++ b/packages/reactivity/src/constants.ts @@ -0,0 +1,30 @@ +// using literal strings instead of numbers so that it's easier to inspect +// debugger events + +export const enum TrackOpTypes { + GET = 'get', + HAS = 'has', + ITERATE = 'iterate' +} + +export const enum TriggerOpTypes { + SET = 'set', + ADD = 'add', + DELETE = 'delete', + CLEAR = 'clear' +} + +export const enum ReactiveFlags { + SKIP = '__v_skip', + IS_REACTIVE = '__v_isReactive', + IS_READONLY = '__v_isReadonly', + IS_SHALLOW = '__v_isShallow', + RAW = '__v_raw' +} + +export const enum DirtyLevels { + NotDirty = 0, + ComputedValueMaybeDirty = 1, + ComputedValueDirty = 2, + Dirty = 3 +} diff --git a/packages/reactivity/src/deferredComputed.ts b/packages/reactivity/src/deferredComputed.ts index a23122046a4..1dbba1f3f03 100644 --- a/packages/reactivity/src/deferredComputed.ts +++ b/packages/reactivity/src/deferredComputed.ts @@ -1,88 +1,6 @@ -import { Dep } from './dep' -import { ReactiveEffect } from './effect' -import { ComputedGetter, ComputedRef } from './computed' -import { ReactiveFlags, toRaw } from './reactive' -import { trackRefValue, triggerRefValue } from './ref' +import { computed } from './computed' -const tick = /*#__PURE__*/ Promise.resolve() -const queue: any[] = [] -let queued = false - -const scheduler = (fn: any) => { - queue.push(fn) - if (!queued) { - queued = true - tick.then(flush) - } -} - -const flush = () => { - for (let i = 0; i < queue.length; i++) { - queue[i]() - } - queue.length = 0 - queued = false -} - -class DeferredComputedRefImpl { - public dep?: Dep = undefined - - private _value!: T - private _dirty = true - public readonly effect: ReactiveEffect - - public readonly __v_isRef = true - public readonly [ReactiveFlags.IS_READONLY] = true - - constructor(getter: ComputedGetter) { - let compareTarget: any - let hasCompareTarget = false - let scheduled = false - this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => { - if (this.dep) { - if (computedTrigger) { - compareTarget = this._value - hasCompareTarget = true - } else if (!scheduled) { - const valueToCompare = hasCompareTarget ? compareTarget : this._value - scheduled = true - hasCompareTarget = false - scheduler(() => { - if (this.effect.active && this._get() !== valueToCompare) { - triggerRefValue(this) - } - scheduled = false - }) - } - // chained upstream computeds are notified synchronously to ensure - // value invalidation in case of sync access; normal effects are - // deferred to be triggered in scheduler. - for (const e of this.dep) { - if (e.computed instanceof DeferredComputedRefImpl) { - e.scheduler!(true /* computedTrigger */) - } - } - } - this._dirty = true - }) - this.effect.computed = this as any - } - - private _get() { - if (this._dirty) { - this._dirty = false - return (this._value = this.effect.run()!) - } - return this._value - } - - get value() { - trackRefValue(this) - // the computed ref may get wrapped by other proxies e.g. readonly() #3376 - return toRaw(this)._get() - } -} - -export function deferredComputed(getter: () => T): ComputedRef { - return new DeferredComputedRefImpl(getter) as any -} +/** + * @deprecated use `computed` instead. See #5912 + */ +export const deferredComputed = computed diff --git a/packages/reactivity/src/dep.ts b/packages/reactivity/src/dep.ts index 8677f575756..eafb2a8af3f 100644 --- a/packages/reactivity/src/dep.ts +++ b/packages/reactivity/src/dep.ts @@ -1,57 +1,17 @@ -import { ReactiveEffect, trackOpBit } from './effect' +import type { ReactiveEffect } from './effect' +import type { ComputedRefImpl } from './computed' -export type Dep = Set & TrackedMarkers - -/** - * wasTracked and newTracked maintain the status for several levels of effect - * tracking recursion. One bit per level is used to define whether the dependency - * was/is tracked. - */ -type TrackedMarkers = { - /** - * wasTracked - */ - w: number - /** - * newTracked - */ - n: number +export type Dep = Map & { + cleanup: () => void + computed?: ComputedRefImpl } -export const createDep = (effects?: ReactiveEffect[]): Dep => { - const dep = new Set(effects) as Dep - dep.w = 0 - dep.n = 0 +export const createDep = ( + cleanup: () => void, + computed?: ComputedRefImpl +): Dep => { + const dep = new Map() as Dep + dep.cleanup = cleanup + dep.computed = computed return dep } - -export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0 - -export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0 - -export const initDepMarkers = ({ deps }: ReactiveEffect) => { - if (deps.length) { - for (let i = 0; i < deps.length; i++) { - deps[i].w |= trackOpBit // set was tracked - } - } -} - -export const finalizeDepMarkers = (effect: ReactiveEffect) => { - const { deps } = effect - if (deps.length) { - let ptr = 0 - for (let i = 0; i < deps.length; i++) { - const dep = deps[i] - if (wasTracked(dep) && !newTracked(dep)) { - dep.delete(effect) - } else { - deps[ptr++] = dep - } - // clear bits - dep.w &= ~trackOpBit - dep.n &= ~trackOpBit - } - deps.length = ptr - } -} diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index c982dbd0b5a..3a25295011c 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -1,34 +1,8 @@ -import { TrackOpTypes, TriggerOpTypes } from './operations' -import { extend, isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared' +import { NOOP, extend } from '@vue/shared' +import type { ComputedRefImpl } from './computed' +import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants' +import type { Dep } from './dep' import { EffectScope, recordEffectScope } from './effectScope' -import { - createDep, - Dep, - finalizeDepMarkers, - initDepMarkers, - newTracked, - wasTracked -} from './dep' -import { ComputedRefImpl } from './computed' - -// The main WeakMap that stores {target -> key -> dep} connections. -// Conceptually, it's easier to think of a dependency as a Dep class -// which maintains a Set of subscribers, but we simply store them as -// raw Sets to reduce memory overhead. -type KeyToDepMap = Map -const targetMap = new WeakMap() - -// The number of effects currently being tracked recursively. -let effectTrackDepth = 0 - -export let trackOpBit = 1 - -/** - * The bitwise track markers support at most 30 levels of recursion. - * This value is chosen to enable modern JS engines to use a SMI on all platforms. - * When recursion depth is greater, fall back to using a full cleanup. - */ -const maxMarkerBits = 30 export type EffectScheduler = (...args: any[]) => any @@ -47,13 +21,9 @@ export type DebuggerEventExtraInfo = { export let activeEffect: ReactiveEffect | undefined -export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '') -export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '') - export class ReactiveEffect { active = true deps: Dep[] = [] - parent: ReactiveEffect | undefined = undefined /** * Can be attached after creation @@ -64,10 +34,6 @@ export class ReactiveEffect { * @internal */ allowRecurse?: boolean - /** - * @internal - */ - private deferStop?: boolean onStop?: () => void // dev only @@ -75,77 +41,115 @@ export class ReactiveEffect { // dev only onTrigger?: (event: DebuggerEvent) => void + /** + * @internal + */ + _dirtyLevel = DirtyLevels.Dirty + /** + * @internal + */ + _trackId = 0 + /** + * @internal + */ + _runnings = 0 + /** + * @internal + */ + _queryings = 0 + /** + * @internal + */ + _depsLength = 0 + constructor( public fn: () => T, - public scheduler: EffectScheduler | null = null, + public trigger: () => void, + public scheduler?: EffectScheduler, scope?: EffectScope ) { recordEffectScope(this, scope) } + public get dirty() { + if (this._dirtyLevel === DirtyLevels.ComputedValueMaybeDirty) { + this._dirtyLevel = DirtyLevels.NotDirty + this._queryings++ + pauseTracking() + for (const dep of this.deps) { + if (dep.computed) { + triggerComputed(dep.computed) + if (this._dirtyLevel >= DirtyLevels.ComputedValueDirty) { + break + } + } + } + resetTracking() + this._queryings-- + } + return this._dirtyLevel >= DirtyLevels.ComputedValueDirty + } + + public set dirty(v) { + this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty + } + run() { + this._dirtyLevel = DirtyLevels.NotDirty if (!this.active) { return this.fn() } - let parent: ReactiveEffect | undefined = activeEffect let lastShouldTrack = shouldTrack - while (parent) { - if (parent === this) { - return - } - parent = parent.parent - } + let lastEffect = activeEffect try { - this.parent = activeEffect - activeEffect = this shouldTrack = true - - trackOpBit = 1 << ++effectTrackDepth - - if (effectTrackDepth <= maxMarkerBits) { - initDepMarkers(this) - } else { - cleanupEffect(this) - } + activeEffect = this + this._runnings++ + preCleanupEffect(this) return this.fn() } finally { - if (effectTrackDepth <= maxMarkerBits) { - finalizeDepMarkers(this) - } - - trackOpBit = 1 << --effectTrackDepth - - activeEffect = this.parent + postCleanupEffect(this) + this._runnings-- + activeEffect = lastEffect shouldTrack = lastShouldTrack - this.parent = undefined - - if (this.deferStop) { - this.stop() - } } } stop() { - // stopped while running itself - defer the cleanup - if (activeEffect === this) { - this.deferStop = true - } else if (this.active) { - cleanupEffect(this) - if (this.onStop) { - this.onStop() - } + if (this.active) { + preCleanupEffect(this) + postCleanupEffect(this) + this.onStop?.() this.active = false } } } -function cleanupEffect(effect: ReactiveEffect) { - const { deps } = effect - if (deps.length) { - for (let i = 0; i < deps.length; i++) { - deps[i].delete(effect) +function triggerComputed(computed: ComputedRefImpl) { + return computed.value +} + +function preCleanupEffect(effect: ReactiveEffect) { + effect._trackId++ + effect._depsLength = 0 +} + +function postCleanupEffect(effect: ReactiveEffect) { + if (effect.deps && effect.deps.length > effect._depsLength) { + for (let i = effect._depsLength; i < effect.deps.length; i++) { + cleanupDepEffect(effect.deps[i], effect) + } + effect.deps.length = effect._depsLength + } +} + +function cleanupDepEffect(dep: Dep, effect: ReactiveEffect) { + const trackId = dep.get(effect) + if (trackId !== undefined && effect._trackId !== trackId) { + dep.delete(effect) + if (dep.size === 0) { + dep.cleanup() } - deps.length = 0 } } @@ -185,7 +189,11 @@ export function effect( fn = (fn as ReactiveEffectRunner).effect.fn } - const _effect = new ReactiveEffect(fn) + const _effect = new ReactiveEffect(fn, NOOP, () => { + if (_effect.dirty) { + _effect.run() + } + }) if (options) { extend(_effect, options) if (options.scope) recordEffectScope(_effect, options.scope) @@ -208,6 +216,8 @@ export function stop(runner: ReactiveEffectRunner) { } export let shouldTrack = true +export let pauseScheduleStack = 0 + const trackStack: boolean[] = [] /** @@ -234,196 +244,70 @@ export function resetTracking() { shouldTrack = last === undefined ? true : last } -/** - * Tracks access to a reactive property. - * - * This will check which effect is running at the moment and record it as dep - * which records all effects that depend on the reactive property. - * - * @param target - Object holding the reactive property. - * @param type - Defines the type of access to the reactive property. - * @param key - Identifier of the reactive property to track. - */ -export function track(target: object, type: TrackOpTypes, key: unknown) { - if (shouldTrack && activeEffect) { - let depsMap = targetMap.get(target) - if (!depsMap) { - targetMap.set(target, (depsMap = new Map())) - } - let dep = depsMap.get(key) - if (!dep) { - depsMap.set(key, (dep = createDep())) - } - - const eventInfo = __DEV__ - ? { effect: activeEffect, target, type, key } - : undefined +export function pauseScheduling() { + pauseScheduleStack++ +} - trackEffects(dep, eventInfo) +export function resetScheduling() { + pauseScheduleStack-- + while (!pauseScheduleStack && queueEffectSchedulers.length) { + queueEffectSchedulers.shift()!() } } -export function trackEffects( +export function trackEffect( + effect: ReactiveEffect, dep: Dep, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { - let shouldTrack = false - if (effectTrackDepth <= maxMarkerBits) { - if (!newTracked(dep)) { - dep.n |= trackOpBit // set newly tracked - shouldTrack = !wasTracked(dep) - } - } else { - // Full cleanup mode. - shouldTrack = !dep.has(activeEffect!) - } - - if (shouldTrack) { - dep.add(activeEffect!) - activeEffect!.deps.push(dep) - if (__DEV__ && activeEffect!.onTrack) { - activeEffect!.onTrack( - extend( - { - effect: activeEffect! - }, - debuggerEventExtraInfo! - ) - ) - } - } -} - -/** - * Finds all deps associated with the target (or a specific property) and - * triggers the effects stored within. - * - * @param target - The reactive object. - * @param type - Defines the type of the operation that needs to trigger effects. - * @param key - Can be used to target a specific reactive property in the target object. - */ -export function trigger( - target: object, - type: TriggerOpTypes, - key?: unknown, - newValue?: unknown, - oldValue?: unknown, - oldTarget?: Map | Set -) { - const depsMap = targetMap.get(target) - if (!depsMap) { - // never been tracked - return - } - - let deps: (Dep | undefined)[] = [] - if (type === TriggerOpTypes.CLEAR) { - // collection being cleared - // trigger all effects for target - deps = [...depsMap.values()] - } else if (key === 'length' && isArray(target)) { - const newLength = Number(newValue) - depsMap.forEach((dep, key) => { - if (key === 'length' || (!isSymbol(key) && key >= newLength)) { - deps.push(dep) - } - }) - } else { - // schedule runs for SET | ADD | DELETE - if (key !== void 0) { - deps.push(depsMap.get(key)) - } - - // also run for iteration key on ADD | DELETE | Map.SET - switch (type) { - case TriggerOpTypes.ADD: - if (!isArray(target)) { - deps.push(depsMap.get(ITERATE_KEY)) - if (isMap(target)) { - deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) - } - } else if (isIntegerKey(key)) { - // new index added to array -> length changes - deps.push(depsMap.get('length')) - } - break - case TriggerOpTypes.DELETE: - if (!isArray(target)) { - deps.push(depsMap.get(ITERATE_KEY)) - if (isMap(target)) { - deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) - } - } - break - case TriggerOpTypes.SET: - if (isMap(target)) { - deps.push(depsMap.get(ITERATE_KEY)) - } - break - } - } - - const eventInfo = __DEV__ - ? { target, type, key, newValue, oldValue, oldTarget } - : undefined - - if (deps.length === 1) { - if (deps[0]) { - if (__DEV__) { - triggerEffects(deps[0], eventInfo) - } else { - triggerEffects(deps[0]) - } - } - } else { - const effects: ReactiveEffect[] = [] - for (const dep of deps) { - if (dep) { - effects.push(...dep) + if (dep.get(effect) !== effect._trackId) { + dep.set(effect, effect._trackId) + const oldDep = effect.deps[effect._depsLength] + if (oldDep !== dep) { + if (oldDep) { + cleanupDepEffect(oldDep, effect) } + effect.deps[effect._depsLength++] = dep + } else { + effect._depsLength++ } if (__DEV__) { - triggerEffects(createDep(effects), eventInfo) - } else { - triggerEffects(createDep(effects)) + effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!)) } } } -export function triggerEffects( - dep: Dep | ReactiveEffect[], - debuggerEventExtraInfo?: DebuggerEventExtraInfo -) { - // spread into array for stabilization - const effects = isArray(dep) ? dep : [...dep] - for (const effect of effects) { - if (effect.computed) { - triggerEffect(effect, debuggerEventExtraInfo) - } - } - for (const effect of effects) { - if (!effect.computed) { - triggerEffect(effect, debuggerEventExtraInfo) - } - } -} +const queueEffectSchedulers: (() => void)[] = [] -function triggerEffect( - effect: ReactiveEffect, +export function triggerEffects( + dep: Dep, + dirtyLevel: DirtyLevels, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { - if (effect !== activeEffect || effect.allowRecurse) { - if (__DEV__ && effect.onTrigger) { - effect.onTrigger(extend({ effect }, debuggerEventExtraInfo)) + pauseScheduling() + for (const effect of dep.keys()) { + if (!effect.allowRecurse && effect._runnings) { + continue } - if (effect.scheduler) { - effect.scheduler() - } else { - effect.run() + if ( + effect._dirtyLevel < dirtyLevel && + (!effect._runnings || dirtyLevel !== DirtyLevels.ComputedValueDirty) + ) { + const lastDirtyLevel = effect._dirtyLevel + effect._dirtyLevel = dirtyLevel + if ( + lastDirtyLevel === DirtyLevels.NotDirty && + (!effect._queryings || dirtyLevel !== DirtyLevels.ComputedValueDirty) + ) { + if (__DEV__) { + effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo)) + } + effect.trigger() + if (effect.scheduler) { + queueEffectSchedulers.push(effect.scheduler) + } + } } } -} - -export function getDepFromReactive(object: any, key: string | number | symbol) { - return targetMap.get(object)?.get(key) + resetScheduling() } diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index ee4da5b1935..9497527e81e 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -31,7 +31,6 @@ export { shallowReadonly, markRaw, toRaw, - ReactiveFlags /* @remove */, type Raw, type DeepReadonly, type ShallowReactive, @@ -49,12 +48,11 @@ export { deferredComputed } from './deferredComputed' export { effect, stop, - trigger, - track, enableTracking, pauseTracking, resetTracking, - ITERATE_KEY, + pauseScheduling, + resetScheduling, ReactiveEffect, type ReactiveEffectRunner, type ReactiveEffectOptions, @@ -63,6 +61,7 @@ export { type DebuggerEvent, type DebuggerEventExtraInfo } from './effect' +export { trigger, track, ITERATE_KEY } from './reactiveEffect' export { effectScope, EffectScope, @@ -71,5 +70,6 @@ export { } from './effectScope' export { TrackOpTypes /* @remove */, - TriggerOpTypes /* @remove */ -} from './operations' + TriggerOpTypes /* @remove */, + ReactiveFlags /* @remove */ +} from './constants' diff --git a/packages/reactivity/src/operations.ts b/packages/reactivity/src/operations.ts deleted file mode 100644 index 1b96e982571..00000000000 --- a/packages/reactivity/src/operations.ts +++ /dev/null @@ -1,15 +0,0 @@ -// using literal strings instead of numbers so that it's easier to inspect -// debugger events - -export const enum TrackOpTypes { - GET = 'get', - HAS = 'has', - ITERATE = 'iterate' -} - -export const enum TriggerOpTypes { - SET = 'set', - ADD = 'add', - DELETE = 'delete', - CLEAR = 'clear' -} diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index 1881955cf1c..2904c69abe2 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -12,14 +12,7 @@ import { shallowReadonlyCollectionHandlers } from './collectionHandlers' import type { UnwrapRefSimple, Ref, RawSymbol } from './ref' - -export const enum ReactiveFlags { - SKIP = '__v_skip', - IS_REACTIVE = '__v_isReactive', - IS_READONLY = '__v_isReadonly', - IS_SHALLOW = '__v_isShallow', - RAW = '__v_raw' -} +import { ReactiveFlags } from './constants' export interface Target { [ReactiveFlags.SKIP]?: boolean diff --git a/packages/reactivity/src/reactiveEffect.ts b/packages/reactivity/src/reactiveEffect.ts new file mode 100644 index 00000000000..d3474db3da1 --- /dev/null +++ b/packages/reactivity/src/reactiveEffect.ts @@ -0,0 +1,150 @@ +import { isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared' +import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants' +import { createDep, Dep } from './dep' +import { + activeEffect, + pauseScheduling, + resetScheduling, + shouldTrack, + trackEffect, + triggerEffects +} from './effect' + +// The main WeakMap that stores {target -> key -> dep} connections. +// Conceptually, it's easier to think of a dependency as a Dep class +// which maintains a Set of subscribers, but we simply store them as +// raw Sets to reduce memory overhead. +type KeyToDepMap = Map +const targetMap = new WeakMap() + +export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '') +export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '') + +/** + * Tracks access to a reactive property. + * + * This will check which effect is running at the moment and record it as dep + * which records all effects that depend on the reactive property. + * + * @param target - Object holding the reactive property. + * @param type - Defines the type of access to the reactive property. + * @param key - Identifier of the reactive property to track. + */ +export function track(target: object, type: TrackOpTypes, key: unknown) { + if (shouldTrack && activeEffect) { + let depsMap = targetMap.get(target) + if (!depsMap) { + targetMap.set(target, (depsMap = new Map())) + } + let dep = depsMap.get(key) + if (!dep) { + depsMap.set(key, (dep = createDep(() => depsMap!.delete(key)))) + } + trackEffect( + activeEffect, + dep, + __DEV__ + ? { + target, + type, + key + } + : void 0 + ) + } +} + +/** + * Finds all deps associated with the target (or a specific property) and + * triggers the effects stored within. + * + * @param target - The reactive object. + * @param type - Defines the type of the operation that needs to trigger effects. + * @param key - Can be used to target a specific reactive property in the target object. + */ +export function trigger( + target: object, + type: TriggerOpTypes, + key?: unknown, + newValue?: unknown, + oldValue?: unknown, + oldTarget?: Map | Set +) { + const depsMap = targetMap.get(target) + if (!depsMap) { + // never been tracked + return + } + + let deps: (Dep | undefined)[] = [] + if (type === TriggerOpTypes.CLEAR) { + // collection being cleared + // trigger all effects for target + deps = [...depsMap.values()] + } else if (key === 'length' && isArray(target)) { + const newLength = Number(newValue) + depsMap.forEach((dep, key) => { + if (key === 'length' || (!isSymbol(key) && key >= newLength)) { + deps.push(dep) + } + }) + } else { + // schedule runs for SET | ADD | DELETE + if (key !== void 0) { + deps.push(depsMap.get(key)) + } + + // also run for iteration key on ADD | DELETE | Map.SET + switch (type) { + case TriggerOpTypes.ADD: + if (!isArray(target)) { + deps.push(depsMap.get(ITERATE_KEY)) + if (isMap(target)) { + deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) + } + } else if (isIntegerKey(key)) { + // new index added to array -> length changes + deps.push(depsMap.get('length')) + } + break + case TriggerOpTypes.DELETE: + if (!isArray(target)) { + deps.push(depsMap.get(ITERATE_KEY)) + if (isMap(target)) { + deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) + } + } + break + case TriggerOpTypes.SET: + if (isMap(target)) { + deps.push(depsMap.get(ITERATE_KEY)) + } + break + } + } + + pauseScheduling() + for (const dep of deps) { + if (dep) { + triggerEffects( + dep, + DirtyLevels.Dirty, + __DEV__ + ? { + target, + type, + key, + newValue, + oldValue, + oldTarget + } + : void 0 + ) + } + } + resetScheduling() +} + +export function getDepFromReactive(object: any, key: string | number | symbol) { + return targetMap.get(object)?.get(key) +} diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 915f5760878..5a4dd710eab 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -1,11 +1,10 @@ import { activeEffect, - getDepFromReactive, shouldTrack, - trackEffects, + trackEffect, triggerEffects } from './effect' -import { TrackOpTypes, TriggerOpTypes } from './operations' +import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants' import { isArray, hasChanged, IfAny, isFunction, isObject } from '@vue/shared' import { isProxy, @@ -18,6 +17,8 @@ import { import type { ShallowReactiveMarker } from './reactive' import { CollectionTypes } from './collectionHandlers' import { createDep, Dep } from './dep' +import { ComputedRefImpl } from './computed' +import { getDepFromReactive } from './reactiveEffect' declare const RefSymbol: unique symbol export declare const RawSymbol: unique symbol @@ -40,32 +41,44 @@ type RefBase = { export function trackRefValue(ref: RefBase) { if (shouldTrack && activeEffect) { ref = toRaw(ref) - if (__DEV__) { - trackEffects(ref.dep || (ref.dep = createDep()), { - target: ref, - type: TrackOpTypes.GET, - key: 'value' - }) - } else { - trackEffects(ref.dep || (ref.dep = createDep())) - } + trackEffect( + activeEffect, + ref.dep || + (ref.dep = createDep( + () => (ref.dep = undefined), + ref instanceof ComputedRefImpl ? ref : undefined + )), + __DEV__ + ? { + target: ref, + type: TrackOpTypes.GET, + key: 'value' + } + : void 0 + ) } } -export function triggerRefValue(ref: RefBase, newVal?: any) { +export function triggerRefValue( + ref: RefBase, + dirtyLevel: DirtyLevels = DirtyLevels.Dirty, + newVal?: any +) { ref = toRaw(ref) const dep = ref.dep if (dep) { - if (__DEV__) { - triggerEffects(dep, { - target: ref, - type: TriggerOpTypes.SET, - key: 'value', - newValue: newVal - }) - } else { - triggerEffects(dep) - } + triggerEffects( + dep, + dirtyLevel, + __DEV__ + ? { + target: ref, + type: TriggerOpTypes.SET, + key: 'value', + newValue: newVal + } + : void 0 + ) } } @@ -158,7 +171,7 @@ class RefImpl { if (hasChanged(newVal, this._rawValue)) { this._rawValue = newVal this._value = useDirectValue ? newVal : toReactive(newVal) - triggerRefValue(this, newVal) + triggerRefValue(this, DirtyLevels.Dirty, newVal) } } } @@ -189,7 +202,7 @@ class RefImpl { * @see {@link https://vuejs.org/api/reactivity-advanced.html#triggerref} */ export function triggerRef(ref: Ref) { - triggerRefValue(ref, __DEV__ ? ref.value : void 0) + triggerRefValue(ref, DirtyLevels.Dirty, __DEV__ ? ref.value : void 0) } export type MaybeRef = T | Ref diff --git a/packages/runtime-core/src/apiAsyncComponent.ts b/packages/runtime-core/src/apiAsyncComponent.ts index 342339042ef..535cb83fb5d 100644 --- a/packages/runtime-core/src/apiAsyncComponent.ts +++ b/packages/runtime-core/src/apiAsyncComponent.ts @@ -187,6 +187,7 @@ export function defineAsyncComponent< if (instance.parent && isKeepAlive(instance.parent.vnode)) { // parent is keep-alive, force update so the loaded component's // name is taken into account + instance.parent.effect.dirty = true queueJob(instance.parent.update) } }) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index c307c4198a3..cedebb01af6 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -322,7 +322,7 @@ function doWatch( ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE) : INITIAL_WATCHER_VALUE const job: SchedulerJob = () => { - if (!effect.active) { + if (!effect.active || !effect.dirty) { return } if (cb) { @@ -376,7 +376,7 @@ function doWatch( scheduler = () => queueJob(job) } - const effect = new ReactiveEffect(getter, scheduler) + const effect = new ReactiveEffect(getter, NOOP, scheduler) const unwatch = () => { effect.stop() diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index b7ef1e07302..7b552c8f92a 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -267,7 +267,12 @@ export const publicPropertiesMap: PublicPropertiesMap = $root: i => getPublicInstance(i.root), $emit: i => i.emit, $options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type), - $forceUpdate: i => i.f || (i.f = () => queueJob(i.update)), + $forceUpdate: i => + i.f || + (i.f = () => { + i.effect.dirty = true + queueJob(i.update) + }), $nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)), $watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP) } as PublicPropertiesMap) diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index 9cb80b94ef0..ef0632384d6 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -246,6 +246,7 @@ const BaseTransitionImpl: ComponentOptions = { // #6835 // it also needs to be updated when active is undefined if (instance.update.active !== false) { + instance.effect.dirty = true instance.update() } } diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts index 1ce66a3da1e..cdf291989bd 100644 --- a/packages/runtime-core/src/hmr.ts +++ b/packages/runtime-core/src/hmr.ts @@ -93,6 +93,7 @@ function rerender(id: string, newRender?: Function) { instance.renderCache = [] // this flag forces child components with slot content to update isHmrUpdating = true + instance.effect.dirty = true instance.update() isHmrUpdating = false }) @@ -137,6 +138,7 @@ function reload(id: string, newComp: HMRComponent) { // 4. Force the parent instance to re-render. This will cause all updated // components to be unmounted and re-mounted. Queue the update so that we // don't end up forcing the same parent to re-render multiple times. + instance.parent.effect.dirty = true queueJob(instance.parent.update) } else if (instance.appContext.reload) { // root instance mounted via createApp() has a reload method diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 8799ecd473c..8dbc1c796d5 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1280,6 +1280,7 @@ function baseCreateRenderer( // double updating the same child component in the same flush. invalidateJob(instance.update) // instance.update is the reactive effect. + instance.effect.dirty = true instance.update() } } else { @@ -1544,11 +1545,16 @@ function baseCreateRenderer( // create reactive effect for rendering const effect = (instance.effect = new ReactiveEffect( componentUpdateFn, + NOOP, () => queueJob(update), instance.scope // track it in component's effect scope )) - const update: SchedulerJob = (instance.update = () => effect.run()) + const update: SchedulerJob = (instance.update = () => { + if (effect.dirty) { + effect.run() + } + }) update.id = instance.uid // allowRecurse // #1801, #2043 component render effects should allow recursive updates From 3c828f3cfbe18cd5392594f7e4b7ea45632cbd6e Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 28 Oct 2023 11:33:46 +0900 Subject: [PATCH 6/9] release: v3.4.0-alpha.1 --- CHANGELOG.md | 13 +++++ package.json | 2 +- packages/compiler-core/package.json | 4 +- packages/compiler-dom/package.json | 6 +-- packages/compiler-sfc/package.json | 12 ++--- packages/compiler-ssr/package.json | 6 +-- packages/dts-built-test/package.json | 2 +- packages/dts-test/package.json | 2 +- packages/reactivity-transform/package.json | 6 +-- packages/reactivity/package.json | 4 +- packages/runtime-core/package.json | 6 +-- packages/runtime-dom/package.json | 6 +-- packages/runtime-test/package.json | 6 +-- packages/server-renderer/package.json | 8 ++-- packages/sfc-playground/package.json | 2 +- packages/shared/package.json | 2 +- packages/template-explorer/package.json | 2 +- packages/vue-compat/package.json | 4 +- packages/vue/package.json | 12 ++--- pnpm-lock.yaml | 56 +++++++++++----------- 20 files changed, 87 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8cdb94b5a6..858b722d192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# [3.4.0-alpha.1](https://github.com/vuejs/core/compare/v3.3.7...v3.4.0-alpha.1) (2023-10-28) + + +### Features + +* **compiler-core:** export error message ([#8729](https://github.com/vuejs/core/issues/8729)) ([f7e80ee](https://github.com/vuejs/core/commit/f7e80ee4a065a9eaba98720abf415d9e87756cbd)) +* **compiler-sfc:** expose resolve type-based props and emits ([#8874](https://github.com/vuejs/core/issues/8874)) ([9e77580](https://github.com/vuejs/core/commit/9e77580c0c2f0d977bd0031a1d43cc334769d433)) +* export runtime error strings ([#9301](https://github.com/vuejs/core/issues/9301)) ([feb2f2e](https://github.com/vuejs/core/commit/feb2f2edce2d91218a5e9a52c81e322e4033296b)) +* **reactivity:** more efficient reactivity system ([#5912](https://github.com/vuejs/core/issues/5912)) ([16e06ca](https://github.com/vuejs/core/commit/16e06ca08f5a1e2af3fc7fb35de153dbe0c3087d)), closes [#311](https://github.com/vuejs/core/issues/311) [#1811](https://github.com/vuejs/core/issues/1811) [#6018](https://github.com/vuejs/core/issues/6018) [#7160](https://github.com/vuejs/core/issues/7160) [#8714](https://github.com/vuejs/core/issues/8714) [#9149](https://github.com/vuejs/core/issues/9149) [#9419](https://github.com/vuejs/core/issues/9419) [#9464](https://github.com/vuejs/core/issues/9464) +* **runtime-core:** add `once` option to watch ([#9034](https://github.com/vuejs/core/issues/9034)) ([a645e7a](https://github.com/vuejs/core/commit/a645e7aa51006516ba668b3a4365d296eb92ee7d)) + + + ## [3.3.7](https://github.com/vuejs/core/compare/v3.3.6...v3.3.7) (2023-10-24) diff --git a/package.json b/package.json index 092feef1382..1655ff8b546 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "3.3.7", + "version": "3.4.0-alpha.1", "packageManager": "pnpm@8.9.2", "type": "module", "scripts": { diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json index 8c9f06f3543..e32771ab1cc 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-core", - "version": "3.3.7", + "version": "3.4.0-alpha.1", "description": "@vue/compiler-core", "main": "index.js", "module": "dist/compiler-core.esm-bundler.js", @@ -33,7 +33,7 @@ "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-core#readme", "dependencies": { "@babel/parser": "^7.23.0", - "@vue/shared": "3.3.7", + "@vue/shared": "3.4.0-alpha.1", "estree-walker": "^2.0.2", "source-map-js": "^1.0.2" }, diff --git a/packages/compiler-dom/package.json b/packages/compiler-dom/package.json index 4e6ea338bb9..f39057c076b 100644 --- a/packages/compiler-dom/package.json +++ b/packages/compiler-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-dom", - "version": "3.3.7", + "version": "3.4.0-alpha.1", "description": "@vue/compiler-dom", "main": "index.js", "module": "dist/compiler-dom.esm-bundler.js", @@ -37,7 +37,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-dom#readme", "dependencies": { - "@vue/shared": "3.3.7", - "@vue/compiler-core": "3.3.7" + "@vue/shared": "3.4.0-alpha.1", + "@vue/compiler-core": "3.4.0-alpha.1" } } diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json index 550b5a7e927..6c28d1c7f03 100644 --- a/packages/compiler-sfc/package.json +++ b/packages/compiler-sfc/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-sfc", - "version": "3.3.7", + "version": "3.4.0-alpha.1", "description": "@vue/compiler-sfc", "main": "dist/compiler-sfc.cjs.js", "module": "dist/compiler-sfc.esm-browser.js", @@ -33,11 +33,11 @@ "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-sfc#readme", "dependencies": { "@babel/parser": "^7.23.0", - "@vue/compiler-core": "3.3.7", - "@vue/compiler-dom": "3.3.7", - "@vue/compiler-ssr": "3.3.7", - "@vue/reactivity-transform": "3.3.7", - "@vue/shared": "3.3.7", + "@vue/compiler-core": "3.4.0-alpha.1", + "@vue/compiler-dom": "3.4.0-alpha.1", + "@vue/compiler-ssr": "3.4.0-alpha.1", + "@vue/reactivity-transform": "3.4.0-alpha.1", + "@vue/shared": "3.4.0-alpha.1", "estree-walker": "^2.0.2", "magic-string": "^0.30.5", "postcss": "^8.4.31", diff --git a/packages/compiler-ssr/package.json b/packages/compiler-ssr/package.json index 556c43f5971..df467affd10 100644 --- a/packages/compiler-ssr/package.json +++ b/packages/compiler-ssr/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-ssr", - "version": "3.3.7", + "version": "3.4.0-alpha.1", "description": "@vue/compiler-ssr", "main": "dist/compiler-ssr.cjs.js", "types": "dist/compiler-ssr.d.ts", @@ -28,7 +28,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/compiler-ssr#readme", "dependencies": { - "@vue/shared": "3.3.7", - "@vue/compiler-dom": "3.3.7" + "@vue/shared": "3.4.0-alpha.1", + "@vue/compiler-dom": "3.4.0-alpha.1" } } diff --git a/packages/dts-built-test/package.json b/packages/dts-built-test/package.json index fb332328fb9..0a544787753 100644 --- a/packages/dts-built-test/package.json +++ b/packages/dts-built-test/package.json @@ -7,5 +7,5 @@ "@vue/reactivity": "workspace:*", "vue": "workspace:*" }, - "version": "3.3.7" + "version": "3.4.0-alpha.1" } diff --git a/packages/dts-test/package.json b/packages/dts-test/package.json index ac246e704af..ff3c8313ea4 100644 --- a/packages/dts-test/package.json +++ b/packages/dts-test/package.json @@ -5,5 +5,5 @@ "vue": "workspace:*", "@vue/dts-built-test": "workspace:*" }, - "version": "3.3.7" + "version": "3.4.0-alpha.1" } diff --git a/packages/reactivity-transform/package.json b/packages/reactivity-transform/package.json index b9f8a74f353..8e43685c5dd 100644 --- a/packages/reactivity-transform/package.json +++ b/packages/reactivity-transform/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity-transform", - "version": "3.3.7", + "version": "3.4.0-alpha.1", "description": "@vue/reactivity-transform", "main": "dist/reactivity-transform.cjs.js", "files": [ @@ -29,8 +29,8 @@ "homepage": "https://github.com/vuejs/core/tree/dev/packages/reactivity-transform#readme", "dependencies": { "@babel/parser": "^7.23.0", - "@vue/compiler-core": "3.3.7", - "@vue/shared": "3.3.7", + "@vue/compiler-core": "3.4.0-alpha.1", + "@vue/shared": "3.4.0-alpha.1", "estree-walker": "^2.0.2", "magic-string": "^0.30.5" }, diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json index 712bb26a0eb..7244b9a926a 100644 --- a/packages/reactivity/package.json +++ b/packages/reactivity/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity", - "version": "3.3.7", + "version": "3.4.0-alpha.1", "description": "@vue/reactivity", "main": "index.js", "module": "dist/reactivity.esm-bundler.js", @@ -36,6 +36,6 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/reactivity#readme", "dependencies": { - "@vue/shared": "3.3.7" + "@vue/shared": "3.4.0-alpha.1" } } diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json index a6335626bea..c89fd157ef2 100644 --- a/packages/runtime-core/package.json +++ b/packages/runtime-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-core", - "version": "3.3.7", + "version": "3.4.0-alpha.1", "description": "@vue/runtime-core", "main": "index.js", "module": "dist/runtime-core.esm-bundler.js", @@ -32,7 +32,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-core#readme", "dependencies": { - "@vue/shared": "3.3.7", - "@vue/reactivity": "3.3.7" + "@vue/shared": "3.4.0-alpha.1", + "@vue/reactivity": "3.4.0-alpha.1" } } diff --git a/packages/runtime-dom/package.json b/packages/runtime-dom/package.json index 4ff70383f46..f09d44bf6d6 100644 --- a/packages/runtime-dom/package.json +++ b/packages/runtime-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-dom", - "version": "3.3.7", + "version": "3.4.0-alpha.1", "description": "@vue/runtime-dom", "main": "index.js", "module": "dist/runtime-dom.esm-bundler.js", @@ -35,8 +35,8 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-dom#readme", "dependencies": { - "@vue/shared": "3.3.7", - "@vue/runtime-core": "3.3.7", + "@vue/shared": "3.4.0-alpha.1", + "@vue/runtime-core": "3.4.0-alpha.1", "csstype": "^3.1.2" } } diff --git a/packages/runtime-test/package.json b/packages/runtime-test/package.json index 77d4fe2dc85..7db3d1cb6ae 100644 --- a/packages/runtime-test/package.json +++ b/packages/runtime-test/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-test", - "version": "3.3.7", + "version": "3.4.0-alpha.1", "description": "@vue/runtime-test", "private": true, "main": "index.js", @@ -25,7 +25,7 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/runtime-test#readme", "dependencies": { - "@vue/shared": "3.3.7", - "@vue/runtime-core": "3.3.7" + "@vue/shared": "3.4.0-alpha.1", + "@vue/runtime-core": "3.4.0-alpha.1" } } diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json index cf6ea02739a..68e5005aba1 100644 --- a/packages/server-renderer/package.json +++ b/packages/server-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@vue/server-renderer", - "version": "3.3.7", + "version": "3.4.0-alpha.1", "description": "@vue/server-renderer", "main": "index.js", "module": "dist/server-renderer.esm-bundler.js", @@ -32,10 +32,10 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/server-renderer#readme", "peerDependencies": { - "vue": "3.3.7" + "vue": "3.4.0-alpha.1" }, "dependencies": { - "@vue/shared": "3.3.7", - "@vue/compiler-ssr": "3.3.7" + "@vue/shared": "3.4.0-alpha.1", + "@vue/compiler-ssr": "3.4.0-alpha.1" } } diff --git a/packages/sfc-playground/package.json b/packages/sfc-playground/package.json index f4f64c19163..96b5da92349 100644 --- a/packages/sfc-playground/package.json +++ b/packages/sfc-playground/package.json @@ -1,6 +1,6 @@ { "name": "@vue/sfc-playground", - "version": "3.3.7", + "version": "3.4.0-alpha.1", "type": "module", "private": true, "scripts": { diff --git a/packages/shared/package.json b/packages/shared/package.json index 8f81872f23b..ebe0217232d 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@vue/shared", - "version": "3.3.7", + "version": "3.4.0-alpha.1", "description": "internal utils shared across @vue packages", "main": "index.js", "module": "dist/shared.esm-bundler.js", diff --git a/packages/template-explorer/package.json b/packages/template-explorer/package.json index 9f525a66c87..5f942030a93 100644 --- a/packages/template-explorer/package.json +++ b/packages/template-explorer/package.json @@ -1,6 +1,6 @@ { "name": "@vue/template-explorer", - "version": "3.3.7", + "version": "3.4.0-alpha.1", "private": true, "buildOptions": { "formats": [ diff --git a/packages/vue-compat/package.json b/packages/vue-compat/package.json index a63b2fccaf4..c1053dc1df3 100644 --- a/packages/vue-compat/package.json +++ b/packages/vue-compat/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compat", - "version": "3.3.7", + "version": "3.4.0-alpha.1", "description": "Vue 3 compatibility build for Vue 2", "main": "index.js", "module": "dist/vue.runtime.esm-bundler.js", @@ -43,6 +43,6 @@ "source-map-js": "^1.0.2" }, "peerDependencies": { - "vue": "3.3.7" + "vue": "3.4.0-alpha.1" } } diff --git a/packages/vue/package.json b/packages/vue/package.json index 65d606a7c2a..2b0f68aba94 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "vue", - "version": "3.3.7", + "version": "3.4.0-alpha.1", "description": "The progressive JavaScript framework for building modern web UI.", "main": "index.js", "module": "dist/vue.runtime.esm-bundler.js", @@ -96,11 +96,11 @@ }, "homepage": "https://github.com/vuejs/core/tree/main/packages/vue#readme", "dependencies": { - "@vue/shared": "3.3.7", - "@vue/compiler-dom": "3.3.7", - "@vue/runtime-dom": "3.3.7", - "@vue/compiler-sfc": "3.3.7", - "@vue/server-renderer": "3.3.7" + "@vue/shared": "3.4.0-alpha.1", + "@vue/compiler-dom": "3.4.0-alpha.1", + "@vue/runtime-dom": "3.4.0-alpha.1", + "@vue/compiler-sfc": "3.4.0-alpha.1", + "@vue/server-renderer": "3.4.0-alpha.1" }, "peerDependencies": { "typescript": "*" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d077b9b1e49..3a88647f41c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,7 +162,7 @@ importers: specifier: ^7.23.0 version: 7.23.0 '@vue/shared': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../shared estree-walker: specifier: ^2.0.2 @@ -178,10 +178,10 @@ importers: packages/compiler-dom: dependencies: '@vue/compiler-core': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../compiler-core '@vue/shared': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../shared packages/compiler-sfc: @@ -190,19 +190,19 @@ importers: specifier: ^7.23.0 version: 7.23.0 '@vue/compiler-core': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../compiler-core '@vue/compiler-dom': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../compiler-dom '@vue/compiler-ssr': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../compiler-ssr '@vue/reactivity-transform': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../reactivity-transform '@vue/shared': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../shared estree-walker: specifier: ^2.0.2 @@ -251,10 +251,10 @@ importers: packages/compiler-ssr: dependencies: '@vue/compiler-dom': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../compiler-dom '@vue/shared': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../shared packages/dts-built-test: @@ -281,7 +281,7 @@ importers: packages/reactivity: dependencies: '@vue/shared': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../shared packages/reactivity-transform: @@ -290,10 +290,10 @@ importers: specifier: ^7.23.0 version: 7.23.0 '@vue/compiler-core': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../compiler-core '@vue/shared': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../shared estree-walker: specifier: ^2.0.2 @@ -312,19 +312,19 @@ importers: packages/runtime-core: dependencies: '@vue/reactivity': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../reactivity '@vue/shared': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../shared packages/runtime-dom: dependencies: '@vue/runtime-core': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../runtime-core '@vue/shared': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../shared csstype: specifier: ^3.1.2 @@ -333,22 +333,22 @@ importers: packages/runtime-test: dependencies: '@vue/runtime-core': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../runtime-core '@vue/shared': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../shared packages/server-renderer: dependencies: '@vue/compiler-ssr': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../compiler-ssr '@vue/shared': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../shared vue: - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../vue packages/sfc-playground: @@ -387,19 +387,19 @@ importers: packages/vue: dependencies: '@vue/compiler-dom': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../compiler-dom '@vue/compiler-sfc': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../compiler-sfc '@vue/runtime-dom': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../runtime-dom '@vue/server-renderer': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../server-renderer '@vue/shared': - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../shared typescript: specifier: '*' @@ -417,7 +417,7 @@ importers: specifier: ^1.0.2 version: 1.0.2 vue: - specifier: 3.3.7 + specifier: 3.4.0-alpha.1 version: link:../vue packages: From 48b47a1ab63577e2dbd91947eea544e3ef185b85 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Tue, 31 Oct 2023 22:19:40 +0800 Subject: [PATCH 7/9] feat(reactivity): expose last result for computed getter (#9497) --- packages/reactivity/src/computed.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index 09247360d06..c48e2f0df2c 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -16,8 +16,8 @@ export interface WritableComputedRef extends Ref { readonly effect: ReactiveEffect } -export type ComputedGetter = (...args: any[]) => T -export type ComputedSetter = (v: T) => void +export type ComputedGetter = (oldValue?: T) => T +export type ComputedSetter = (newValue: T) => void export interface WritableComputedOptions { get: ComputedGetter @@ -41,9 +41,10 @@ export class ComputedRefImpl { isReadonly: boolean, isSSR: boolean ) { - this.effect = new ReactiveEffect(getter, () => { - triggerRefValue(this, DirtyLevels.ComputedValueMaybeDirty) - }) + this.effect = new ReactiveEffect( + () => getter(this._value), + () => triggerRefValue(this, DirtyLevels.ComputedValueMaybeDirty) + ) this.effect.computed = this this.effect.active = this._cacheable = !isSSR this[ReactiveFlags.IS_READONLY] = isReadonly From 26399aa6fac1596b294ffeba06bb498d86f5508c Mon Sep 17 00:00:00 2001 From: zhiyuanzmj <32807958+zhiyuanzmj@users.noreply.github.com> Date: Thu, 2 Nov 2023 17:48:11 +0800 Subject: [PATCH 8/9] feat(compiler-core): support v-bind shorthand for key and value with the same name (#9451) --- .../__tests__/transforms/vBind.spec.ts | 120 +++++++++++++++++- .../compiler-core/src/transforms/vBind.ts | 16 ++- .../compileTemplate.spec.ts.snap | 1 - .../__tests__/compileTemplate.spec.ts | 2 +- 4 files changed, 133 insertions(+), 6 deletions(-) diff --git a/packages/compiler-core/__tests__/transforms/vBind.spec.ts b/packages/compiler-core/__tests__/transforms/vBind.spec.ts index 322cf9d1bde..2e94dc1f7de 100644 --- a/packages/compiler-core/__tests__/transforms/vBind.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vBind.spec.ts @@ -72,6 +72,60 @@ describe('compiler: transform v-bind', () => { }) }) + test('no expression', () => { + const node = parseWithVBind(`
`) + const props = (node.codegenNode as VNodeCall).props as ObjectExpression + expect(props.properties[0]).toMatchObject({ + key: { + content: `id`, + isStatic: true, + loc: { + start: { + line: 1, + column: 13, + offset: 12 + }, + end: { + line: 1, + column: 15, + offset: 14 + } + } + }, + value: { + content: `id`, + isStatic: false, + loc: { + start: { + line: 1, + column: 1, + offset: 0 + }, + end: { + line: 1, + column: 1, + offset: 0 + } + } + } + }) + }) + + test('no expression (shorthand)', () => { + const node = parseWithVBind(`
`) + const props = (node.codegenNode as VNodeCall).props as ObjectExpression + expect(props.properties[0]).toMatchObject({ + key: { + content: `id`, + isStatic: true + }, + value: { + content: `id`, + isStatic: false + } + }) + }) + test('dynamic arg', () => { const node = parseWithVBind(`
`) const props = (node.codegenNode as VNodeCall).props as CallExpression @@ -98,9 +152,9 @@ describe('compiler: transform v-bind', () => { }) }) - test('should error if no expression', () => { + test('should error if empty expression', () => { const onError = vi.fn() - const node = parseWithVBind(`
`, { onError }) + const node = parseWithVBind(`
`, { onError }) const props = (node.codegenNode as VNodeCall).props as ObjectExpression expect(onError.mock.calls[0][0]).toMatchObject({ code: ErrorCodes.X_V_BIND_NO_EXPRESSION, @@ -111,7 +165,7 @@ describe('compiler: transform v-bind', () => { }, end: { line: 1, - column: 16 + column: 19 } } }) @@ -142,6 +196,21 @@ describe('compiler: transform v-bind', () => { }) }) + test('.camel modifier w/ no expression', () => { + const node = parseWithVBind(`
`) + const props = (node.codegenNode as VNodeCall).props as ObjectExpression + expect(props.properties[0]).toMatchObject({ + key: { + content: `fooBar`, + isStatic: true + }, + value: { + content: `fooBar`, + isStatic: false + } + }) + }) + test('.camel modifier w/ dynamic arg', () => { const node = parseWithVBind(`
`) const props = (node.codegenNode as VNodeCall).props as CallExpression @@ -219,6 +288,21 @@ describe('compiler: transform v-bind', () => { }) }) + test('.prop modifier w/ no expression', () => { + const node = parseWithVBind(`
`) + const props = (node.codegenNode as VNodeCall).props as ObjectExpression + expect(props.properties[0]).toMatchObject({ + key: { + content: `.fooBar`, + isStatic: true + }, + value: { + content: `fooBar`, + isStatic: false + } + }) + }) + test('.prop modifier w/ dynamic arg', () => { const node = parseWithVBind(`
`) const props = (node.codegenNode as VNodeCall).props as CallExpression @@ -296,6 +380,21 @@ describe('compiler: transform v-bind', () => { }) }) + test('.prop modifier (shortband) w/ no expression', () => { + const node = parseWithVBind(`
`) + const props = (node.codegenNode as VNodeCall).props as ObjectExpression + expect(props.properties[0]).toMatchObject({ + key: { + content: `.fooBar`, + isStatic: true + }, + value: { + content: `fooBar`, + isStatic: false + } + }) + }) + test('.attr modifier', () => { const node = parseWithVBind(`
`) const props = (node.codegenNode as VNodeCall).props as ObjectExpression @@ -310,4 +409,19 @@ describe('compiler: transform v-bind', () => { } }) }) + + test('.attr modifier w/ no expression', () => { + const node = parseWithVBind(`
`) + const props = (node.codegenNode as VNodeCall).props as ObjectExpression + expect(props.properties[0]).toMatchObject({ + key: { + content: `^foo-bar`, + isStatic: true + }, + value: { + content: `fooBar`, + isStatic: false + } + }) + }) }) diff --git a/packages/compiler-core/src/transforms/vBind.ts b/packages/compiler-core/src/transforms/vBind.ts index ffaf903b9e8..9f85d6b3d63 100644 --- a/packages/compiler-core/src/transforms/vBind.ts +++ b/packages/compiler-core/src/transforms/vBind.ts @@ -3,17 +3,19 @@ import { createObjectProperty, createSimpleExpression, ExpressionNode, + locStub, NodeTypes } from '../ast' import { createCompilerError, ErrorCodes } from '../errors' import { camelize } from '@vue/shared' import { CAMELIZE } from '../runtimeHelpers' +import { processExpression } from './transformExpression' // v-bind without arg is handled directly in ./transformElements.ts due to it affecting // codegen for the entire props object. This transform here is only for v-bind // *with* args. export const transformBind: DirectiveTransform = (dir, _node, context) => { - const { exp, modifiers, loc } = dir + const { modifiers, loc } = dir const arg = dir.arg! if (arg.type !== NodeTypes.SIMPLE_EXPRESSION) { @@ -46,6 +48,18 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => { } } + // :arg is replaced by :arg="arg" + let { exp } = dir + if (!exp && arg.type === NodeTypes.SIMPLE_EXPRESSION) { + const propName = camelize(arg.loc.source) + const simpleExpression = createSimpleExpression(propName, false, { + ...locStub, + source: propName + }) + + exp = dir.exp = processExpression(simpleExpression, context) + } + if ( !exp || (exp.type === NodeTypes.SIMPLE_EXPRESSION && !exp.content.trim()) diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileTemplate.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileTemplate.spec.ts.snap index 35b87c4ec71..f97eef6094e 100644 --- a/packages/compiler-sfc/__tests__/__snapshots__/compileTemplate.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/__snapshots__/compileTemplate.spec.ts.snap @@ -79,7 +79,6 @@ exports[`source map 1`] = ` exports[`template errors 1`] = ` [ [SyntaxError: Error parsing JavaScript expression: Unexpected token (1:3)], - [SyntaxError: v-bind is missing expression.], [SyntaxError: v-model can only be used on ,