wip: shallowReactive/shallowReadonly

This commit is contained in:
Evan You 2022-05-27 12:07:19 +08:00
parent a5ad708ccf
commit f29ad1def5
8 changed files with 450 additions and 39 deletions

View File

@ -81,7 +81,7 @@ export function computed<T>(
} as any
def(ref, RefFlag, true)
def(ref, ReactiveFlags.IS_READONLY, true)
def(ref, ReactiveFlags.IS_READONLY, onlyGetter)
return ref
}

View File

@ -1,5 +1,5 @@
import { observe, Observer } from 'core/observer'
import { def, isPrimitive, warn, toRawType } from 'core/util'
import { def, isArray, isPrimitive, warn, toRawType } from 'core/util'
import type { Ref, UnwrapRefSimple, RawSymbol } from './ref'
export const enum ReactiveFlags {
@ -22,20 +22,7 @@ export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<T>
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (!isReadonly(target)) {
const ob = observe(target)
if (__DEV__ && !ob) {
if (target == null || isPrimitive(target)) {
warn(`value cannot be made reactive: ${String(target)}`)
}
if (isCollectionType(target)) {
warn(
`Vue 2 does not support reactive collection types such as Map or Set.`
)
}
}
}
makeReactive(target, false)
return target
}
@ -51,10 +38,49 @@ export type ShallowReactive<T> = T & { [ShallowReactiveMarker]?: true }
export function shallowReactive<T extends object>(
target: T
): ShallowReactive<T> {
// TODO
makeReactive(target, true)
def(target, ReactiveFlags.IS_SHALLOW, true)
return target
}
function makeReactive(target: any, shallow: boolean) {
// if trying to observe a readonly proxy, return the readonly version.
if (!isReadonly(target)) {
if (__DEV__) {
if (isArray(target)) {
warn(
`Avoid using Array as root value for ${
shallow ? `shallowReactive()` : `reactive()`
} as it cannot be tracked in watch() or watchEffect(). Use ${
shallow ? `shallowRef()` : `ref()`
} instead. This is a Vue-2-only limitation.`
)
}
const existingOb = target && target.__ob__
if (existingOb && existingOb.shallow !== shallow) {
warn(
`Target is already a ${
existingOb.shallow ? `` : `non-`
}shallow reactive object, and cannot be converted to ${
shallow ? `` : `non-`
}shallow.`
)
}
}
const ob = observe(target, shallow)
if (__DEV__ && !ob) {
if (target == null || isPrimitive(target)) {
warn(`value cannot be made reactive: ${String(target)}`)
}
if (isCollectionType(target)) {
warn(
`Vue 2 does not support reactive collection types such as Map or Set.`
)
}
}
}
}
export function isReactive(value: unknown): boolean {
if (isReadonly(value)) {
return isReactive((value as Target)[ReactiveFlags.RAW])

View File

@ -33,10 +33,15 @@ export type DeepReadonly<T> = T extends Builtin
: Readonly<T>
const rawToReadonlyFlag = `__v_rawToReadonly`
const rawToShallowReadonlyFlag = `__v_rawToShallowReadonly`
export function readonly<T extends object>(
target: T
): DeepReadonly<UnwrapNestedRefs<T>> {
return createReadonly(target, false)
}
function createReadonly(target: any, shallow: boolean) {
if (!isPlainObject(target)) {
if (__DEV__) {
if (isArray(target)) {
@ -58,13 +63,14 @@ export function readonly<T extends object>(
}
// already has a readonly proxy
const existingProxy = target[rawToReadonlyFlag]
const existingFlag = shallow ? rawToShallowReadonlyFlag : rawToReadonlyFlag
const existingProxy = target[existingFlag]
if (existingProxy) {
return existingProxy
}
const proxy = {}
def(target, rawToReadonlyFlag, proxy)
def(target, existingFlag, proxy)
def(proxy, ReactiveFlags.IS_READONLY, true)
def(proxy, ReactiveFlags.RAW, target)
@ -72,25 +78,30 @@ export function readonly<T extends object>(
if (isRef(target)) {
def(proxy, RefFlag, true)
}
if (isShallow(target)) {
if (shallow || isShallow(target)) {
def(proxy, ReactiveFlags.IS_SHALLOW, true)
}
const keys = Object.keys(target)
for (let i = 0; i < keys.length; i++) {
defineReadonlyProperty(proxy, target, keys[i])
defineReadonlyProperty(proxy, target, keys[i], shallow)
}
return proxy as any
}
function defineReadonlyProperty(proxy: any, target: any, key: string) {
function defineReadonlyProperty(
proxy: any,
target: any,
key: string,
shallow: boolean
) {
Object.defineProperty(proxy, key, {
enumerable: true,
configurable: true,
get() {
const val = target[key]
return isPlainObject(val) ? readonly(val) : val
return shallow || !isPlainObject(val) ? val : readonly(val)
},
set() {
__DEV__ &&
@ -106,5 +117,5 @@ function defineReadonlyProperty(proxy: any, target: any, key: string) {
* This is used for creating the props proxy object for stateful components.
*/
export function shallowReadonly<T extends object>(target: T): Readonly<T> {
return target as any
return createReadonly(target, true)
}

View File

@ -54,7 +54,7 @@ export function initState(vm: Component) {
if (opts.data) {
initData(vm)
} else {
observe((vm._data = {}), true /* asRootData */)
observe((vm._data = {}))!.vmCount++
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
@ -148,7 +148,7 @@ function initData(vm: Component) {
}
}
// observe data
observe(data, true /* asRootData */)
observe(data)!.vmCount++
}
export function getData(data: Function, vm: Component): any {

View File

@ -36,12 +36,11 @@ export function toggleObserving(value: boolean) {
* collect dependencies and dispatch updates.
*/
export class Observer {
value: any
dep: Dep
vmCount: number // number of vms that have this object as root $data
constructor(value: any) {
this.value = value
constructor(public value: any, public shallow = false) {
// this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
@ -51,9 +50,11 @@ export class Observer {
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
if (!shallow) {
this.observeArray(value)
}
} else {
this.walk(value)
this.walk(value, shallow)
}
}
@ -62,10 +63,11 @@ export class Observer {
* getter/setters. This method should only be called when
* value type is Object.
*/
walk(obj: object) {
walk(obj: object, shallow: boolean) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
const key = keys[i]
defineReactive(obj, key, obj[key], undefined, shallow)
}
}
@ -108,7 +110,7 @@ function copyAugment(target: Object, src: Object, keys: Array<string>) {
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
export function observe(value: any, asRootData?: boolean): Observer | void {
export function observe(value: any, shallow?: boolean): Observer | void {
if (!isObject(value) || isRef(value) || value instanceof VNode) {
return
}
@ -122,10 +124,7 @@ export function observe(value: any, asRootData?: boolean): Observer | void {
Object.isExtensible(value) &&
!value.__v_skip
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
ob = new Observer(value, shallow)
}
return ob
}

View File

@ -341,7 +341,7 @@ describe('reactivity/ref', () => {
})
test('toRefs reactive array', () => {
const arr = reactive(['a', 'b', 'c'])
const { arr } = reactive({ arr: ['a', 'b', 'c'] })
const refs = toRefs(arr)
expect(Array.isArray(refs)).toBe(true)

View File

@ -0,0 +1,169 @@
import {
isReactive,
isShallow,
reactive,
shallowReactive,
shallowReadonly
} from 'vca/index'
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)
})
test('isShallow', () => {
expect(isShallow(shallowReactive({}))).toBe(true)
expect(isShallow(shallowReadonly({}))).toBe(true)
})
// #5271
test('should respect shallow reactive nested inside reactive on reset', () => {
const r = reactive({ foo: shallowReactive({ bar: {} }) })
expect(isShallow(r.foo)).toBe(true)
expect(isReactive(r.foo.bar)).toBe(false)
r.foo = shallowReactive({ bar: {} })
expect(isShallow(r.foo)).toBe(true)
expect(isReactive(r.foo.bar)).toBe(false)
})
// @discrepancy no shallow/non-shallow versions from the same source -
// cannot support this without real proxies
// #2843
// test('should allow shallow and normal reactive for same target', () => {
// const original = { foo: {} }
// const shallowProxy = shallowReactive(original)
// const reactiveProxy = reactive(original)
// expect(shallowProxy).not.toBe(reactiveProxy)
// expect(isReactive(shallowProxy.foo)).toBe(false)
// expect(isReactive(reactiveProxy.foo)).toBe(true)
// })
// test('should respect shallow/deep versions of same target on access', () => {
// const original = {}
// const shallow = shallowReactive(original)
// const deep = reactive(original)
// const r = reactive({ shallow, deep })
// expect(r.shallow).toBe(shallow)
// expect(r.deep).toBe(deep)
// })
// @discrepancy Vue 2 does not support collections
// describe('collections', () => {
// test('should be reactive', () => {
// const shallowSet = shallowReactive(new Set())
// const a = {}
// let size
// effect(() => {
// size = shallowSet.size
// })
// expect(size).toBe(0)
// shallowSet.add(a)
// expect(size).toBe(1)
// shallowSet.delete(a)
// expect(size).toBe(0)
// })
// test('should not observe when iterating', () => {
// const shallowSet = shallowReactive(new Set())
// const a = {}
// shallowSet.add(a)
// const spreadA = [...shallowSet][0]
// expect(isReactive(spreadA)).toBe(false)
// })
// test('should not get reactive entry', () => {
// const shallowMap = shallowReactive(new Map())
// const a = {}
// const key = 'a'
// shallowMap.set(key, a)
// expect(isReactive(shallowMap.get(key))).toBe(false)
// })
// test('should not get reactive on foreach', () => {
// const shallowSet = shallowReactive(new Set())
// const a = {}
// shallowSet.add(a)
// shallowSet.forEach(x => expect(isReactive(x)).toBe(false))
// })
// // #1210
// test('onTrack on called on objectSpread', () => {
// const onTrackFn = vi.fn()
// const shallowSet = shallowReactive(new Set())
// let a
// effect(
// () => {
// a = Array.from(shallowSet)
// },
// {
// onTrack: onTrackFn
// }
// )
// expect(a).toMatchObject([])
// expect(onTrackFn).toHaveBeenCalled()
// })
// })
// @discrepancy Vue 2 does not track array without access
// describe('array', () => {
// test('should be reactive', () => {
// const shallowArray = shallowReactive<unknown[]>([])
// const a = {}
// let size
// effect(() => {
// size = shallowArray.length
// })
// expect(size).toBe(0)
// shallowArray.push(a)
// expect(size).toBe(1)
// shallowArray.pop()
// expect(size).toBe(0)
// })
// test('should not observe when iterating', () => {
// const shallowArray = shallowReactive<object[]>([])
// const a = {}
// shallowArray.push(a)
// const spreadA = [...shallowArray][0]
// expect(isReactive(spreadA)).toBe(false)
// })
// test('onTrack on called on objectSpread', () => {
// const onTrackFn = vi.fn()
// const shallowArray = shallowReactive([])
// let a
// effect(
// () => {
// a = Array.from(shallowArray)
// },
// {
// onTrack: onTrackFn
// }
// )
// expect(a).toMatchObject([])
// expect(onTrackFn).toHaveBeenCalled()
// })
// })
})

View File

@ -0,0 +1,206 @@
import { isReactive, shallowReadonly, readonly, isReadonly } from 'vca/index'
describe('reactivity/shallowReadonly', () => {
test('should be readonly', () => {
expect(isReadonly(shallowReadonly({}))).toBe(true)
})
test('should not make non-reactive properties reactive', () => {
const props = shallowReadonly({ n: { foo: 1 } })
expect(isReactive(props.n)).toBe(false)
})
test('should make root level properties readonly', () => {
const props = shallowReadonly({ n: 1 })
// @ts-expect-error
props.n = 2
expect(props.n).toBe(1)
expect(
`Set operation on key "n" failed: target is readonly.`
).toHaveBeenWarned()
})
// to retain 2.x behavior.
test('should NOT make nested properties readonly', () => {
const props = shallowReadonly({ n: { foo: 1 } })
props.n.foo = 2
expect(props.n.foo).toBe(2)
expect(
`Set operation on key "foo" failed: target is readonly.`
).not.toHaveBeenWarned()
})
// #2843
test('should differentiate from normal readonly calls', () => {
const original = { foo: {} }
const shallowProxy = shallowReadonly(original)
const reactiveProxy = readonly(original)
expect(shallowProxy).not.toBe(reactiveProxy)
expect(isReadonly(shallowProxy.foo)).toBe(false)
expect(isReadonly(reactiveProxy.foo)).toBe(true)
})
// @discrepancy does not support collections
// describe('collection/Map', () => {
// ;[Map, WeakMap].forEach(Collection => {
// test('should make the map/weak-map readonly', () => {
// const key = {}
// const val = { foo: 1 }
// const original = new Collection([[key, val]])
// const sroMap = shallowReadonly(original)
// expect(isReadonly(sroMap)).toBe(true)
// expect(isReactive(sroMap)).toBe(false)
// expect(sroMap.get(key)).toBe(val)
// sroMap.set(key, {} as any)
// expect(
// `Set operation on key "[object Object]" failed: target is readonly.`
// ).toHaveBeenWarned()
// })
// test('should not make nested values readonly', () => {
// const key = {}
// const val = { foo: 1 }
// const original = new Collection([[key, val]])
// const sroMap = shallowReadonly(original)
// expect(isReadonly(sroMap.get(key))).toBe(false)
// expect(isReactive(sroMap.get(key))).toBe(false)
// sroMap.get(key)!.foo = 2
// expect(
// `Set operation on key "foo" failed: target is readonly.`
// ).not.toHaveBeenWarned()
// })
// })
// test('should not make the value generated by the iterable method readonly', () => {
// const key = {}
// const val = { foo: 1 }
// const original = new Map([[key, val]])
// const sroMap = shallowReadonly(original)
// const values1 = [...sroMap.values()]
// const values2 = [...sroMap.entries()]
// expect(isReadonly(values1[0])).toBe(false)
// expect(isReactive(values1[0])).toBe(false)
// expect(values1[0]).toBe(val)
// values1[0].foo = 2
// expect(
// `Set operation on key "foo" failed: target is readonly.`
// ).not.toHaveBeenWarned()
// expect(isReadonly(values2[0][1])).toBe(false)
// expect(isReactive(values2[0][1])).toBe(false)
// expect(values2[0][1]).toBe(val)
// values2[0][1].foo = 2
// expect(
// `Set operation on key "foo" failed: target is readonly.`
// ).not.toHaveBeenWarned()
// })
// test('should not make the value generated by the forEach method readonly', () => {
// const val = { foo: 1 }
// const original = new Map([['key', val]])
// const sroMap = shallowReadonly(original)
// sroMap.forEach(val => {
// expect(isReadonly(val)).toBe(false)
// expect(isReactive(val)).toBe(false)
// expect(val).toBe(val)
// val.foo = 2
// expect(
// `Set operation on key "foo" failed: target is readonly.`
// ).not.toHaveBeenWarned()
// })
// })
// })
// describe('collection/Set', () => {
// test('should make the set/weak-set readonly', () => {
// ;[Set, WeakSet].forEach(Collection => {
// const obj = { foo: 1 }
// const original = new Collection([obj])
// const sroSet = shallowReadonly(original)
// expect(isReadonly(sroSet)).toBe(true)
// expect(isReactive(sroSet)).toBe(false)
// expect(sroSet.has(obj)).toBe(true)
// sroSet.add({} as any)
// expect(
// `Add operation on key "[object Object]" failed: target is readonly.`
// ).toHaveBeenWarned()
// })
// })
// test('should not make nested values readonly', () => {
// const obj = { foo: 1 }
// const original = new Set([obj])
// const sroSet = shallowReadonly(original)
// const values = [...sroSet.values()]
// expect(values[0]).toBe(obj)
// expect(isReadonly(values[0])).toBe(false)
// expect(isReactive(values[0])).toBe(false)
// sroSet.add({} as any)
// expect(
// `Add operation on key "[object Object]" failed: target is readonly.`
// ).toHaveBeenWarned()
// values[0].foo = 2
// expect(
// `Set operation on key "foo" failed: target is readonly.`
// ).not.toHaveBeenWarned()
// })
// test('should not make the value generated by the iterable method readonly', () => {
// const val = { foo: 1 }
// const original = new Set([val])
// const sroSet = shallowReadonly(original)
// const values1 = [...sroSet.values()]
// const values2 = [...sroSet.entries()]
// expect(isReadonly(values1[0])).toBe(false)
// expect(isReactive(values1[0])).toBe(false)
// expect(values1[0]).toBe(val)
// values1[0].foo = 2
// expect(
// `Set operation on key "foo" failed: target is readonly.`
// ).not.toHaveBeenWarned()
// expect(isReadonly(values2[0][1])).toBe(false)
// expect(isReactive(values2[0][1])).toBe(false)
// expect(values2[0][1]).toBe(val)
// values2[0][1].foo = 2
// expect(
// `Set operation on key "foo" failed: target is readonly.`
// ).not.toHaveBeenWarned()
// })
// test('should not make the value generated by the forEach method readonly', () => {
// const val = { foo: 1 }
// const original = new Set([val])
// const sroSet = shallowReadonly(original)
// sroSet.forEach(val => {
// expect(isReadonly(val)).toBe(false)
// expect(isReactive(val)).toBe(false)
// expect(val).toBe(val)
// val.foo = 2
// expect(
// `Set operation on key "foo" failed: target is readonly.`
// ).not.toHaveBeenWarned()
// })
// })
// })
})