From ca7daefaa15a192046d22d060220cd595a6a275f Mon Sep 17 00:00:00 2001 From: blackie Date: Fri, 8 Jul 2022 09:44:41 +0800 Subject: [PATCH 01/35] fix(ssr/reactivity): fix array setting error at created in ssr [#12632] (#12633) fix #12632 --- packages/server-renderer/test/ssr-reactivity.spec.ts | 4 ++++ src/core/observer/index.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/server-renderer/test/ssr-reactivity.spec.ts b/packages/server-renderer/test/ssr-reactivity.spec.ts index f9b145c7740..773fa9dd650 100644 --- a/packages/server-renderer/test/ssr-reactivity.spec.ts +++ b/packages/server-renderer/test/ssr-reactivity.spec.ts @@ -93,6 +93,10 @@ describe('SSR Reactive', () => { set(state.value, 1, {}) expect(isReactive(state.value[1])).toBe(true) + + const rawArr = [] + set(rawArr, 1, {}) + expect(isReactive(rawArr[1])).toBe(false) }) // #550 diff --git a/src/core/observer/index.ts b/src/core/observer/index.ts index 004a65fe6b5..84744dd85f9 100644 --- a/src/core/observer/index.ts +++ b/src/core/observer/index.ts @@ -241,7 +241,7 @@ export function set( target.length = Math.max(target.length, key) target.splice(key, 1, val) // when mocking for SSR, array methods are not hijacked - if (!ob.shallow && ob.mock) { + if (ob && !ob.shallow && ob.mock) { observe(val, false, true) } return val From b70a2585fcd102def2bb5a3b2b589edf5311122d Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 8 Jul 2022 09:45:11 +0800 Subject: [PATCH 02/35] fix(compiler-sfc): use safer deindent default for compatibility with previous behavior --- packages/compiler-sfc/src/parseComponent.ts | 11 +++++++---- packages/compiler-sfc/test/parseComponent.spec.ts | 13 ++++++------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/compiler-sfc/src/parseComponent.ts b/packages/compiler-sfc/src/parseComponent.ts index 39322a3efd4..65b858c9fc0 100644 --- a/packages/compiler-sfc/src/parseComponent.ts +++ b/packages/compiler-sfc/src/parseComponent.ts @@ -10,7 +10,6 @@ export const DEFAULT_FILENAME = 'anonymous.vue' const splitRE = /\r?\n/g const replaceRE = /./g const isSpecialTag = makeMap('script,style,template', true) -const isNeedIndentLang = makeMap('pug,jade') export interface SFCCustomBlock { type: string @@ -179,9 +178,13 @@ export function parseComponent( currentBlock.end = start let text = source.slice(currentBlock.start, currentBlock.end) if ( - options.deindent || - // certain langs like pug are indent sensitive, preserve old behavior - (currentBlock.lang && isNeedIndentLang(currentBlock.lang)) + options.deindent === true || + // by default, deindent unless it's script with default lang or ts + (options.deindent !== false && + !( + currentBlock.type === 'script' && + (!currentBlock.lang || currentBlock.lang === 'ts') + )) ) { text = deindent(text) } diff --git a/packages/compiler-sfc/test/parseComponent.spec.ts b/packages/compiler-sfc/test/parseComponent.spec.ts index 4624bb2040d..83f73483e0c 100644 --- a/packages/compiler-sfc/test/parseComponent.spec.ts +++ b/packages/compiler-sfc/test/parseComponent.spec.ts @@ -25,8 +25,7 @@ describe('Single File Component parser', () => {
- `, - { deindent: true } + ` ) expect(res.template!.content.trim()).toBe('
hi
') expect(res.styles.length).toBe(4) @@ -76,8 +75,7 @@ describe('Single File Component parser', () => { ` const deindentDefault = parseComponent(content.trim(), { - pad: false, - deindent: true + pad: false }) const deindentEnabled = parseComponent(content.trim(), { pad: false, @@ -89,7 +87,9 @@ describe('Single File Component parser', () => { }) expect(deindentDefault.template!.content).toBe('\n
\n') - expect(deindentDefault.script!.content).toBe('\nexport default {}\n') + expect(deindentDefault.script!.content).toBe( + '\n export default {}\n ' + ) expect(deindentDefault.styles[0].content).toBe('\nh1 { color: red }\n') expect(deindentEnabled.template!.content).toBe('\n
\n') expect(deindentEnabled.script!.content).toBe('\nexport default {}\n') @@ -203,8 +203,7 @@ describe('Single File Component parser', () => { } - `, - { deindent: true } + ` ) expect(res.customBlocks.length).toBe(4) From 26ff4bc0ed75d8bf7921523a2e546df24ec81d8f Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 8 Jul 2022 10:39:23 +0800 Subject: [PATCH 03/35] fix(types): global component registration type compat w/ defineComponent fix #12622 --- types/test/v3/define-component-test.tsx | 13 +++++++------ types/vue.d.ts | 5 +++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/types/test/v3/define-component-test.tsx b/types/test/v3/define-component-test.tsx index 7bdbdb82444..9b01e3e45f4 100644 --- a/types/test/v3/define-component-test.tsx +++ b/types/test/v3/define-component-test.tsx @@ -1,3 +1,4 @@ +import Vue, { VueConstructor } from '../../index' import { Component, defineComponent, @@ -8,12 +9,12 @@ import { } from '../../index' import { describe, test, expectType, expectError, IsUnion } from '../utils' -defineComponent({ - props: { - foo: Number - }, - render() { - this.foo +describe('compat with v2 APIs', () => { + const comp = defineComponent({}) + + Vue.component('foo', comp) + function install(app: VueConstructor) { + app.component('foo', comp) } }) diff --git a/types/vue.d.ts b/types/vue.d.ts index afa66b5c2c5..82cad69e793 100644 --- a/types/vue.d.ts +++ b/types/vue.d.ts @@ -14,6 +14,7 @@ import { } from './options' import { VNode, VNodeData, VNodeChildren, NormalizedScopedSlot } from './vnode' import { PluginFunction, PluginObject } from './plugin' +import { DefineComponent } from './v3-define-component' export interface CreateElement { ( @@ -313,6 +314,10 @@ export interface VueConstructor { id: string, definition?: ComponentOptions ): ExtendedVue + component>( + id: string, + definition?: T + ): T use( plugin: PluginObject | PluginFunction, From 9d12106e211e0cbf33f9066606a8ff29f8cc8e8d Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 8 Jul 2022 10:55:23 +0800 Subject: [PATCH 04/35] feat: defineAsyncComponent close #12608 --- src/v3/apiAsyncComponent.ts | 117 +++++++++ src/v3/index.ts | 2 + .../features/v3/apiAsyncComponent.spec.ts | 241 ++++++++++++++++++ types/test/v3/define-async-component-test.tsx | 19 ++ types/v3-define-async-component.d.ts | 26 ++ types/v3-define-component.d.ts | 1 - 6 files changed, 405 insertions(+), 1 deletion(-) create mode 100644 src/v3/apiAsyncComponent.ts create mode 100644 test/unit/features/v3/apiAsyncComponent.spec.ts create mode 100644 types/test/v3/define-async-component-test.tsx create mode 100644 types/v3-define-async-component.d.ts diff --git a/src/v3/apiAsyncComponent.ts b/src/v3/apiAsyncComponent.ts new file mode 100644 index 00000000000..aca3a7a0ab8 --- /dev/null +++ b/src/v3/apiAsyncComponent.ts @@ -0,0 +1,117 @@ +import { warn, isFunction, isObject } from 'core/util' + +interface AsyncComponentOptions { + loader: Function + loadingComponent?: any + errorComponent?: any + delay?: number + timeout?: number + suspensible?: boolean + onError?: ( + error: Error, + retry: () => void, + fail: () => void, + attempts: number + ) => any +} + +type AsyncComponentFactory = () => { + component: Promise + loading?: any + error?: any + delay?: number + timeout?: number +} + +/** + * v3-compatible async component API. + * @internal the type is manually declared in /types/v3-define-async-component.d.ts + * because it relies on existing manual types + */ +export function defineAsyncComponent( + source: (() => any) | AsyncComponentOptions +): AsyncComponentFactory { + if (isFunction(source)) { + source = { loader: source } as AsyncComponentOptions + } + + const { + loader, + loadingComponent, + errorComponent, + delay = 200, + timeout, // undefined = never times out + suspensible = false, // in Vue 3 default is true + onError: userOnError + } = source + + if (__DEV__ && suspensible) { + warn( + `The suspensiblbe option for async components is not supported in Vue2. It is ignored.` + ) + } + + let pendingRequest: Promise | null = null + + let retries = 0 + const retry = () => { + retries++ + pendingRequest = null + return load() + } + + const load = (): Promise => { + let thisRequest: Promise + return ( + pendingRequest || + (thisRequest = pendingRequest = + loader() + .catch(err => { + err = err instanceof Error ? err : new Error(String(err)) + if (userOnError) { + return new Promise((resolve, reject) => { + const userRetry = () => resolve(retry()) + const userFail = () => reject(err) + userOnError(err, userRetry, userFail, retries + 1) + }) + } else { + throw err + } + }) + .then((comp: any) => { + if (thisRequest !== pendingRequest && pendingRequest) { + return pendingRequest + } + if (__DEV__ && !comp) { + warn( + `Async component loader resolved to undefined. ` + + `If you are using retry(), make sure to return its return value.` + ) + } + // interop module default + if ( + comp && + (comp.__esModule || comp[Symbol.toStringTag] === 'Module') + ) { + comp = comp.default + } + if (__DEV__ && comp && !isObject(comp) && !isFunction(comp)) { + throw new Error(`Invalid async component load result: ${comp}`) + } + return comp + })) + ) + } + + return () => { + const component = load() + + return { + component, + delay, + timeout, + error: errorComponent, + loading: loadingComponent + } + } +} diff --git a/src/v3/index.ts b/src/v3/index.ts index 75c21ad32f3..ec9d59e5444 100644 --- a/src/v3/index.ts +++ b/src/v3/index.ts @@ -87,4 +87,6 @@ export function defineComponent(options: any) { return options } +export { defineAsyncComponent } from './apiAsyncComponent' + export * from './apiLifecycle' diff --git a/test/unit/features/v3/apiAsyncComponent.spec.ts b/test/unit/features/v3/apiAsyncComponent.spec.ts new file mode 100644 index 00000000000..2970738f9d5 --- /dev/null +++ b/test/unit/features/v3/apiAsyncComponent.spec.ts @@ -0,0 +1,241 @@ +import Vue from 'vue' +import { defineAsyncComponent, h, ref, nextTick, defineComponent } from 'v3' +import { Component } from 'types/component' + +const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n)) + +const loadingComponent = defineComponent({ + template: `
loading
` +}) + +const resolvedComponent = defineComponent({ + template: `
resolved
` +}) + +describe('api: defineAsyncComponent', () => { + afterEach(() => { + Vue.config.errorHandler = undefined + }) + + test('simple usage', async () => { + let resolve: (comp: Component) => void + const Foo = defineAsyncComponent( + () => + new Promise(r => { + resolve = r as any + }) + ) + + const toggle = ref(true) + + const vm = new Vue({ + render: () => (toggle.value ? h(Foo) : null) + }).$mount() + + expect(vm.$el.nodeType).toBe(8) + + resolve!(resolvedComponent) + // first time resolve, wait for macro task since there are multiple + // microtasks / .then() calls + await timeout() + expect(vm.$el.innerHTML).toBe('resolved') + + toggle.value = false + await nextTick() + expect(vm.$el.nodeType).toBe(8) + + // already resolved component should update on nextTick + toggle.value = true + await nextTick() + expect(vm.$el.innerHTML).toBe('resolved') + }) + + test('with loading component', async () => { + let resolve: (comp: Component) => void + const Foo = defineAsyncComponent({ + loader: () => + new Promise(r => { + resolve = r as any + }), + loadingComponent, + delay: 1 // defaults to 200 + }) + + const toggle = ref(true) + + const vm = new Vue({ + render: () => (toggle.value ? h(Foo) : null) + }).$mount() + + // due to the delay, initial mount should be empty + expect(vm.$el.nodeType).toBe(8) + + // loading show up after delay + await timeout(1) + expect(vm.$el.innerHTML).toBe('loading') + + resolve!(resolvedComponent) + await timeout() + expect(vm.$el.innerHTML).toBe('resolved') + + toggle.value = false + await nextTick() + expect(vm.$el.nodeType).toBe(8) + + // already resolved component should update on nextTick without loading + // state + toggle.value = true + await nextTick() + expect(vm.$el.innerHTML).toBe('resolved') + }) + + test('error with error component', async () => { + let reject: (e: Error) => void + const Foo = defineAsyncComponent({ + loader: () => + new Promise((_resolve, _reject) => { + reject = _reject + }), + errorComponent: { + template: `
errored
` + } + }) + + const toggle = ref(true) + + const vm = new Vue({ + render: () => (toggle.value ? h(Foo) : null) + }).$mount() + + expect(vm.$el.nodeType).toBe(8) + + const err = new Error('errored') + reject!(err) + await timeout() + expect('Failed to resolve async').toHaveBeenWarned() + expect(vm.$el.innerHTML).toBe('errored') + + toggle.value = false + await nextTick() + expect(vm.$el.nodeType).toBe(8) + }) + + test('retry (success)', async () => { + let loaderCallCount = 0 + let resolve: (comp: Component) => void + let reject: (e: Error) => void + + const Foo = defineAsyncComponent({ + loader: () => { + loaderCallCount++ + return new Promise((_resolve, _reject) => { + resolve = _resolve as any + reject = _reject + }) + }, + onError(error, retry, fail) { + if (error.message.match(/foo/)) { + retry() + } else { + fail() + } + } + }) + + const vm = new Vue({ + render: () => h(Foo) + }).$mount() + + expect(vm.$el.nodeType).toBe(8) + expect(loaderCallCount).toBe(1) + + const err = new Error('foo') + reject!(err) + await timeout() + expect(loaderCallCount).toBe(2) + expect(vm.$el.nodeType).toBe(8) + + // should render this time + resolve!(resolvedComponent) + await timeout() + expect(vm.$el.innerHTML).toBe('resolved') + }) + + test('retry (skipped)', async () => { + let loaderCallCount = 0 + let reject: (e: Error) => void + + const Foo = defineAsyncComponent({ + loader: () => { + loaderCallCount++ + return new Promise((_resolve, _reject) => { + reject = _reject + }) + }, + onError(error, retry, fail) { + if (error.message.match(/bar/)) { + retry() + } else { + fail() + } + } + }) + + const vm = new Vue({ + render: () => h(Foo) + }).$mount() + + expect(vm.$el.nodeType).toBe(8) + expect(loaderCallCount).toBe(1) + + const err = new Error('foo') + reject!(err) + await timeout() + // should fail because retryWhen returns false + expect(loaderCallCount).toBe(1) + expect(vm.$el.nodeType).toBe(8) + expect('Failed to resolve async').toHaveBeenWarned() + }) + + test('retry (fail w/ max retry attempts)', async () => { + let loaderCallCount = 0 + let reject: (e: Error) => void + + const Foo = defineAsyncComponent({ + loader: () => { + loaderCallCount++ + return new Promise((_resolve, _reject) => { + reject = _reject + }) + }, + onError(error, retry, fail, attempts) { + if (error.message.match(/foo/) && attempts <= 1) { + retry() + } else { + fail() + } + } + }) + + const vm = new Vue({ + render: () => h(Foo) + }).$mount() + + expect(vm.$el.nodeType).toBe(8) + expect(loaderCallCount).toBe(1) + + // first retry + const err = new Error('foo') + reject!(err) + await timeout() + expect(loaderCallCount).toBe(2) + expect(vm.$el.nodeType).toBe(8) + + // 2nd retry, should fail due to reaching maxRetries + reject!(err) + await timeout() + expect(loaderCallCount).toBe(2) + expect(vm.$el.nodeType).toBe(8) + expect('Failed to resolve async').toHaveBeenWarned() + }) +}) diff --git a/types/test/v3/define-async-component-test.tsx b/types/test/v3/define-async-component-test.tsx new file mode 100644 index 00000000000..f4fbe4ba452 --- /dev/null +++ b/types/test/v3/define-async-component-test.tsx @@ -0,0 +1,19 @@ +import { defineAsyncComponent } from '../../v3-define-async-component' +import { defineComponent } from '../../v3-define-component' + +defineAsyncComponent(() => Promise.resolve({})) + +// @ts-expect-error +defineAsyncComponent({}) + +defineAsyncComponent({ + loader: () => Promise.resolve({}), + loadingComponent: defineComponent({}), + errorComponent: defineComponent({}), + delay: 123, + timeout: 3000, + onError(err, retry, fail, attempts) { + retry() + fail() + } +}) diff --git a/types/v3-define-async-component.d.ts b/types/v3-define-async-component.d.ts new file mode 100644 index 00000000000..8648ef6229f --- /dev/null +++ b/types/v3-define-async-component.d.ts @@ -0,0 +1,26 @@ +import { AsyncComponent, Component } from './options' + +export type AsyncComponentResolveResult = T | { default: T } // es modules + +export type AsyncComponentLoader = () => Promise< + AsyncComponentResolveResult +> + +export interface AsyncComponentOptions { + loader: AsyncComponentLoader + loadingComponent?: Component + errorComponent?: Component + delay?: number + timeout?: number + // suspensible?: boolean + onError?: ( + error: Error, + retry: () => void, + fail: () => void, + attempts: number + ) => any +} + +export function defineAsyncComponent( + source: AsyncComponentLoader | AsyncComponentOptions +): AsyncComponent diff --git a/types/v3-define-component.d.ts b/types/v3-define-component.d.ts index d6e73914ff0..f2d3ab1684c 100644 --- a/types/v3-define-component.d.ts +++ b/types/v3-define-component.d.ts @@ -1,4 +1,3 @@ -import { Component } from '..' import { ComponentPropsOptions, ExtractDefaultPropTypes, From 559600f13d312915c0a1b54ed4edd41327dbedd6 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 8 Jul 2022 11:18:36 +0800 Subject: [PATCH 05/35] feat: support functional components in defineComponent close #12619 --- types/test/v3/define-component-test.tsx | 36 +++++++++++++++++++++++++ types/v3-define-component.d.ts | 25 +++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/types/test/v3/define-component-test.tsx b/types/test/v3/define-component-test.tsx index 9b01e3e45f4..1b0847a3795 100644 --- a/types/test/v3/define-component-test.tsx +++ b/types/test/v3/define-component-test.tsx @@ -1079,3 +1079,39 @@ export default { } }) } + +describe('functional w/ array props', () => { + const Foo = defineComponent({ + functional: true, + props: ['foo'], + render(h, ctx) { + ctx.props.foo + // @ts-expect-error + ctx.props.bar + } + }) + + ; + // @ts-expect-error + ; +}) + +describe('functional w/ object props', () => { + const Foo = defineComponent({ + functional: true, + props: { + foo: String + }, + render(h, ctx) { + ctx.props.foo + // @ts-expect-error + ctx.props.bar + } + }) + + ; + // @ts-expect-error + ; + // @ts-expect-error + ; +}) diff --git a/types/v3-define-component.d.ts b/types/v3-define-component.d.ts index f2d3ab1684c..30f7046e403 100644 --- a/types/v3-define-component.d.ts +++ b/types/v3-define-component.d.ts @@ -18,6 +18,7 @@ import { } from './v3-component-public-instance' import { Data, HasDefined } from './common' import { EmitsOptions } from './v3-setup-context' +import { CreateElement, RenderContext } from './umd' type DefineComponent< PropsOrPropOptions = {}, @@ -66,6 +67,30 @@ type DefineComponent< props: PropsOrPropOptions } +/** + * overload 0.0: functional component with array props + */ +export function defineComponent< + PropNames extends string, + Props = Readonly<{ [key in PropNames]?: any }> +>(options: { + functional: true + props?: PropNames[] + render?: (h: CreateElement, context: RenderContext) => any +}): DefineComponent + +/** + * overload 0.1: functional component with object props + */ +export function defineComponent< + PropsOptions extends ComponentPropsOptions = ComponentPropsOptions, + Props = ExtractPropTypes +>(options: { + functional: true + props?: PropsOptions + render?: (h: CreateElement, context: RenderContext) => any +}): DefineComponent + /** * overload 1: object format with no props */ From 012e10c9ca13fcbc9bf67bf2835883edcd4faace Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 8 Jul 2022 11:42:36 +0800 Subject: [PATCH 06/35] fix(build): fix mjs dual package hazard close #12626 --- dist/vue.runtime.mjs | 75 ++++++++++++++++++++++++++++++++++++++++++++ scripts/config.js | 7 ----- src/v3/index.ts | 4 +++ 3 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 dist/vue.runtime.mjs diff --git a/dist/vue.runtime.mjs b/dist/vue.runtime.mjs new file mode 100644 index 00000000000..3ac875cdab6 --- /dev/null +++ b/dist/vue.runtime.mjs @@ -0,0 +1,75 @@ +import Vue from './vue.runtime.common.js' +export default Vue + +// this should be kept in sync with src/v3/index.ts +export const { + version, + + // refs + ref, + shallowRef, + isRef, + toRef, + toRefs, + unref, + proxyRefs, + customRef, + triggerRef, + computed, + + // reactive + reactive, + isReactive, + isReadonly, + isShallow, + isProxy, + shallowReactive, + markRaw, + toRaw, + readonly, + shallowReadonly, + + // watch + watch, + watchEffect, + watchPostEffect, + watchSyncEffect, + + // effectScope + effectScope, + onScopeDispose, + getCurrentScope, + + // provide / inject + provide, + inject, + + // lifecycle + onBeforeMount, + onMounted, + onBeforeUpdate, + onUpdated, + onUnmounted, + onErrorCaptured, + onActivated, + onDeactivated, + onServerPrefetch, + onRenderTracked, + onRenderTriggered, + + // v2 only + set, + del, + + // v3 compat + h, + getCurrentInstance, + useSlots, + useAttrs, + mergeDefaults, + nextTick, + useCssModule, + useCssVars, + defineComponent, + defineAsyncComponent +} = Vue diff --git a/scripts/config.js b/scripts/config.js index 851dc22971c..27a318499f9 100644 --- a/scripts/config.js +++ b/scripts/config.js @@ -71,13 +71,6 @@ const builds = { format: 'es', banner }, - // Runtime only ES modules build (for Node) - 'runtime-mjs': { - entry: resolve('web/entry-runtime-esm.ts'), - dest: resolve('dist/vue.runtime.mjs'), - format: 'es', - banner - }, // Runtime+compiler ES modules build (for bundlers) 'full-esm': { entry: resolve('web/entry-runtime-with-compiler-esm.ts'), diff --git a/src/v3/index.ts b/src/v3/index.ts index ec9d59e5444..30b89c3162e 100644 --- a/src/v3/index.ts +++ b/src/v3/index.ts @@ -1,3 +1,7 @@ +/** + * Note: also update dist/vue.runtime.mjs when adding new exports to this file. + */ + export const version: string = '__VERSION__' export { From 8904ca77c2d675707728e6a50decd3ef3370a428 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 8 Jul 2022 14:47:03 +0800 Subject: [PATCH 07/35] fix(watch): fix watchers triggered in mounted hook fix #12624 --- src/v3/apiWatch.ts | 4 ++-- test/unit/features/v3/apiWatch.spec.ts | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/v3/apiWatch.ts b/src/v3/apiWatch.ts index e1d7d018bf0..52be4d32e4c 100644 --- a/src/v3/apiWatch.ts +++ b/src/v3/apiWatch.ts @@ -320,8 +320,8 @@ function doWatch( } else { // pre watcher.update = () => { - if (instance && instance === currentInstance) { - // pre-watcher triggered inside setup() + if (instance && instance === currentInstance && !instance._isMounted) { + // pre-watcher triggered before const buffer = instance._preWatchers || (instance._preWatchers = []) if (buffer.indexOf(watcher) < 0) buffer.push(watcher) } else { diff --git a/test/unit/features/v3/apiWatch.spec.ts b/test/unit/features/v3/apiWatch.spec.ts index 82aba414ce0..41870ad1d51 100644 --- a/test/unit/features/v3/apiWatch.spec.ts +++ b/test/unit/features/v3/apiWatch.spec.ts @@ -1116,4 +1116,24 @@ describe('api: watch', () => { await nextTick() expect(order).toMatchObject([`mounted`, `watcher`]) }) + + // #12624 + test('pre watch triggered in mounted hook', async () => { + const spy = vi.fn() + new Vue({ + setup() { + const c = ref(0) + + onMounted(() => { + c.value++ + }) + + watchEffect(() => spy(c.value)) + return () => {} + } + }).$mount() + expect(spy).toHaveBeenCalledTimes(1) + await nextTick() + expect(spy).toHaveBeenCalledTimes(2) + }) }) From dc8a68e8c6c4e8ed4fdde094004fca272d71ef2e Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 8 Jul 2022 15:03:09 +0800 Subject: [PATCH 08/35] fix: pass element creation helper to static render fns for functional components fix #12625 --- src/core/instance/render-helpers/render-static.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/instance/render-helpers/render-static.ts b/src/core/instance/render-helpers/render-static.ts index 4c7fb0b7777..9de7ab7ec70 100644 --- a/src/core/instance/render-helpers/render-static.ts +++ b/src/core/instance/render-helpers/render-static.ts @@ -18,7 +18,7 @@ export function renderStatic( // otherwise, render a fresh tree. tree = cached[index] = this.$options.staticRenderFns[index].call( this._renderProxy, - null, + this._c, this // for render fns generated for functional component templates ) markStatic(tree, `__static__${index}`, false) From 1d5a411c1e3aa062aa5080432cf3f852f1583ed2 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 8 Jul 2022 15:15:22 +0800 Subject: [PATCH 09/35] fix(types): fix type inference when using components option --- types/options.d.ts | 6 +++--- types/test/v3/define-component-test.tsx | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/types/options.d.ts b/types/options.d.ts index f1db523a04a..736da7ade30 100644 --- a/types/options.d.ts +++ b/types/options.d.ts @@ -20,7 +20,7 @@ export type Component< | typeof Vue | FunctionalComponentOptions | ComponentOptions - | DefineComponent + | DefineComponent type EsModule = T | { default: T } @@ -201,9 +201,9 @@ export interface ComponentOptions< directives?: { [key: string]: DirectiveFunction | DirectiveOptions } components?: { [key: string]: - | Component + | {} + | Component | AsyncComponent - | DefineComponent } transitions?: { [key: string]: object } filters?: { [key: string]: Function } diff --git a/types/test/v3/define-component-test.tsx b/types/test/v3/define-component-test.tsx index 1b0847a3795..dc71b44e466 100644 --- a/types/test/v3/define-component-test.tsx +++ b/types/test/v3/define-component-test.tsx @@ -1115,3 +1115,26 @@ describe('functional w/ object props', () => { // @ts-expect-error ; }) + +// #12628 +defineComponent({ + components: { + App: defineComponent({}) + }, + data() { + return {} + }, + provide(): any { + return { + fetchData: this.fetchData + } + }, + created() { + this.fetchData() + }, + methods: { + fetchData() { + throw new Error('Not implemented.') + } + } +}) From d3add06e6e18a78a3745240632fecd076eb49d19 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 8 Jul 2022 15:17:22 +0800 Subject: [PATCH 10/35] fix(types): fix this.$slots type for defineComponent --- types/test/v3/define-component-test.tsx | 7 +++++++ types/v3-component-public-instance.d.ts | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/types/test/v3/define-component-test.tsx b/types/test/v3/define-component-test.tsx index dc71b44e466..9c9a25bea80 100644 --- a/types/test/v3/define-component-test.tsx +++ b/types/test/v3/define-component-test.tsx @@ -1138,3 +1138,10 @@ defineComponent({ } } }) + +// https://github.com/vuejs/vue/issues/12628#issuecomment-1177258223 +defineComponent({ + render(h) { + return h('div', {}, [...this.$slots.default!]) + } +}) diff --git a/types/v3-component-public-instance.d.ts b/types/v3-component-public-instance.d.ts index 6734c596ac1..585f66de37a 100644 --- a/types/v3-component-public-instance.d.ts +++ b/types/v3-component-public-instance.d.ts @@ -18,6 +18,7 @@ import { ComponentOptionsBase } from './v3-component-options' import { EmitFn, EmitsOptions, Slots } from './v3-setup-context' +import { VNode } from './vnode' /** * Custom properties added to component instances in any way and can be accessed through `this` @@ -162,7 +163,8 @@ export type ComponentPublicInstance< > $attrs: Data $refs: Data - $slots: Slots + $slots: Record + $scopedSlots: Slots $root: ComponentPublicInstance | null $parent: ComponentPublicInstance | null $emit: EmitFn From f8de4ca9d458a03378e848b1e62d6507f7124871 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 8 Jul 2022 15:39:16 +0800 Subject: [PATCH 11/35] fix(types): fix missing instance properties on defineComponent this ref https://github.com/vuejs/vue/issues/12628#issuecomment-1177258223 --- types/test/v3/define-component-test.tsx | 5 ++ types/v3-component-public-instance.d.ts | 66 +++++++++++++------------ types/vue.d.ts | 22 +++++---- 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/types/test/v3/define-component-test.tsx b/types/test/v3/define-component-test.tsx index 9c9a25bea80..b982a805787 100644 --- a/types/test/v3/define-component-test.tsx +++ b/types/test/v3/define-component-test.tsx @@ -1139,9 +1139,14 @@ defineComponent({ } }) +// Missing / mismatching Vue 2 properties // https://github.com/vuejs/vue/issues/12628#issuecomment-1177258223 defineComponent({ render(h) { + this.$listeners + this.$on('foo', () => {}) + this.$ssrContext + this.$isServer return h('div', {}, [...this.$slots.default!]) } }) diff --git a/types/v3-component-public-instance.d.ts b/types/v3-component-public-instance.d.ts index 585f66de37a..6c2770f8cf9 100644 --- a/types/v3-component-public-instance.d.ts +++ b/types/v3-component-public-instance.d.ts @@ -1,15 +1,11 @@ -import { ExtractDefaultPropTypes, ExtractPropTypes } from './v3-component-props' import { DebuggerEvent, - nextTick, ShallowUnwrapRef, - UnwrapNestedRefs, - WatchOptions, - WatchStopHandle + UnwrapNestedRefs } from './v3-generated' -import { Data, UnionToIntersection } from './common' +import { UnionToIntersection } from './common' -import { VueConstructor } from './vue' +import { Vue, Vue2Instance, VueConstructor } from './vue' import { ComputedOptions, MethodOptions, @@ -153,37 +149,43 @@ export type ComponentPublicInstance< any, any > -> = { - // $: ComponentInternalInstance - $data: D - $props: Readonly< - MakeDefaultsOptional extends true - ? Partial & Omit

- : P & PublicProps - > - $attrs: Data - $refs: Data - $slots: Record - $scopedSlots: Slots - $root: ComponentPublicInstance | null - $parent: ComponentPublicInstance | null - $emit: EmitFn - $el: any - $options: Options & MergedComponentOptionsOverride - $forceUpdate: () => void - $nextTick: typeof nextTick - $watch( - source: string | Function, - cb: Function, - options?: WatchOptions - ): WatchStopHandle -} & Readonly

& +> = Vue3Instance< + D, + P, + PublicProps, + E, + Defaults, + MakeDefaultsOptional, + Options +> & + Readonly

& ShallowUnwrapRef & UnwrapNestedRefs & ExtractComputedReturns & M & ComponentCustomProperties +interface Vue3Instance< + D, + P, + PublicProps, + E, + Defaults, + MakeDefaultsOptional, + Options +> extends Vue2Instance { + $data: D + readonly $props: Readonly< + MakeDefaultsOptional extends true + ? Partial & Omit

+ : P & PublicProps + > + readonly $root: ComponentPublicInstance | null + readonly $parent: ComponentPublicInstance | null + readonly $emit: EmitFn + readonly $options: Options & MergedComponentOptionsOverride +} + type MergedHook void> = T | T[] export type MergedComponentOptionsOverride = { diff --git a/types/vue.d.ts b/types/vue.d.ts index 82cad69e793..ccc7702ace6 100644 --- a/types/vue.d.ts +++ b/types/vue.d.ts @@ -3,8 +3,6 @@ import { AsyncComponent, ComponentOptions, FunctionalComponentOptions, - WatchOptionsWithHandler, - WatchHandler, DirectiveOptions, DirectiveFunction, RecordPropsDefinition, @@ -15,6 +13,7 @@ import { import { VNode, VNodeData, VNodeChildren, NormalizedScopedSlot } from './vnode' import { PluginFunction, PluginObject } from './plugin' import { DefineComponent } from './v3-define-component' +import { nextTick } from './v3-generated' export interface CreateElement { ( @@ -36,20 +35,25 @@ export interface CreateElement { ): VNode } -export interface Vue { - readonly $el: Element - readonly $options: ComponentOptions +export interface Vue extends Vue2Instance { + readonly $data: Record + readonly $props: Record readonly $parent: Vue readonly $root: Vue readonly $children: Vue[] + readonly $options: ComponentOptions + $emit(event: string, ...args: any[]): this +} + +export interface Vue2Instance { + readonly $el: Element readonly $refs: { [key: string]: Vue | Element | (Vue | Element)[] | undefined } readonly $slots: { [key: string]: VNode[] | undefined } readonly $scopedSlots: { [key: string]: NormalizedScopedSlot | undefined } readonly $isServer: boolean - readonly $data: Record - readonly $props: Record + readonly $ssrContext: any readonly $vnode: VNode readonly $attrs: Record @@ -73,9 +77,7 @@ export interface Vue { $on(event: string | string[], callback: Function): this $once(event: string | string[], callback: Function): this $off(event?: string | string[], callback?: Function): this - $emit(event: string, ...args: any[]): this - $nextTick(callback: (this: this) => void): void - $nextTick(): Promise + $nextTick: typeof nextTick $createElement: CreateElement } From 018d29af5fa171b2d2f4793c5d471f21bb7cd1e7 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 8 Jul 2022 15:46:21 +0800 Subject: [PATCH 12/35] release: v2.7.4 --- CHANGELOG.md | 23 +++++++++++++++++++++++ package.json | 2 +- packages/compiler-sfc/package.json | 2 +- packages/server-renderer/package.json | 2 +- packages/template-compiler/package.json | 2 +- 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cfac15899c..e97c0c5540c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +## [2.7.4](https://github.com/vuejs/vue/compare/v2.7.3...v2.7.4) (2022-07-08) + + +### Bug Fixes + +* **build:** fix mjs dual package hazard ([012e10c](https://github.com/vuejs/vue/commit/012e10c9ca13fcbc9bf67bf2835883edcd4faace)), closes [#12626](https://github.com/vuejs/vue/issues/12626) +* **compiler-sfc:** use safer deindent default for compatibility with previous behavior ([b70a258](https://github.com/vuejs/vue/commit/b70a2585fcd102def2bb5a3b2b589edf5311122d)) +* pass element creation helper to static render fns for functional components ([dc8a68e](https://github.com/vuejs/vue/commit/dc8a68e8c6c4e8ed4fdde094004fca272d71ef2e)), closes [#12625](https://github.com/vuejs/vue/issues/12625) +* **ssr/reactivity:** fix array setting error at created in ssr [[#12632](https://github.com/vuejs/vue/issues/12632)] ([#12633](https://github.com/vuejs/vue/issues/12633)) ([ca7daef](https://github.com/vuejs/vue/commit/ca7daefaa15a192046d22d060220cd595a6a275f)) +* **types:** fix missing instance properties on defineComponent this ([f8de4ca](https://github.com/vuejs/vue/commit/f8de4ca9d458a03378e848b1e62d6507f7124871)), closes [/github.com/vuejs/vue/issues/12628#issuecomment-1177258223](https://github.com//github.com/vuejs/vue/issues/12628/issues/issuecomment-1177258223) +* **types:** fix this.$slots type for defineComponent ([d3add06](https://github.com/vuejs/vue/commit/d3add06e6e18a78a3745240632fecd076eb49d19)) +* **types:** fix type inference when using components option ([1d5a411](https://github.com/vuejs/vue/commit/1d5a411c1e3aa062aa5080432cf3f852f1583ed2)) +* **types:** global component registration type compat w/ defineComponent ([26ff4bc](https://github.com/vuejs/vue/commit/26ff4bc0ed75d8bf7921523a2e546df24ec81d8f)), closes [#12622](https://github.com/vuejs/vue/issues/12622) +* **watch:** fix watchers triggered in mounted hook ([8904ca7](https://github.com/vuejs/vue/commit/8904ca77c2d675707728e6a50decd3ef3370a428)), closes [#12624](https://github.com/vuejs/vue/issues/12624) + + +### Features + +* defineAsyncComponent ([9d12106](https://github.com/vuejs/vue/commit/9d12106e211e0cbf33f9066606a8ff29f8cc8e8d)), closes [#12608](https://github.com/vuejs/vue/issues/12608) +* support functional components in defineComponent ([559600f](https://github.com/vuejs/vue/commit/559600f13d312915c0a1b54ed4edd41327dbedd6)), closes [#12619](https://github.com/vuejs/vue/issues/12619) + + + ## [2.7.3](https://github.com/vuejs/vue/compare/v2.7.2...v2.7.3) (2022-07-06) diff --git a/package.json b/package.json index 29c5ec3b674..10d852d4db2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue", - "version": "2.7.3", + "version": "2.7.4", "packageManager": "pnpm@7.1.0", "description": "Reactive, component-oriented view layer for modern web interfaces.", "main": "dist/vue.runtime.common.js", diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json index 2cf6872ed9d..eb5a5967197 100644 --- a/packages/compiler-sfc/package.json +++ b/packages/compiler-sfc/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-sfc", - "version": "2.7.3", + "version": "2.7.4", "description": "compiler-sfc for Vue 2", "main": "dist/compiler-sfc.js", "types": "dist/compiler-sfc.d.ts", diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json index e791637f9c7..a296a8bfc32 100644 --- a/packages/server-renderer/package.json +++ b/packages/server-renderer/package.json @@ -1,6 +1,6 @@ { "name": "vue-server-renderer", - "version": "2.7.3", + "version": "2.7.4", "description": "server renderer for Vue 2.0", "main": "index.js", "types": "types/index.d.ts", diff --git a/packages/template-compiler/package.json b/packages/template-compiler/package.json index 5d2a22ad2d4..2f8e92fa83b 100644 --- a/packages/template-compiler/package.json +++ b/packages/template-compiler/package.json @@ -1,6 +1,6 @@ { "name": "vue-template-compiler", - "version": "2.7.3", + "version": "2.7.4", "description": "template compiler for Vue 2.0", "main": "index.js", "unpkg": "browser.js", From 04b4703de72b1c1e686a3aa81d5b5b56799dabab Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 12 Jul 2022 17:39:45 +0800 Subject: [PATCH 13/35] fix(sfc): fix sfc name inference type check fix #12637 --- src/core/components/keep-alive.ts | 9 +++++---- src/core/global-api/extend.ts | 4 +++- src/core/util/debug.ts | 3 ++- src/core/vdom/create-component.ts | 6 +++++- src/platforms/web/runtime/components/transition-group.ts | 3 ++- types/options.d.ts | 2 ++ 6 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/core/components/keep-alive.ts b/src/core/components/keep-alive.ts index c2676a35574..2bb2206be37 100644 --- a/src/core/components/keep-alive.ts +++ b/src/core/components/keep-alive.ts @@ -3,6 +3,7 @@ import { getFirstComponentChild } from 'core/vdom/helpers/index' import type VNode from 'core/vdom/vnode' import type { VNodeComponentOptions } from 'types/vnode' import type { Component } from 'types/component' +import { getComponentName } from '../vdom/create-component' type CacheEntry = { name?: string @@ -12,8 +13,8 @@ type CacheEntry = { type CacheEntryMap = Record -function getComponentName(opts?: VNodeComponentOptions): string | null { - return opts && (opts.Ctor.options.name || opts.tag) +function _getComponentName(opts?: VNodeComponentOptions): string | null { + return opts && (getComponentName(opts.Ctor.options as any) || opts.tag) } function matches( @@ -81,7 +82,7 @@ export default { if (vnodeToCache) { const { tag, componentInstance, componentOptions } = vnodeToCache cache[keyToCache] = { - name: getComponentName(componentOptions), + name: _getComponentName(componentOptions), tag, componentInstance } @@ -126,7 +127,7 @@ export default { const componentOptions = vnode && vnode.componentOptions if (componentOptions) { // check pattern - const name = getComponentName(componentOptions) + const name = _getComponentName(componentOptions) const { include, exclude } = this if ( // not included diff --git a/src/core/global-api/extend.ts b/src/core/global-api/extend.ts index 5fed6fe90a9..800e8c2329e 100644 --- a/src/core/global-api/extend.ts +++ b/src/core/global-api/extend.ts @@ -3,6 +3,7 @@ import type { Component } from 'types/component' import type { GlobalAPI } from 'types/global-api' import { defineComputed, proxy } from '../instance/state' import { extend, mergeOptions, validateComponentName } from '../util/index' +import { getComponentName } from '../vdom/create-component' export function initExtend(Vue: GlobalAPI) { /** @@ -25,7 +26,8 @@ export function initExtend(Vue: GlobalAPI) { return cachedCtors[SuperId] } - const name = extendOptions.name || Super.options.name + const name = + getComponentName(extendOptions) || getComponentName(Super.options) if (__DEV__ && name) { validateComponentName(name) } diff --git a/src/core/util/debug.ts b/src/core/util/debug.ts index 8dbc1edb19a..891f0177c68 100644 --- a/src/core/util/debug.ts +++ b/src/core/util/debug.ts @@ -2,6 +2,7 @@ import config from '../config' import { noop, isArray, isFunction } from 'shared/util' import type { Component } from 'types/component' import { currentInstance } from 'v3/currentInstance' +import { getComponentName } from '../vdom/create-component' export let warn: (msg: string, vm?: Component | null) => void = noop export let tip = noop @@ -40,7 +41,7 @@ if (__DEV__) { : vm._isVue ? vm.$options || (vm.constructor as any).options : vm - let name = options.name || options._componentTag + let name = getComponentName(options) const file = options.__file if (!name && file) { const match = file.match(/([^/\\]+)\.vue$/) diff --git a/src/core/vdom/create-component.ts b/src/core/vdom/create-component.ts index 7ec6d82038b..9e48c575230 100644 --- a/src/core/vdom/create-component.ts +++ b/src/core/vdom/create-component.ts @@ -28,6 +28,10 @@ import type { import type { Component } from 'types/component' import type { ComponentOptions, InternalComponentOptions } from 'types/options' +export function getComponentName(options: ComponentOptions) { + return options.name || options.__name || options._componentTag +} + // inline hooks to be invoked on component VNodes during patch const componentVNodeHooks = { init(vnode: VNodeWithData, hydrating: boolean): boolean | void { @@ -188,7 +192,7 @@ export function createComponent( // return a placeholder vnode // @ts-expect-error - const name = Ctor.options.name || tag + const name = getComponentName(Ctor.options) || tag const vnode = new VNode( // @ts-expect-error `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, diff --git a/src/platforms/web/runtime/components/transition-group.ts b/src/platforms/web/runtime/components/transition-group.ts index d7a454d3e19..8588ea80561 100644 --- a/src/platforms/web/runtime/components/transition-group.ts +++ b/src/platforms/web/runtime/components/transition-group.ts @@ -23,6 +23,7 @@ import { } from 'web/runtime/transition-util' import VNode from 'core/vdom/vnode' import { VNodeWithData } from 'types/vnode' +import { getComponentName } from 'core/vdom/create-component' const props = extend( { @@ -72,7 +73,7 @@ export default { } else if (__DEV__) { const opts = c.componentOptions const name: string = opts - ? opts.Ctor.options.name || opts.tag || '' + ? getComponentName(opts.Ctor.options as any) || opts.tag || '' : c.tag warn(` children must be keyed: <${name}>`) } diff --git a/types/options.d.ts b/types/options.d.ts index 736da7ade30..e11020f4948 100644 --- a/types/options.d.ts +++ b/types/options.d.ts @@ -219,6 +219,8 @@ export interface ComponentOptions< parent?: Vue mixins?: (ComponentOptions | typeof Vue)[] name?: string + // for SFC auto name inference w/ ts-loader check + __name?: string // TODO: support properly inferred 'extends' extends?: ComponentOptions | typeof Vue delimiters?: [string, string] From 0825d3087f9f39435838329c16adc2a7bfccd51d Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 12 Jul 2022 18:19:19 +0800 Subject: [PATCH 14/35] fix: do not set currentInstance in beforeCreate fix #12636 --- src/core/instance/init.ts | 2 +- src/core/instance/lifecycle.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/core/instance/init.ts b/src/core/instance/init.ts index 564dbf9fbbb..876c9ddbf96 100644 --- a/src/core/instance/init.ts +++ b/src/core/instance/init.ts @@ -58,7 +58,7 @@ export function initMixin(Vue: typeof Component) { initLifecycle(vm) initEvents(vm) initRender(vm) - callHook(vm, 'beforeCreate') + callHook(vm, 'beforeCreate', undefined, false /* setContext */) initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props diff --git a/src/core/instance/lifecycle.ts b/src/core/instance/lifecycle.ts index 171c4e5d9a5..0d1f0732087 100644 --- a/src/core/instance/lifecycle.ts +++ b/src/core/instance/lifecycle.ts @@ -375,11 +375,16 @@ export function deactivateChildComponent(vm: Component, direct?: boolean) { } } -export function callHook(vm: Component, hook: string, args?: any[]) { +export function callHook( + vm: Component, + hook: string, + args?: any[], + setContext = true +) { // #7573 disable dep collection when invoking lifecycle hooks pushTarget() const prev = currentInstance - setCurrentInstance(vm) + setContext && setCurrentInstance(vm) const handlers = vm.$options[hook] const info = `${hook} hook` if (handlers) { @@ -390,6 +395,6 @@ export function callHook(vm: Component, hook: string, args?: any[]) { if (vm._hasHookEvent) { vm.$emit('hook:' + hook) } - setCurrentInstance(prev) + setContext && setCurrentInstance(prev) popTarget() } From 98fb01c79c41c3b9f9134f0abb77d233ce4e5b44 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 12 Jul 2022 18:50:01 +0800 Subject: [PATCH 15/35] fix(reactivity): fix watch behavior inconsistency + deep ref shallow check fix #12643 --- src/v3/apiWatch.ts | 10 ++++------ src/v3/reactivity/ref.ts | 2 +- test/unit/features/v3/apiWatch.spec.ts | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/v3/apiWatch.ts b/src/v3/apiWatch.ts index 52be4d32e4c..6a0b65fa298 100644 --- a/src/v3/apiWatch.ts +++ b/src/v3/apiWatch.ts @@ -196,12 +196,10 @@ function doWatch( getter = () => source.value forceTrigger = isShallow(source) } else if (isReactive(source)) { - getter = isArray(source) - ? () => { - ;(source as any).__ob__.dep.depend() - return source - } - : () => source + getter = () => { + ;(source as any).__ob__.dep.depend() + return source + } deep = true } else if (isArray(source)) { isMultiSource = true diff --git a/src/v3/reactivity/ref.ts b/src/v3/reactivity/ref.ts index f7026df05a1..114e6f563d8 100644 --- a/src/v3/reactivity/ref.ts +++ b/src/v3/reactivity/ref.ts @@ -68,7 +68,7 @@ function createRef(rawValue: unknown, shallow: boolean) { } const ref: any = {} def(ref, RefFlag, true) - def(ref, ReactiveFlags.IS_SHALLOW, true) + def(ref, ReactiveFlags.IS_SHALLOW, shallow) def( ref, 'dep', diff --git a/test/unit/features/v3/apiWatch.spec.ts b/test/unit/features/v3/apiWatch.spec.ts index 41870ad1d51..52025afa96a 100644 --- a/test/unit/features/v3/apiWatch.spec.ts +++ b/test/unit/features/v3/apiWatch.spec.ts @@ -1136,4 +1136,21 @@ describe('api: watch', () => { await nextTick() expect(spy).toHaveBeenCalledTimes(2) }) + + // #12643 + test('should trigger watch on reactive object when new property is added via set()', () => { + const spy = vi.fn() + const obj = reactive({}) + watch(obj, spy, { flush: 'sync' }) + set(obj, 'foo', 1) + expect(spy).toHaveBeenCalled() + }) + + test('should not trigger watch when calling set() on ref value', () => { + const spy = vi.fn() + const r = ref({}) + watch(r, spy, { flush: 'sync' }) + set(r.value, 'foo', 1) + expect(spy).not.toHaveBeenCalled() + }) }) From a6e74985cf2eab6f16d03a8eda3bf3fc7950127c Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 12 Jul 2022 18:59:26 +0800 Subject: [PATCH 16/35] fix: detect property add/deletion on reactive objects from setup when used in templates --- src/v3/reactivity/ref.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/v3/reactivity/ref.ts b/src/v3/reactivity/ref.ts index 114e6f563d8..244acc9b0d4 100644 --- a/src/v3/reactivity/ref.ts +++ b/src/v3/reactivity/ref.ts @@ -119,7 +119,16 @@ export function proxyWithRefUnwrap( Object.defineProperty(target, key, { enumerable: true, configurable: true, - get: () => unref(source[key]), + get: () => { + const val = source[key] + if (isRef(val)) { + return val.value + } else { + const ob = val && val.__ob__ + if (ob) ob.dep.depend() + return val + } + }, set: value => { const oldValue = source[key] if (isRef(oldValue) && !isRef(value)) { From 0198f9f9eea728ceb45af92eb2952fa97c9abfe1 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 13 Jul 2022 09:31:39 +0800 Subject: [PATCH 17/35] chore: document TS changes in changelog [ci skip] --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e97c0c5540c..da649da33f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,6 +117,12 @@ In addition, the following features are explicitly **NOT** ported: - ❌ Reactivity transform (still experimental) - ❌ `expose` option is not supported for options components (but `defineExpose()` is supported in `