diff --git a/packages/reactivity/src/watch.ts b/packages/reactivity/src/watch.ts
index a82830a39ca..07fbbcd5117 100644
--- a/packages/reactivity/src/watch.ts
+++ b/packages/reactivity/src/watch.ts
@@ -189,6 +189,10 @@ export class WatcherEffect extends ReactiveEffect {
this.forceTrigger = forceTrigger
this.isMultiSource = isMultiSource
+ if (__COMPAT__ && (options as any).compatWatchArray) {
+ ;(this as any).compatWatchArray = true
+ }
+
if (once && cb) {
const _cb = cb
cb = (...args) => {
@@ -224,7 +228,8 @@ export class WatcherEffect extends ReactiveEffect {
this.forceTrigger ||
(this.isMultiSource
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
- : hasChanged(newValue, oldValue))
+ : hasChanged(newValue, oldValue)) ||
+ (__COMPAT__ && (this as any).compatWatchArray && isArray(newValue))
) {
// cleanup before running cb again
cleanup(this)
diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts
index f542acc22aa..41e05169985 100644
--- a/packages/runtime-core/src/apiWatch.ts
+++ b/packages/runtime-core/src/apiWatch.ts
@@ -8,9 +8,17 @@ import {
type WatchHandle,
type WatchSource,
WatcherEffect,
+ traverse,
} from '@vue/reactivity'
import { type SchedulerJob, SchedulerJobFlags, queueJob } from './scheduler'
-import { EMPTY_OBJ, NOOP, extend, isFunction, isString } from '@vue/shared'
+import {
+ EMPTY_OBJ,
+ NOOP,
+ extend,
+ isArray,
+ isFunction,
+ isString,
+} from '@vue/shared'
import {
type ComponentInternalInstance,
type GenericComponentInstance,
@@ -21,6 +29,11 @@ import {
import { callWithAsyncErrorHandling } from './errorHandling'
import { queuePostRenderEffect } from './renderer'
import { warn } from './warning'
+import {
+ DeprecationTypes,
+ checkCompatEnabled,
+ isCompatEnabled,
+} from './compat/compatConfig'
import type { ObjectWatchOptionItem } from './componentOptions'
import { useSSRContext } from './helpers/useSsrContext'
import type { ComponentPublicInstance } from './componentPublicInstance'
@@ -275,6 +288,22 @@ function doWatch(
return stop
}
+export function createCompatWatchGetter(
+ baseGetter: () => any,
+ instance: ComponentInternalInstance,
+) {
+ return (): any => {
+ const val = baseGetter()
+ if (
+ isArray(val) &&
+ checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
+ ) {
+ traverse(val, 1)
+ }
+ return val
+ }
+}
+
// this.$watch
export function instanceWatch(
this: ComponentInternalInstance,
@@ -283,7 +312,7 @@ export function instanceWatch(
options?: WatchOptions,
): WatchHandle {
const publicThis = this.proxy
- const getter = isString(source)
+ let getter = isString(source)
? source.includes('.')
? createPathGetter(publicThis!, source)
: () => publicThis![source as keyof typeof publicThis]
@@ -295,6 +324,19 @@ export function instanceWatch(
cb = value.handler as Function
options = value
}
+
+ if (
+ __COMPAT__ &&
+ isString(source) &&
+ isCompatEnabled(DeprecationTypes.WATCH_ARRAY, this)
+ ) {
+ const deep = options && options.deep
+ if (!deep) {
+ options = extend({ compatWatchArray: true }, options)
+ getter = createCompatWatchGetter(getter, this)
+ }
+ }
+
const prev = setCurrentInstance(this)
const res = doWatch(getter, cb.bind(publicThis), options)
setCurrentInstance(...prev)
diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts
index eeb527a780f..ea3f96c964c 100644
--- a/packages/runtime-core/src/componentOptions.ts
+++ b/packages/runtime-core/src/componentOptions.ts
@@ -1,12 +1,11 @@
-import {
- type AsyncComponentInternalOptions,
- type Component,
- type ComponentInternalInstance,
- type ComponentInternalOptions,
- type Data,
- type InternalRenderFunction,
- type SetupContext,
- getCurrentInstance,
+import type {
+ AsyncComponentInternalOptions,
+ Component,
+ ComponentInternalInstance,
+ ComponentInternalOptions,
+ Data,
+ InternalRenderFunction,
+ SetupContext,
} from './component'
import {
type LooseRequired,
@@ -19,11 +18,12 @@ import {
isPromise,
isString,
} from '@vue/shared'
-import { type Ref, getCurrentScope, isRef, traverse } from '@vue/reactivity'
+import { type Ref, isRef } from '@vue/reactivity'
import { computed } from './apiComputed'
import {
type WatchCallback,
type WatchOptions,
+ createCompatWatchGetter,
createPathGetter,
watch,
} from './apiWatch'
@@ -72,9 +72,9 @@ import { warn } from './warning'
import type { VNodeChild } from './vnode'
import { callWithAsyncErrorHandling } from './errorHandling'
import { deepMergeData } from './compat/data'
-import { DeprecationTypes, checkCompatEnabled } from './compat/compatConfig'
import {
type CompatConfig,
+ DeprecationTypes,
isCompatEnabled,
softAssertCompatEnabled,
} from './compat/compatConfig'
@@ -825,7 +825,7 @@ function callHook(
)
}
-export function createWatcher(
+function createWatcher(
raw: ComponentWatchOptionItem,
ctx: Data,
publicThis: ComponentPublicInstance,
@@ -836,28 +836,14 @@ export function createWatcher(
: () => publicThis[key as keyof typeof publicThis]
const options: WatchOptions = {}
- if (__COMPAT__) {
- const cur = getCurrentInstance()
- const instance = cur && getCurrentScope() === cur.scope ? cur : null
-
- const newValue = getter()
- if (
- isArray(newValue) &&
- isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
- ) {
- options.deep = true
- }
-
- const baseGetter = getter
- getter = () => {
- const val = baseGetter()
- if (
- isArray(val) &&
- checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
- ) {
- traverse(val)
- }
- return val
+ if (
+ __COMPAT__ &&
+ isCompatEnabled(DeprecationTypes.WATCH_ARRAY, publicThis.$)
+ ) {
+ const deep = isObject(raw) && !isArray(raw) && !isFunction(raw) && raw.deep
+ if (!deep) {
+ ;(options as any).compatWatchArray = true
+ getter = createCompatWatchGetter(getter, publicThis.$)
}
}
diff --git a/packages/vue-compat/__tests__/misc.spec.ts b/packages/vue-compat/__tests__/misc.spec.ts
index 17fddd94c4a..c51dd682694 100644
--- a/packages/vue-compat/__tests__/misc.spec.ts
+++ b/packages/vue-compat/__tests__/misc.spec.ts
@@ -47,26 +47,377 @@ test('mode as function', () => {
expect(vm.$el.innerHTML).toBe(`
foo
bar
`)
})
-test('WATCH_ARRAY', async () => {
- const spy = vi.fn()
- const vm = new Vue({
- data() {
- return {
- foo: [],
- }
- },
- watch: {
- foo: spy,
- },
- }) as any
- expect(
- deprecationData[DeprecationTypes.WATCH_ARRAY].message,
- ).toHaveBeenWarned()
+describe('WATCH_ARRAY', () => {
+ describe('watch option', () => {
+ test('basic usage', async () => {
+ const spy = vi.fn()
+ const vm = new Vue({
+ data() {
+ return {
+ foo: [],
+ }
+ },
+ watch: {
+ foo: spy,
+ },
+ }) as any
+ expect(
+ deprecationData[DeprecationTypes.WATCH_ARRAY].message,
+ ).toHaveBeenWarned()
- expect(spy).not.toHaveBeenCalled()
- vm.foo.push(1)
- await nextTick()
- expect(spy).toHaveBeenCalledTimes(1)
+ expect(spy).not.toHaveBeenCalled()
+ vm.foo.push(1)
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(1)
+ })
+
+ test('dynamic depth depending on the value', async () => {
+ const spy = vi.fn()
+ const vm = new Vue({
+ data() {
+ return {
+ foo: {},
+ }
+ },
+ watch: {
+ foo: spy,
+ },
+ }) as any
+
+ vm.foo.bar = 1
+ await nextTick()
+ expect(
+ deprecationData[DeprecationTypes.WATCH_ARRAY].message,
+ ).not.toHaveBeenWarned()
+ expect(spy).not.toHaveBeenCalled()
+
+ vm.foo = []
+ await nextTick()
+ expect(
+ deprecationData[DeprecationTypes.WATCH_ARRAY].message,
+ ).toHaveBeenWarned()
+ expect(spy).toHaveBeenCalledTimes(1)
+
+ vm.foo.push({})
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(2)
+
+ vm.foo[0].bar = 2
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(2)
+
+ vm.foo = {}
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(3)
+
+ vm.foo.bar = 3
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(3)
+ })
+
+ test('deep: true', async () => {
+ const spy = vi.fn()
+ const vm = new Vue({
+ data() {
+ return {
+ foo: {},
+ }
+ },
+ watch: {
+ foo: {
+ handler: spy,
+ deep: true,
+ },
+ },
+ }) as any
+
+ vm.foo.bar = 1
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(1)
+
+ vm.foo = []
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(2)
+
+ vm.foo.push({})
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(3)
+
+ vm.foo[0].bar = 2
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(4)
+
+ vm.foo = {}
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(5)
+
+ vm.foo.bar = 3
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(6)
+
+ expect(
+ deprecationData[DeprecationTypes.WATCH_ARRAY].message,
+ ).not.toHaveBeenWarned()
+ })
+
+ test('checks correct instance for compat config', async () => {
+ const spy = vi.fn()
+ const vm = new Vue({
+ compatConfig: {
+ WATCH_ARRAY: false,
+ },
+ data() {
+ return {
+ foo: [],
+ }
+ },
+ watch: {
+ foo: spy,
+ },
+ }) as any
+
+ vm.foo.push(1)
+ await nextTick()
+ expect(spy).not.toHaveBeenCalled()
+
+ const orig = vm.foo
+ vm.foo = []
+ vm.foo = orig
+ await nextTick()
+ expect(spy).not.toHaveBeenCalled()
+
+ vm.foo = []
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(1)
+
+ expect(
+ deprecationData[DeprecationTypes.WATCH_ARRAY].message,
+ ).not.toHaveBeenWarned()
+ })
+
+ test('passing other options', async () => {
+ const spy = vi.fn()
+ const vm = new Vue({
+ data() {
+ return {
+ foo: [],
+ }
+ },
+ watch: {
+ foo: {
+ handler: spy,
+ immediate: true,
+ },
+ },
+ }) as any
+
+ expect(spy).toHaveBeenCalledTimes(1)
+ expect(
+ deprecationData[DeprecationTypes.WATCH_ARRAY].message,
+ ).toHaveBeenWarned()
+
+ vm.foo.push({})
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(2)
+
+ vm.foo[0].bar = 1
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(2)
+ })
+ })
+
+ describe('$watch()', () => {
+ test('basic usage', async () => {
+ const spy = vi.fn()
+ const vm = new Vue({
+ data() {
+ return {
+ foo: [],
+ }
+ },
+ }) as any
+ vm.$watch('foo', spy)
+ expect(
+ deprecationData[DeprecationTypes.WATCH_ARRAY].message,
+ ).toHaveBeenWarned()
+
+ expect(spy).not.toHaveBeenCalled()
+ vm.foo.push(1)
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(1)
+ })
+
+ test('dynamic depth depending on the value', async () => {
+ const spy = vi.fn()
+ const vm = new Vue({
+ data() {
+ return {
+ foo: {},
+ }
+ },
+ }) as any
+ vm.$watch('foo', spy)
+
+ vm.foo.bar = 1
+ await nextTick()
+ expect(
+ deprecationData[DeprecationTypes.WATCH_ARRAY].message,
+ ).not.toHaveBeenWarned()
+ expect(spy).not.toHaveBeenCalled()
+
+ vm.foo = []
+ await nextTick()
+ expect(
+ deprecationData[DeprecationTypes.WATCH_ARRAY].message,
+ ).toHaveBeenWarned()
+ expect(spy).toHaveBeenCalledTimes(1)
+
+ vm.foo.push({})
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(2)
+
+ vm.foo[0].bar = 2
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(2)
+
+ vm.foo = {}
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(3)
+
+ vm.foo.bar = 3
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(3)
+ })
+
+ test('deep: true', async () => {
+ const spy = vi.fn()
+ const vm = new Vue({
+ data() {
+ return {
+ foo: {},
+ }
+ },
+ }) as any
+ vm.$watch('foo', spy, { deep: true })
+
+ vm.foo.bar = 1
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(1)
+
+ vm.foo = []
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(2)
+
+ vm.foo.push({})
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(3)
+
+ vm.foo[0].bar = 2
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(4)
+
+ vm.foo = {}
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(5)
+
+ vm.foo.bar = 3
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(6)
+
+ expect(
+ deprecationData[DeprecationTypes.WATCH_ARRAY].message,
+ ).not.toHaveBeenWarned()
+ })
+
+ test('not deep for a function getter', async () => {
+ const spy = vi.fn()
+ const vm = new Vue({
+ data() {
+ return {
+ foo: [],
+ }
+ },
+ }) as any
+ vm.$watch(() => vm.foo, spy)
+
+ expect(spy).not.toHaveBeenCalled()
+ vm.foo.push(1)
+ await nextTick()
+ expect(spy).not.toHaveBeenCalled()
+ vm.foo = []
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(1)
+ vm.foo.push(1)
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(1)
+
+ expect(
+ deprecationData[DeprecationTypes.WATCH_ARRAY].message,
+ ).not.toHaveBeenWarned()
+ })
+
+ test('checks correct instance for compat config', async () => {
+ const spy = vi.fn()
+ const vm = new Vue({
+ compatConfig: {
+ WATCH_ARRAY: false,
+ },
+ data() {
+ return {
+ foo: [],
+ }
+ },
+ }) as any
+ vm.$watch('foo', spy)
+
+ vm.foo.push(1)
+ await nextTick()
+ expect(spy).not.toHaveBeenCalled()
+
+ const orig = vm.foo
+ vm.foo = []
+ vm.foo = orig
+ await nextTick()
+ expect(spy).not.toHaveBeenCalled()
+
+ vm.foo = []
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(1)
+
+ expect(
+ deprecationData[DeprecationTypes.WATCH_ARRAY].message,
+ ).not.toHaveBeenWarned()
+ })
+
+ test('passing other options', async () => {
+ const spy = vi.fn()
+ const vm = new Vue({
+ data() {
+ return {
+ foo: [],
+ }
+ },
+ }) as any
+
+ vm.$watch('foo', {
+ handler: spy,
+ immediate: true,
+ })
+
+ expect(spy).toHaveBeenCalledTimes(1)
+ expect(
+ deprecationData[DeprecationTypes.WATCH_ARRAY].message,
+ ).toHaveBeenWarned()
+
+ vm.foo.push({})
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(2)
+
+ vm.foo[0].bar = 1
+ await nextTick()
+ expect(spy).toHaveBeenCalledTimes(2)
+ })
+ })
})
test('PROPS_DEFAULT_THIS', () => {