This commit is contained in:
skirtle 2025-06-26 12:36:27 -07:00 committed by GitHub
commit b198df027e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 436 additions and 58 deletions

View File

@ -245,7 +245,8 @@ export function watch(
forceTrigger || forceTrigger ||
(isMultiSource (isMultiSource
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i])) ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
: hasChanged(newValue, oldValue)) : hasChanged(newValue, oldValue)) ||
(__COMPAT__ && (options as any).compatWatchArray && isArray(newValue))
) { ) {
// cleanup before running cb again // cleanup before running cb again
if (cleanup) { if (cleanup) {

View File

@ -7,9 +7,17 @@ import {
type WatchHandle, type WatchHandle,
type WatchSource, type WatchSource,
watch as baseWatch, watch as baseWatch,
traverse,
} from '@vue/reactivity' } from '@vue/reactivity'
import { type SchedulerJob, SchedulerJobFlags, queueJob } from './scheduler' 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 { import {
type ComponentInternalInstance, type ComponentInternalInstance,
currentInstance, currentInstance,
@ -19,6 +27,11 @@ import {
import { callWithAsyncErrorHandling } from './errorHandling' import { callWithAsyncErrorHandling } from './errorHandling'
import { queuePostRenderEffect } from './renderer' import { queuePostRenderEffect } from './renderer'
import { warn } from './warning' import { warn } from './warning'
import {
DeprecationTypes,
checkCompatEnabled,
isCompatEnabled,
} from './compat/compatConfig'
import type { ObjectWatchOptionItem } from './componentOptions' import type { ObjectWatchOptionItem } from './componentOptions'
import { useSSRContext } from './helpers/useSsrContext' import { useSSRContext } from './helpers/useSsrContext'
@ -236,6 +249,22 @@ function doWatch(
return watchHandle return watchHandle
} }
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 // this.$watch
export function instanceWatch( export function instanceWatch(
this: ComponentInternalInstance, this: ComponentInternalInstance,
@ -244,7 +273,7 @@ export function instanceWatch(
options?: WatchOptions, options?: WatchOptions,
): WatchHandle { ): WatchHandle {
const publicThis = this.proxy as any const publicThis = this.proxy as any
const getter = isString(source) let getter = isString(source)
? source.includes('.') ? source.includes('.')
? createPathGetter(publicThis, source) ? createPathGetter(publicThis, source)
: () => publicThis[source] : () => publicThis[source]
@ -256,6 +285,19 @@ export function instanceWatch(
cb = value.handler as Function cb = value.handler as Function
options = value 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 reset = setCurrentInstance(this) const reset = setCurrentInstance(this)
const res = doWatch(getter, cb.bind(publicThis), options) const res = doWatch(getter, cb.bind(publicThis), options)
reset() reset()

View File

@ -1,12 +1,11 @@
import { import type {
type Component, Component,
type ComponentInternalInstance, ComponentInternalInstance,
type ComponentInternalOptions, ComponentInternalOptions,
type ConcreteComponent, ConcreteComponent,
type Data, Data,
type InternalRenderFunction, InternalRenderFunction,
type SetupContext, SetupContext,
currentInstance,
} from './component' } from './component'
import { import {
type LooseRequired, type LooseRequired,
@ -19,11 +18,12 @@ import {
isPromise, isPromise,
isString, isString,
} from '@vue/shared' } from '@vue/shared'
import { type Ref, getCurrentScope, isRef, traverse } from '@vue/reactivity' import { type Ref, isRef } from '@vue/reactivity'
import { computed } from './apiComputed' import { computed } from './apiComputed'
import { import {
type WatchCallback, type WatchCallback,
type WatchOptions, type WatchOptions,
createCompatWatchGetter,
createPathGetter, createPathGetter,
watch, watch,
} from './apiWatch' } from './apiWatch'
@ -72,9 +72,9 @@ import { warn } from './warning'
import type { VNodeChild } from './vnode' import type { VNodeChild } from './vnode'
import { callWithAsyncErrorHandling } from './errorHandling' import { callWithAsyncErrorHandling } from './errorHandling'
import { deepMergeData } from './compat/data' import { deepMergeData } from './compat/data'
import { DeprecationTypes, checkCompatEnabled } from './compat/compatConfig'
import { import {
type CompatConfig, type CompatConfig,
DeprecationTypes,
isCompatEnabled, isCompatEnabled,
softAssertCompatEnabled, softAssertCompatEnabled,
} from './compat/compatConfig' } from './compat/compatConfig'
@ -843,7 +843,7 @@ function callHook(
) )
} }
export function createWatcher( function createWatcher(
raw: ComponentWatchOptionItem, raw: ComponentWatchOptionItem,
ctx: Data, ctx: Data,
publicThis: ComponentPublicInstance, publicThis: ComponentPublicInstance,
@ -854,30 +854,14 @@ export function createWatcher(
: () => (publicThis as any)[key] : () => (publicThis as any)[key]
const options: WatchOptions = {} const options: WatchOptions = {}
if (__COMPAT__) { if (
const instance = __COMPAT__ &&
currentInstance && getCurrentScope() === currentInstance.scope isCompatEnabled(DeprecationTypes.WATCH_ARRAY, publicThis.$)
? currentInstance ) {
: null const deep = isObject(raw) && !isArray(raw) && !isFunction(raw) && raw.deep
if (!deep) {
const newValue = getter() ;(options as any).compatWatchArray = true
if ( getter = createCompatWatchGetter(getter, publicThis.$)
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
} }
} }

View File

@ -47,26 +47,377 @@ test('mode as function', () => {
expect(vm.$el.innerHTML).toBe(`<div>foo</div><div>bar</div>`) expect(vm.$el.innerHTML).toBe(`<div>foo</div><div>bar</div>`)
}) })
test('WATCH_ARRAY', async () => { describe('WATCH_ARRAY', () => {
const spy = vi.fn() describe('watch option', () => {
const vm = new Vue({ test('basic usage', async () => {
data() { const spy = vi.fn()
return { const vm = new Vue({
foo: [], data() {
} return {
}, foo: [],
watch: { }
foo: spy, },
}, watch: {
}) as any foo: spy,
expect( },
deprecationData[DeprecationTypes.WATCH_ARRAY].message, }) as any
).toHaveBeenWarned() expect(
deprecationData[DeprecationTypes.WATCH_ARRAY].message,
).toHaveBeenWarned()
expect(spy).not.toHaveBeenCalled() expect(spy).not.toHaveBeenCalled()
vm.foo.push(1) vm.foo.push(1)
await nextTick() await nextTick()
expect(spy).toHaveBeenCalledTimes(1) 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', () => { test('PROPS_DEFAULT_THIS', () => {