775a7c2b41
BREAKING CHANGE: reactive arrays no longer unwraps contained refs When reactive arrays contain refs, especially a mix of refs and plain values, Array prototype methods will fail to function properly - e.g. sort() or reverse() will overwrite the ref's value instead of moving it (see #737). Ensuring correct behavior for all possible Array methods while retaining the ref unwrapping behavior is exceedinly complicated; In addition, even if Vue handles the built-in methods internally, it would still break when the user attempts to use a 3rd party utility functioon (e.g. lodash) on a reactive array containing refs. After this commit, similar to other collection types like Map and Set, Arrays will no longer automatically unwrap contained refs. The usage of mixed refs and plain values in Arrays should be rare in practice. In cases where this is necessary, the user can create a computed property that performs the unwrapping.
171 lines
4.5 KiB
TypeScript
171 lines
4.5 KiB
TypeScript
import { ref, isRef } from '../src/ref'
|
|
import {
|
|
reactive,
|
|
isReactive,
|
|
toRaw,
|
|
markNonReactive,
|
|
shallowReactive
|
|
} from '../src/reactive'
|
|
import { mockWarn } from '@vue/shared'
|
|
import { computed } from '../src/computed'
|
|
|
|
describe('reactivity/reactive', () => {
|
|
mockWarn()
|
|
|
|
test('Object', () => {
|
|
const original = { foo: 1 }
|
|
const observed = reactive(original)
|
|
expect(observed).not.toBe(original)
|
|
expect(isReactive(observed)).toBe(true)
|
|
expect(isReactive(original)).toBe(false)
|
|
// get
|
|
expect(observed.foo).toBe(1)
|
|
// has
|
|
expect('foo' in observed).toBe(true)
|
|
// ownKeys
|
|
expect(Object.keys(observed)).toEqual(['foo'])
|
|
})
|
|
|
|
test('nested reactives', () => {
|
|
const original = {
|
|
nested: {
|
|
foo: 1
|
|
},
|
|
array: [{ bar: 2 }]
|
|
}
|
|
const observed = reactive(original)
|
|
expect(isReactive(observed.nested)).toBe(true)
|
|
expect(isReactive(observed.array)).toBe(true)
|
|
expect(isReactive(observed.array[0])).toBe(true)
|
|
})
|
|
|
|
test('observed value should proxy mutations to original (Object)', () => {
|
|
const original: any = { foo: 1 }
|
|
const observed = reactive(original)
|
|
// set
|
|
observed.bar = 1
|
|
expect(observed.bar).toBe(1)
|
|
expect(original.bar).toBe(1)
|
|
// delete
|
|
delete observed.foo
|
|
expect('foo' in observed).toBe(false)
|
|
expect('foo' in original).toBe(false)
|
|
})
|
|
|
|
test('setting a property with an unobserved value should wrap with reactive', () => {
|
|
const observed = reactive<{ foo?: object }>({})
|
|
const raw = {}
|
|
observed.foo = raw
|
|
expect(observed.foo).not.toBe(raw)
|
|
expect(isReactive(observed.foo)).toBe(true)
|
|
})
|
|
|
|
test('observing already observed value should return same Proxy', () => {
|
|
const original = { foo: 1 }
|
|
const observed = reactive(original)
|
|
const observed2 = reactive(observed)
|
|
expect(observed2).toBe(observed)
|
|
})
|
|
|
|
test('observing the same value multiple times should return same Proxy', () => {
|
|
const original = { foo: 1 }
|
|
const observed = reactive(original)
|
|
const observed2 = reactive(original)
|
|
expect(observed2).toBe(observed)
|
|
})
|
|
|
|
test('should not pollute original object with Proxies', () => {
|
|
const original: any = { foo: 1 }
|
|
const original2 = { bar: 2 }
|
|
const observed = reactive(original)
|
|
const observed2 = reactive(original2)
|
|
observed.bar = observed2
|
|
expect(observed.bar).toBe(observed2)
|
|
expect(original.bar).toBe(original2)
|
|
})
|
|
|
|
test('unwrap', () => {
|
|
const original = { foo: 1 }
|
|
const observed = reactive(original)
|
|
expect(toRaw(observed)).toBe(original)
|
|
expect(toRaw(original)).toBe(original)
|
|
})
|
|
|
|
test('should not unwrap Ref<T>', () => {
|
|
const observedNumberRef = reactive(ref(1))
|
|
const observedObjectRef = reactive(ref({ foo: 1 }))
|
|
|
|
expect(isRef(observedNumberRef)).toBe(true)
|
|
expect(isRef(observedObjectRef)).toBe(true)
|
|
})
|
|
|
|
test('should unwrap computed refs', () => {
|
|
// readonly
|
|
const a = computed(() => 1)
|
|
// writable
|
|
const b = computed({
|
|
get: () => 1,
|
|
set: () => {}
|
|
})
|
|
const obj = reactive({ a, b })
|
|
// check type
|
|
obj.a + 1
|
|
obj.b + 1
|
|
expect(typeof obj.a).toBe(`number`)
|
|
expect(typeof obj.b).toBe(`number`)
|
|
})
|
|
|
|
test('non-observable values', () => {
|
|
const assertValue = (value: any) => {
|
|
reactive(value)
|
|
expect(
|
|
`value cannot be made reactive: ${String(value)}`
|
|
).toHaveBeenWarnedLast()
|
|
}
|
|
|
|
// number
|
|
assertValue(1)
|
|
// string
|
|
assertValue('foo')
|
|
// boolean
|
|
assertValue(false)
|
|
// null
|
|
assertValue(null)
|
|
// undefined
|
|
assertValue(undefined)
|
|
// symbol
|
|
const s = Symbol()
|
|
assertValue(s)
|
|
|
|
// built-ins should work and return same value
|
|
const p = Promise.resolve()
|
|
expect(reactive(p)).toBe(p)
|
|
const r = new RegExp('')
|
|
expect(reactive(r)).toBe(r)
|
|
const d = new Date()
|
|
expect(reactive(d)).toBe(d)
|
|
})
|
|
|
|
test('markNonReactive', () => {
|
|
const obj = reactive({
|
|
foo: { a: 1 },
|
|
bar: markNonReactive({ b: 2 })
|
|
})
|
|
expect(isReactive(obj.foo)).toBe(true)
|
|
expect(isReactive(obj.bar)).toBe(false)
|
|
})
|
|
|
|
describe('shallowReactive', () => {
|
|
test('should not make non-reactive properties reactive', () => {
|
|
const props = shallowReactive({ n: { foo: 1 } })
|
|
expect(isReactive(props.n)).toBe(false)
|
|
})
|
|
|
|
test('should keep reactive properties reactive', () => {
|
|
const props: any = shallowReactive({ n: reactive({ foo: 1 }) })
|
|
props.n = reactive({ foo: 2 })
|
|
expect(isReactive(props.n)).toBe(true)
|
|
})
|
|
})
|
|
})
|