Merge remote-tracking branch 'github/master' into changing_unwrap_ref
This commit is contained in:
commit
dcb5985c00
@ -30,7 +30,7 @@
|
|||||||
* **compiler:** compiler options have been adjusted.
|
* **compiler:** compiler options have been adjusted.
|
||||||
- new option `decodeEntities` is added.
|
- new option `decodeEntities` is added.
|
||||||
- `namedCharacterReferences` option has been removed.
|
- `namedCharacterReferences` option has been removed.
|
||||||
- `maxCRNameLength` option has been rmeoved.
|
- `maxCRNameLength` option has been removed.
|
||||||
* **asyncComponent:** `retryWhen` and `maxRetries` options for
|
* **asyncComponent:** `retryWhen` and `maxRetries` options for
|
||||||
`defineAsyncComponent` has been replaced by the more flexible `onError`
|
`defineAsyncComponent` has been replaced by the more flexible `onError`
|
||||||
option, per https://github.com/vuejs/rfcs/pull/148
|
option, per https://github.com/vuejs/rfcs/pull/148
|
||||||
|
@ -5,9 +5,6 @@ import {
|
|||||||
isReactive,
|
isReactive,
|
||||||
isReadonly,
|
isReadonly,
|
||||||
markNonReactive,
|
markNonReactive,
|
||||||
markReadonly,
|
|
||||||
lock,
|
|
||||||
unlock,
|
|
||||||
effect,
|
effect,
|
||||||
ref,
|
ref,
|
||||||
shallowReadonly
|
shallowReadonly
|
||||||
@ -91,22 +88,7 @@ describe('reactivity/readonly', () => {
|
|||||||
).toHaveBeenWarnedLast()
|
).toHaveBeenWarnedLast()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should allow mutation when unlocked', () => {
|
it('should not trigger effects', () => {
|
||||||
const observed: any = readonly({ foo: 1, bar: { baz: 2 } })
|
|
||||||
unlock()
|
|
||||||
observed.prop = 2
|
|
||||||
observed.bar.qux = 3
|
|
||||||
delete observed.bar.baz
|
|
||||||
delete observed.foo
|
|
||||||
lock()
|
|
||||||
expect(observed.prop).toBe(2)
|
|
||||||
expect(observed.foo).toBeUndefined()
|
|
||||||
expect(observed.bar.qux).toBe(3)
|
|
||||||
expect('baz' in observed.bar).toBe(false)
|
|
||||||
expect(`target is readonly`).not.toHaveBeenWarned()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not trigger effects when locked', () => {
|
|
||||||
const observed: any = readonly({ a: 1 })
|
const observed: any = readonly({ a: 1 })
|
||||||
let dummy
|
let dummy
|
||||||
effect(() => {
|
effect(() => {
|
||||||
@ -118,20 +100,6 @@ describe('reactivity/readonly', () => {
|
|||||||
expect(dummy).toBe(1)
|
expect(dummy).toBe(1)
|
||||||
expect(`target is readonly`).toHaveBeenWarned()
|
expect(`target is readonly`).toHaveBeenWarned()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should trigger effects when unlocked', () => {
|
|
||||||
const observed: any = readonly({ a: 1 })
|
|
||||||
let dummy
|
|
||||||
effect(() => {
|
|
||||||
dummy = observed.a
|
|
||||||
})
|
|
||||||
expect(dummy).toBe(1)
|
|
||||||
unlock()
|
|
||||||
observed.a = 2
|
|
||||||
lock()
|
|
||||||
expect(observed.a).toBe(2)
|
|
||||||
expect(dummy).toBe(2)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Array', () => {
|
describe('Array', () => {
|
||||||
@ -183,23 +151,7 @@ describe('reactivity/readonly', () => {
|
|||||||
expect(`target is readonly.`).toHaveBeenWarnedTimes(5)
|
expect(`target is readonly.`).toHaveBeenWarnedTimes(5)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should allow mutation when unlocked', () => {
|
it('should not trigger effects', () => {
|
||||||
const observed: any = readonly([{ foo: 1, bar: { baz: 2 } }])
|
|
||||||
unlock()
|
|
||||||
observed[1] = 2
|
|
||||||
observed.push(3)
|
|
||||||
observed[0].foo = 2
|
|
||||||
observed[0].bar.baz = 3
|
|
||||||
lock()
|
|
||||||
expect(observed.length).toBe(3)
|
|
||||||
expect(observed[1]).toBe(2)
|
|
||||||
expect(observed[2]).toBe(3)
|
|
||||||
expect(observed[0].foo).toBe(2)
|
|
||||||
expect(observed[0].bar.baz).toBe(3)
|
|
||||||
expect(`target is readonly`).not.toHaveBeenWarned()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not trigger effects when locked', () => {
|
|
||||||
const observed: any = readonly([{ a: 1 }])
|
const observed: any = readonly([{ a: 1 }])
|
||||||
let dummy
|
let dummy
|
||||||
effect(() => {
|
effect(() => {
|
||||||
@ -215,30 +167,6 @@ describe('reactivity/readonly', () => {
|
|||||||
expect(dummy).toBe(1)
|
expect(dummy).toBe(1)
|
||||||
expect(`target is readonly`).toHaveBeenWarnedTimes(2)
|
expect(`target is readonly`).toHaveBeenWarnedTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should trigger effects when unlocked', () => {
|
|
||||||
const observed: any = readonly([{ a: 1 }])
|
|
||||||
let dummy
|
|
||||||
effect(() => {
|
|
||||||
dummy = observed[0].a
|
|
||||||
})
|
|
||||||
expect(dummy).toBe(1)
|
|
||||||
|
|
||||||
unlock()
|
|
||||||
|
|
||||||
observed[0].a = 2
|
|
||||||
expect(observed[0].a).toBe(2)
|
|
||||||
expect(dummy).toBe(2)
|
|
||||||
|
|
||||||
observed[0] = { a: 3 }
|
|
||||||
expect(observed[0].a).toBe(3)
|
|
||||||
expect(dummy).toBe(3)
|
|
||||||
|
|
||||||
observed.unshift({ a: 4 })
|
|
||||||
expect(observed[0].a).toBe(4)
|
|
||||||
expect(dummy).toBe(4)
|
|
||||||
lock()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const maps = [Map, WeakMap]
|
const maps = [Map, WeakMap]
|
||||||
@ -276,23 +204,6 @@ describe('reactivity/readonly', () => {
|
|||||||
).toHaveBeenWarned()
|
).toHaveBeenWarned()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should allow mutation & trigger effect when unlocked', () => {
|
|
||||||
const map = readonly(new Collection())
|
|
||||||
const isWeak = Collection === WeakMap
|
|
||||||
const key = {}
|
|
||||||
let dummy
|
|
||||||
effect(() => {
|
|
||||||
dummy = map.get(key) + (isWeak ? 0 : map.size)
|
|
||||||
})
|
|
||||||
expect(dummy).toBeNaN()
|
|
||||||
unlock()
|
|
||||||
map.set(key, 1)
|
|
||||||
lock()
|
|
||||||
expect(dummy).toBe(isWeak ? 1 : 2)
|
|
||||||
expect(map.get(key)).toBe(1)
|
|
||||||
expect(`target is readonly`).not.toHaveBeenWarned()
|
|
||||||
})
|
|
||||||
|
|
||||||
if (Collection === Map) {
|
if (Collection === Map) {
|
||||||
test('should retrieve readonly values on iteration', () => {
|
test('should retrieve readonly values on iteration', () => {
|
||||||
const key1 = {}
|
const key1 = {}
|
||||||
@ -347,22 +258,6 @@ describe('reactivity/readonly', () => {
|
|||||||
).toHaveBeenWarned()
|
).toHaveBeenWarned()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should allow mutation & trigger effect when unlocked', () => {
|
|
||||||
const set = readonly(new Collection())
|
|
||||||
const key = {}
|
|
||||||
let dummy
|
|
||||||
effect(() => {
|
|
||||||
dummy = set.has(key)
|
|
||||||
})
|
|
||||||
expect(dummy).toBe(false)
|
|
||||||
unlock()
|
|
||||||
set.add(key)
|
|
||||||
lock()
|
|
||||||
expect(dummy).toBe(true)
|
|
||||||
expect(set.has(key)).toBe(true)
|
|
||||||
expect(`target is readonly`).not.toHaveBeenWarned()
|
|
||||||
})
|
|
||||||
|
|
||||||
if (Collection === Set) {
|
if (Collection === Set) {
|
||||||
test('should retrieve readonly values on iteration', () => {
|
test('should retrieve readonly values on iteration', () => {
|
||||||
const original = new Collection([{}, {}])
|
const original = new Collection([{}, {}])
|
||||||
@ -401,6 +296,19 @@ describe('reactivity/readonly', () => {
|
|||||||
expect(toRaw(a)).toBe(toRaw(b))
|
expect(toRaw(a)).toBe(toRaw(b))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('readonly should track and trigger if wrapping reactive original', () => {
|
||||||
|
const a = reactive({ n: 1 })
|
||||||
|
const b = readonly(a)
|
||||||
|
let dummy
|
||||||
|
effect(() => {
|
||||||
|
dummy = b.n
|
||||||
|
})
|
||||||
|
expect(dummy).toBe(1)
|
||||||
|
a.n++
|
||||||
|
expect(b.n).toBe(2)
|
||||||
|
expect(dummy).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
test('observing already observed value should return same Proxy', () => {
|
test('observing already observed value should return same Proxy', () => {
|
||||||
const original = { foo: 1 }
|
const original = { foo: 1 }
|
||||||
const observed = readonly(original)
|
const observed = readonly(original)
|
||||||
@ -424,17 +332,6 @@ describe('reactivity/readonly', () => {
|
|||||||
expect(isReactive(obj.bar)).toBe(false)
|
expect(isReactive(obj.bar)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('markReadonly', () => {
|
|
||||||
const obj = reactive({
|
|
||||||
foo: { a: 1 },
|
|
||||||
bar: markReadonly({ b: 2 })
|
|
||||||
})
|
|
||||||
expect(isReactive(obj.foo)).toBe(true)
|
|
||||||
expect(isReactive(obj.bar)).toBe(true)
|
|
||||||
expect(isReadonly(obj.foo)).toBe(false)
|
|
||||||
expect(isReadonly(obj.bar)).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('should make ref readonly', () => {
|
test('should make ref readonly', () => {
|
||||||
const n: any = readonly(ref(1))
|
const n: any = readonly(ref(1))
|
||||||
n.value = 2
|
n.value = 2
|
||||||
@ -470,13 +367,5 @@ describe('reactivity/readonly', () => {
|
|||||||
`Set operation on key "foo" failed: target is readonly.`
|
`Set operation on key "foo" failed: target is readonly.`
|
||||||
).not.toHaveBeenWarned()
|
).not.toHaveBeenWarned()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should keep reactive properties reactive', () => {
|
|
||||||
const props: any = shallowReadonly({ n: reactive({ foo: 1 }) })
|
|
||||||
unlock()
|
|
||||||
props.n = reactive({ foo: 2 })
|
|
||||||
lock()
|
|
||||||
expect(isReactive(props.n)).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -3,12 +3,13 @@ import {
|
|||||||
effect,
|
effect,
|
||||||
reactive,
|
reactive,
|
||||||
isRef,
|
isRef,
|
||||||
|
toRef,
|
||||||
toRefs,
|
toRefs,
|
||||||
Ref,
|
Ref,
|
||||||
isReactive
|
isReactive
|
||||||
} from '../src/index'
|
} from '../src/index'
|
||||||
import { computed } from '@vue/runtime-dom'
|
import { computed } from '@vue/runtime-dom'
|
||||||
import { shallowRef, unref } from '../src/ref'
|
import { shallowRef, unref, customRef } from '../src/ref'
|
||||||
|
|
||||||
describe('reactivity/ref', () => {
|
describe('reactivity/ref', () => {
|
||||||
it('should hold a value', () => {
|
it('should hold a value', () => {
|
||||||
@ -168,6 +169,34 @@ describe('reactivity/ref', () => {
|
|||||||
expect(isRef({ value: 0 })).toBe(false)
|
expect(isRef({ value: 0 })).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('toRef', () => {
|
||||||
|
const a = reactive({
|
||||||
|
x: 1
|
||||||
|
})
|
||||||
|
const x = toRef(a, 'x')
|
||||||
|
expect(isRef(x)).toBe(true)
|
||||||
|
expect(x.value).toBe(1)
|
||||||
|
|
||||||
|
// source -> proxy
|
||||||
|
a.x = 2
|
||||||
|
expect(x.value).toBe(2)
|
||||||
|
|
||||||
|
// proxy -> source
|
||||||
|
x.value = 3
|
||||||
|
expect(a.x).toBe(3)
|
||||||
|
|
||||||
|
// reactivity
|
||||||
|
let dummyX
|
||||||
|
effect(() => {
|
||||||
|
dummyX = x.value
|
||||||
|
})
|
||||||
|
expect(dummyX).toBe(x.value)
|
||||||
|
|
||||||
|
// mutating source should trigger effect using the proxy refs
|
||||||
|
a.x = 4
|
||||||
|
expect(dummyX).toBe(4)
|
||||||
|
})
|
||||||
|
|
||||||
test('toRefs', () => {
|
test('toRefs', () => {
|
||||||
const a = reactive({
|
const a = reactive({
|
||||||
x: 1,
|
x: 1,
|
||||||
@ -208,4 +237,35 @@ describe('reactivity/ref', () => {
|
|||||||
expect(dummyX).toBe(4)
|
expect(dummyX).toBe(4)
|
||||||
expect(dummyY).toBe(5)
|
expect(dummyY).toBe(5)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('customRef', () => {
|
||||||
|
let value = 1
|
||||||
|
let _trigger: () => void
|
||||||
|
|
||||||
|
const custom = customRef((track, trigger) => ({
|
||||||
|
get() {
|
||||||
|
track()
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
set(newValue: number) {
|
||||||
|
value = newValue
|
||||||
|
_trigger = trigger
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
expect(isRef(custom)).toBe(true)
|
||||||
|
|
||||||
|
let dummy
|
||||||
|
effect(() => {
|
||||||
|
dummy = custom.value
|
||||||
|
})
|
||||||
|
expect(dummy).toBe(1)
|
||||||
|
|
||||||
|
custom.value = 2
|
||||||
|
// should not trigger yet
|
||||||
|
expect(dummy).toBe(1)
|
||||||
|
|
||||||
|
_trigger!()
|
||||||
|
expect(dummy).toBe(2)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { reactive, readonly, toRaw } from './reactive'
|
import { reactive, readonly, toRaw } from './reactive'
|
||||||
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
||||||
import { track, trigger, ITERATE_KEY } from './effect'
|
import { track, trigger, ITERATE_KEY } from './effect'
|
||||||
import { LOCKED } from './lock'
|
|
||||||
import { isObject, hasOwn, isSymbol, hasChanged, isArray } from '@vue/shared'
|
import { isObject, hasOwn, isSymbol, hasChanged, isArray } from '@vue/shared'
|
||||||
import { isRef } from './ref'
|
import { isRef } from './ref'
|
||||||
|
|
||||||
@ -12,7 +11,7 @@ const builtInSymbols = new Set(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const get = /*#__PURE__*/ createGetter()
|
const get = /*#__PURE__*/ createGetter()
|
||||||
const shallowReactiveGet = /*#__PURE__*/ createGetter(false, true)
|
const shallowGet = /*#__PURE__*/ createGetter(false, true)
|
||||||
const readonlyGet = /*#__PURE__*/ createGetter(true)
|
const readonlyGet = /*#__PURE__*/ createGetter(true)
|
||||||
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)
|
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)
|
||||||
|
|
||||||
@ -36,23 +35,32 @@ const arrayInstrumentations: Record<string, Function> = {}
|
|||||||
|
|
||||||
function createGetter(isReadonly = false, shallow = false) {
|
function createGetter(isReadonly = false, shallow = false) {
|
||||||
return function get(target: object, key: string | symbol, receiver: object) {
|
return function get(target: object, key: string | symbol, receiver: object) {
|
||||||
if (isArray(target) && hasOwn(arrayInstrumentations, key)) {
|
const targetIsArray = isArray(target)
|
||||||
|
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
|
||||||
return Reflect.get(arrayInstrumentations, key, receiver)
|
return Reflect.get(arrayInstrumentations, key, receiver)
|
||||||
}
|
}
|
||||||
const res = Reflect.get(target, key, receiver)
|
const res = Reflect.get(target, key, receiver)
|
||||||
|
|
||||||
if (isSymbol(key) && builtInSymbols.has(key)) {
|
if (isSymbol(key) && builtInSymbols.has(key)) {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shallow) {
|
if (shallow) {
|
||||||
track(target, TrackOpTypes.GET, key)
|
!isReadonly && track(target, TrackOpTypes.GET, key)
|
||||||
// TODO strict mode that returns a shallow-readonly version of the value
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
// ref unwrapping, only for Objects, not for Arrays.
|
|
||||||
if (isRef(res) && !isArray(target)) {
|
if (isRef(res)) {
|
||||||
return res.value
|
if (targetIsArray) {
|
||||||
|
!isReadonly && track(target, TrackOpTypes.GET, key)
|
||||||
|
return res
|
||||||
|
} else {
|
||||||
|
// ref unwrapping, only for Objects, not for Arrays.
|
||||||
|
return res.value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
track(target, TrackOpTypes.GET, key)
|
|
||||||
|
!isReadonly && track(target, TrackOpTypes.GET, key)
|
||||||
return isObject(res)
|
return isObject(res)
|
||||||
? isReadonly
|
? isReadonly
|
||||||
? // need to lazy access readonly and reactive here to avoid
|
? // need to lazy access readonly and reactive here to avoid
|
||||||
@ -64,27 +72,15 @@ function createGetter(isReadonly = false, shallow = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const set = /*#__PURE__*/ createSetter()
|
const set = /*#__PURE__*/ createSetter()
|
||||||
const shallowReactiveSet = /*#__PURE__*/ createSetter(false, true)
|
const shallowSet = /*#__PURE__*/ createSetter(true)
|
||||||
const readonlySet = /*#__PURE__*/ createSetter(true)
|
|
||||||
const shallowReadonlySet = /*#__PURE__*/ createSetter(true, true)
|
|
||||||
|
|
||||||
function createSetter(isReadonly = false, shallow = false) {
|
function createSetter(shallow = false) {
|
||||||
return function set(
|
return function set(
|
||||||
target: object,
|
target: object,
|
||||||
key: string | symbol,
|
key: string | symbol,
|
||||||
value: unknown,
|
value: unknown,
|
||||||
receiver: object
|
receiver: object
|
||||||
): boolean {
|
): boolean {
|
||||||
if (isReadonly && LOCKED) {
|
|
||||||
if (__DEV__) {
|
|
||||||
console.warn(
|
|
||||||
`Set operation on key "${String(key)}" failed: target is readonly.`,
|
|
||||||
target
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldValue = (target as any)[key]
|
const oldValue = (target as any)[key]
|
||||||
if (!shallow) {
|
if (!shallow) {
|
||||||
value = toRaw(value)
|
value = toRaw(value)
|
||||||
@ -141,30 +137,32 @@ export const mutableHandlers: ProxyHandler<object> = {
|
|||||||
|
|
||||||
export const readonlyHandlers: ProxyHandler<object> = {
|
export const readonlyHandlers: ProxyHandler<object> = {
|
||||||
get: readonlyGet,
|
get: readonlyGet,
|
||||||
set: readonlySet,
|
|
||||||
has,
|
has,
|
||||||
ownKeys,
|
ownKeys,
|
||||||
deleteProperty(target: object, key: string | symbol): boolean {
|
set(target, key) {
|
||||||
if (LOCKED) {
|
if (__DEV__) {
|
||||||
if (__DEV__) {
|
console.warn(
|
||||||
console.warn(
|
`Set operation on key "${String(key)}" failed: target is readonly.`,
|
||||||
`Delete operation on key "${String(
|
target
|
||||||
key
|
)
|
||||||
)}" failed: target is readonly.`,
|
|
||||||
target
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return deleteProperty(target, key)
|
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
deleteProperty(target, key) {
|
||||||
|
if (__DEV__) {
|
||||||
|
console.warn(
|
||||||
|
`Delete operation on key "${String(key)}" failed: target is readonly.`,
|
||||||
|
target
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const shallowReactiveHandlers: ProxyHandler<object> = {
|
export const shallowReactiveHandlers: ProxyHandler<object> = {
|
||||||
...mutableHandlers,
|
...mutableHandlers,
|
||||||
get: shallowReactiveGet,
|
get: shallowGet,
|
||||||
set: shallowReactiveSet
|
set: shallowSet
|
||||||
}
|
}
|
||||||
|
|
||||||
// Props handlers are special in the sense that it should not unwrap top-level
|
// Props handlers are special in the sense that it should not unwrap top-level
|
||||||
@ -172,6 +170,5 @@ export const shallowReactiveHandlers: ProxyHandler<object> = {
|
|||||||
// retain the reactivity of the normal readonly object.
|
// retain the reactivity of the normal readonly object.
|
||||||
export const shallowReadonlyHandlers: ProxyHandler<object> = {
|
export const shallowReadonlyHandlers: ProxyHandler<object> = {
|
||||||
...readonlyHandlers,
|
...readonlyHandlers,
|
||||||
get: shallowReadonlyGet,
|
get: shallowReadonlyGet
|
||||||
set: shallowReadonlySet
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { toRaw, reactive, readonly } from './reactive'
|
import { toRaw, reactive, readonly } from './reactive'
|
||||||
import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect'
|
import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect'
|
||||||
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
||||||
import { LOCKED } from './lock'
|
|
||||||
import {
|
import {
|
||||||
isObject,
|
isObject,
|
||||||
capitalize,
|
capitalize,
|
||||||
@ -142,7 +141,7 @@ function createForEach(isReadonly: boolean) {
|
|||||||
const observed = this
|
const observed = this
|
||||||
const target = toRaw(observed)
|
const target = toRaw(observed)
|
||||||
const wrap = isReadonly ? toReadonly : toReactive
|
const wrap = isReadonly ? toReadonly : toReactive
|
||||||
track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
|
!isReadonly && track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
|
||||||
// important: create sure the callback is
|
// important: create sure the callback is
|
||||||
// 1. invoked with the reactive map as `this` and 3rd arg
|
// 1. invoked with the reactive map as `this` and 3rd arg
|
||||||
// 2. the value received should be a corresponding reactive/readonly.
|
// 2. the value received should be a corresponding reactive/readonly.
|
||||||
@ -161,11 +160,12 @@ function createIterableMethod(method: string | symbol, isReadonly: boolean) {
|
|||||||
const isKeyOnly = method === 'keys' && isMap
|
const isKeyOnly = method === 'keys' && isMap
|
||||||
const innerIterator = getProto(target)[method].apply(target, args)
|
const innerIterator = getProto(target)[method].apply(target, args)
|
||||||
const wrap = isReadonly ? toReadonly : toReactive
|
const wrap = isReadonly ? toReadonly : toReactive
|
||||||
track(
|
!isReadonly &&
|
||||||
target,
|
track(
|
||||||
TrackOpTypes.ITERATE,
|
target,
|
||||||
isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
|
TrackOpTypes.ITERATE,
|
||||||
)
|
isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
|
||||||
|
)
|
||||||
// return a wrapped iterator which returns observed versions of the
|
// return a wrapped iterator which returns observed versions of the
|
||||||
// values emitted from the real iterator
|
// values emitted from the real iterator
|
||||||
return {
|
return {
|
||||||
@ -187,23 +187,16 @@ function createIterableMethod(method: string | symbol, isReadonly: boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createReadonlyMethod(
|
function createReadonlyMethod(type: TriggerOpTypes): Function {
|
||||||
method: Function,
|
|
||||||
type: TriggerOpTypes
|
|
||||||
): Function {
|
|
||||||
return function(this: CollectionTypes, ...args: unknown[]) {
|
return function(this: CollectionTypes, ...args: unknown[]) {
|
||||||
if (LOCKED) {
|
if (__DEV__) {
|
||||||
if (__DEV__) {
|
const key = args[0] ? `on key "${args[0]}" ` : ``
|
||||||
const key = args[0] ? `on key "${args[0]}" ` : ``
|
console.warn(
|
||||||
console.warn(
|
`${capitalize(type)} operation ${key}failed: target is readonly.`,
|
||||||
`${capitalize(type)} operation ${key}failed: target is readonly.`,
|
toRaw(this)
|
||||||
toRaw(this)
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
return type === TriggerOpTypes.DELETE ? false : this
|
|
||||||
} else {
|
|
||||||
return method.apply(this, args)
|
|
||||||
}
|
}
|
||||||
|
return type === TriggerOpTypes.DELETE ? false : this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,10 +223,10 @@ const readonlyInstrumentations: Record<string, Function> = {
|
|||||||
return size((this as unknown) as IterableCollections)
|
return size((this as unknown) as IterableCollections)
|
||||||
},
|
},
|
||||||
has,
|
has,
|
||||||
add: createReadonlyMethod(add, TriggerOpTypes.ADD),
|
add: createReadonlyMethod(TriggerOpTypes.ADD),
|
||||||
set: createReadonlyMethod(set, TriggerOpTypes.SET),
|
set: createReadonlyMethod(TriggerOpTypes.SET),
|
||||||
delete: createReadonlyMethod(deleteEntry, TriggerOpTypes.DELETE),
|
delete: createReadonlyMethod(TriggerOpTypes.DELETE),
|
||||||
clear: createReadonlyMethod(clear, TriggerOpTypes.CLEAR),
|
clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
|
||||||
forEach: createForEach(true)
|
forEach: createForEach(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ const targetMap = new WeakMap<any, KeyToDepMap>()
|
|||||||
export interface ReactiveEffect<T = any> {
|
export interface ReactiveEffect<T = any> {
|
||||||
(...args: any[]): T
|
(...args: any[]): T
|
||||||
_isEffect: true
|
_isEffect: true
|
||||||
|
id: number
|
||||||
active: boolean
|
active: boolean
|
||||||
raw: () => T
|
raw: () => T
|
||||||
deps: Array<Dep>
|
deps: Array<Dep>
|
||||||
@ -21,7 +22,7 @@ export interface ReactiveEffect<T = any> {
|
|||||||
export interface ReactiveEffectOptions {
|
export interface ReactiveEffectOptions {
|
||||||
lazy?: boolean
|
lazy?: boolean
|
||||||
computed?: boolean
|
computed?: boolean
|
||||||
scheduler?: (job: () => void) => void
|
scheduler?: (job: ReactiveEffect) => void
|
||||||
onTrack?: (event: DebuggerEvent) => void
|
onTrack?: (event: DebuggerEvent) => void
|
||||||
onTrigger?: (event: DebuggerEvent) => void
|
onTrigger?: (event: DebuggerEvent) => void
|
||||||
onStop?: () => void
|
onStop?: () => void
|
||||||
@ -74,6 +75,8 @@ export function stop(effect: ReactiveEffect) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let uid = 0
|
||||||
|
|
||||||
function createReactiveEffect<T = any>(
|
function createReactiveEffect<T = any>(
|
||||||
fn: (...args: any[]) => T,
|
fn: (...args: any[]) => T,
|
||||||
options: ReactiveEffectOptions
|
options: ReactiveEffectOptions
|
||||||
@ -96,6 +99,7 @@ function createReactiveEffect<T = any>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} as ReactiveEffect
|
} as ReactiveEffect
|
||||||
|
effect.id = uid++
|
||||||
effect._isEffect = true
|
effect._isEffect = true
|
||||||
effect.active = true
|
effect.active = true
|
||||||
effect.raw = fn
|
effect.raw = fn
|
||||||
|
@ -1,4 +1,14 @@
|
|||||||
export { ref, unref, shallowRef, isRef, toRefs, Ref, UnwrapRef } from './ref'
|
export {
|
||||||
|
ref,
|
||||||
|
unref,
|
||||||
|
shallowRef,
|
||||||
|
isRef,
|
||||||
|
toRef,
|
||||||
|
toRefs,
|
||||||
|
customRef,
|
||||||
|
Ref,
|
||||||
|
UnwrapRef
|
||||||
|
} from './ref'
|
||||||
export {
|
export {
|
||||||
reactive,
|
reactive,
|
||||||
isReactive,
|
isReactive,
|
||||||
@ -7,7 +17,6 @@ export {
|
|||||||
isReadonly,
|
isReadonly,
|
||||||
shallowReadonly,
|
shallowReadonly,
|
||||||
toRaw,
|
toRaw,
|
||||||
markReadonly,
|
|
||||||
markNonReactive
|
markNonReactive
|
||||||
} from './reactive'
|
} from './reactive'
|
||||||
export {
|
export {
|
||||||
@ -31,5 +40,4 @@ export {
|
|||||||
ReactiveEffectOptions,
|
ReactiveEffectOptions,
|
||||||
DebuggerEvent
|
DebuggerEvent
|
||||||
} from './effect'
|
} from './effect'
|
||||||
export { lock, unlock } from './lock'
|
|
||||||
export { TrackOpTypes, TriggerOpTypes } from './operations'
|
export { TrackOpTypes, TriggerOpTypes } from './operations'
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
// global immutability lock
|
|
||||||
export let LOCKED = true
|
|
||||||
|
|
||||||
export function lock() {
|
|
||||||
LOCKED = true
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unlock() {
|
|
||||||
LOCKED = false
|
|
||||||
}
|
|
@ -2,14 +2,14 @@ import { isObject, toRawType } from '@vue/shared'
|
|||||||
import {
|
import {
|
||||||
mutableHandlers,
|
mutableHandlers,
|
||||||
readonlyHandlers,
|
readonlyHandlers,
|
||||||
shallowReadonlyHandlers,
|
shallowReactiveHandlers,
|
||||||
shallowReactiveHandlers
|
shallowReadonlyHandlers
|
||||||
} from './baseHandlers'
|
} from './baseHandlers'
|
||||||
import {
|
import {
|
||||||
mutableCollectionHandlers,
|
mutableCollectionHandlers,
|
||||||
readonlyCollectionHandlers
|
readonlyCollectionHandlers
|
||||||
} from './collectionHandlers'
|
} from './collectionHandlers'
|
||||||
import { UnwrapRef, Ref, isRef } from './ref'
|
import { UnwrapRef, Ref } from './ref'
|
||||||
import { makeMap } from '@vue/shared'
|
import { makeMap } from '@vue/shared'
|
||||||
|
|
||||||
// WeakMaps that store {raw <-> observed} pairs.
|
// WeakMaps that store {raw <-> observed} pairs.
|
||||||
@ -20,7 +20,6 @@ const readonlyToRaw = new WeakMap<any, any>()
|
|||||||
|
|
||||||
// WeakSets for values that are marked readonly or non-reactive during
|
// WeakSets for values that are marked readonly or non-reactive during
|
||||||
// observable creation.
|
// observable creation.
|
||||||
const readonlyValues = new WeakSet<any>()
|
|
||||||
const nonReactiveValues = new WeakSet<any>()
|
const nonReactiveValues = new WeakSet<any>()
|
||||||
|
|
||||||
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
|
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
|
||||||
@ -47,13 +46,6 @@ export function reactive(target: object) {
|
|||||||
if (readonlyToRaw.has(target)) {
|
if (readonlyToRaw.has(target)) {
|
||||||
return target
|
return target
|
||||||
}
|
}
|
||||||
// target is explicitly marked as readonly by user
|
|
||||||
if (readonlyValues.has(target)) {
|
|
||||||
return readonly(target)
|
|
||||||
}
|
|
||||||
if (isRef(target)) {
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
return createReactiveObject(
|
return createReactiveObject(
|
||||||
target,
|
target,
|
||||||
rawToReactive,
|
rawToReactive,
|
||||||
@ -63,14 +55,22 @@ export function reactive(target: object) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return a reactive-copy of the original object, where only the root level
|
||||||
|
// properties are reactive, and does NOT unwrap refs nor recursively convert
|
||||||
|
// returned properties.
|
||||||
|
export function shallowReactive<T extends object>(target: T): T {
|
||||||
|
return createReactiveObject(
|
||||||
|
target,
|
||||||
|
rawToReactive,
|
||||||
|
reactiveToRaw,
|
||||||
|
shallowReactiveHandlers,
|
||||||
|
mutableCollectionHandlers
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function readonly<T extends object>(
|
export function readonly<T extends object>(
|
||||||
target: T
|
target: T
|
||||||
): Readonly<UnwrapNestedRefs<T>> {
|
): Readonly<UnwrapNestedRefs<T>> {
|
||||||
// value is a mutable observable, retrieve its original and return
|
|
||||||
// a readonly version.
|
|
||||||
if (reactiveToRaw.has(target)) {
|
|
||||||
target = reactiveToRaw.get(target)
|
|
||||||
}
|
|
||||||
return createReactiveObject(
|
return createReactiveObject(
|
||||||
target,
|
target,
|
||||||
rawToReadonly,
|
rawToReadonly,
|
||||||
@ -96,19 +96,6 @@ export function shallowReadonly<T extends object>(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a reactive-copy of the original object, where only the root level
|
|
||||||
// properties are reactive, and does NOT unwrap refs nor recursively convert
|
|
||||||
// returned properties.
|
|
||||||
export function shallowReactive<T extends object>(target: T): T {
|
|
||||||
return createReactiveObject(
|
|
||||||
target,
|
|
||||||
rawToReactive,
|
|
||||||
reactiveToRaw,
|
|
||||||
shallowReactiveHandlers,
|
|
||||||
mutableCollectionHandlers
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function createReactiveObject(
|
function createReactiveObject(
|
||||||
target: unknown,
|
target: unknown,
|
||||||
toProxy: WeakMap<any, any>,
|
toProxy: WeakMap<any, any>,
|
||||||
@ -153,12 +140,8 @@ export function isReadonly(value: unknown): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function toRaw<T>(observed: T): T {
|
export function toRaw<T>(observed: T): T {
|
||||||
return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed
|
observed = readonlyToRaw.get(observed) || observed
|
||||||
}
|
return reactiveToRaw.get(observed) || observed
|
||||||
|
|
||||||
export function markReadonly<T>(value: T): T {
|
|
||||||
readonlyValues.add(value)
|
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markNonReactive<T extends object>(value: T): T {
|
export function markNonReactive<T extends object>(value: T): T {
|
||||||
|
@ -70,6 +70,31 @@ export function unref<T>(ref: T): T extends Ref<infer V> ? V : T {
|
|||||||
return isRef(ref) ? (ref.value as any) : ref
|
return isRef(ref) ? (ref.value as any) : ref
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CustomRefFactory<T> = (
|
||||||
|
track: () => void,
|
||||||
|
trigger: () => void
|
||||||
|
) => {
|
||||||
|
get: () => T
|
||||||
|
set: (value: T) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
|
||||||
|
const { get, set } = factory(
|
||||||
|
() => track(r, TrackOpTypes.GET, 'value'),
|
||||||
|
() => trigger(r, TriggerOpTypes.SET, 'value')
|
||||||
|
)
|
||||||
|
const r = {
|
||||||
|
_isRef: true,
|
||||||
|
get value() {
|
||||||
|
return get()
|
||||||
|
},
|
||||||
|
set value(v) {
|
||||||
|
set(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r as any
|
||||||
|
}
|
||||||
|
|
||||||
export function toRefs<T extends object>(
|
export function toRefs<T extends object>(
|
||||||
object: T
|
object: T
|
||||||
): { [K in keyof T]: Ref<T[K]> } {
|
): { [K in keyof T]: Ref<T[K]> } {
|
||||||
@ -78,12 +103,12 @@ export function toRefs<T extends object>(
|
|||||||
}
|
}
|
||||||
const ret: any = {}
|
const ret: any = {}
|
||||||
for (const key in object) {
|
for (const key in object) {
|
||||||
ret[key] = toProxyRef(object, key)
|
ret[key] = toRef(object, key)
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
function toProxyRef<T extends object, K extends keyof T>(
|
export function toRef<T extends object, K extends keyof T>(
|
||||||
object: T,
|
object: T,
|
||||||
key: K
|
key: K
|
||||||
): Ref<T[K]> {
|
): Ref<T[K]> {
|
||||||
|
@ -175,9 +175,17 @@ describe('component props', () => {
|
|||||||
expect(proxy.foo).toBe(2)
|
expect(proxy.foo).toBe(2)
|
||||||
expect(proxy.bar).toEqual({ a: 1 })
|
expect(proxy.bar).toEqual({ a: 1 })
|
||||||
|
|
||||||
render(h(Comp, { foo: undefined, bar: { b: 2 } }), root)
|
render(h(Comp, { bar: { b: 2 } }), root)
|
||||||
expect(proxy.foo).toBe(1)
|
expect(proxy.foo).toBe(1)
|
||||||
expect(proxy.bar).toEqual({ b: 2 })
|
expect(proxy.bar).toEqual({ b: 2 })
|
||||||
|
|
||||||
|
render(h(Comp, { foo: 3, bar: { b: 3 } }), root)
|
||||||
|
expect(proxy.foo).toBe(3)
|
||||||
|
expect(proxy.bar).toEqual({ b: 3 })
|
||||||
|
|
||||||
|
render(h(Comp, { bar: { b: 4 } }), root)
|
||||||
|
expect(proxy.foo).toBe(1)
|
||||||
|
expect(proxy.bar).toEqual({ b: 4 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('optimized props updates', async () => {
|
test('optimized props updates', async () => {
|
||||||
|
@ -3,7 +3,8 @@ import {
|
|||||||
render,
|
render,
|
||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
nodeOps,
|
nodeOps,
|
||||||
createApp
|
createApp,
|
||||||
|
shallowReadonly
|
||||||
} from '@vue/runtime-test'
|
} from '@vue/runtime-test'
|
||||||
import { mockWarn } from '@vue/shared'
|
import { mockWarn } from '@vue/shared'
|
||||||
import { ComponentInternalInstance } from '../src/component'
|
import { ComponentInternalInstance } from '../src/component'
|
||||||
@ -85,10 +86,10 @@ describe('component: proxy', () => {
|
|||||||
}
|
}
|
||||||
render(h(Comp), nodeOps.createElement('div'))
|
render(h(Comp), nodeOps.createElement('div'))
|
||||||
expect(instanceProxy.$data).toBe(instance!.data)
|
expect(instanceProxy.$data).toBe(instance!.data)
|
||||||
expect(instanceProxy.$props).toBe(instance!.props)
|
expect(instanceProxy.$props).toBe(shallowReadonly(instance!.props))
|
||||||
expect(instanceProxy.$attrs).toBe(instance!.attrs)
|
expect(instanceProxy.$attrs).toBe(shallowReadonly(instance!.attrs))
|
||||||
expect(instanceProxy.$slots).toBe(instance!.slots)
|
expect(instanceProxy.$slots).toBe(shallowReadonly(instance!.slots))
|
||||||
expect(instanceProxy.$refs).toBe(instance!.refs)
|
expect(instanceProxy.$refs).toBe(shallowReadonly(instance!.refs))
|
||||||
expect(instanceProxy.$parent).toBe(
|
expect(instanceProxy.$parent).toBe(
|
||||||
instance!.parent && instance!.parent.proxy
|
instance!.parent && instance!.parent.proxy
|
||||||
)
|
)
|
||||||
|
@ -262,4 +262,20 @@ describe('scheduler', () => {
|
|||||||
// job2 should be called only once
|
// job2 should be called only once
|
||||||
expect(calls).toEqual(['job1', 'job2', 'job3', 'job4'])
|
expect(calls).toEqual(['job1', 'job2', 'job3', 'job4'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('sort job based on id', async () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
const job1 = () => calls.push('job1')
|
||||||
|
// job1 has no id
|
||||||
|
const job2 = () => calls.push('job2')
|
||||||
|
job2.id = 2
|
||||||
|
const job3 = () => calls.push('job3')
|
||||||
|
job3.id = 1
|
||||||
|
|
||||||
|
queueJob(job1)
|
||||||
|
queueJob(job2)
|
||||||
|
queueJob(job3)
|
||||||
|
await nextTick()
|
||||||
|
expect(calls).toEqual(['job3', 'job2', 'job1'])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -3,7 +3,8 @@ import {
|
|||||||
reactive,
|
reactive,
|
||||||
ReactiveEffect,
|
ReactiveEffect,
|
||||||
pauseTracking,
|
pauseTracking,
|
||||||
resetTracking
|
resetTracking,
|
||||||
|
shallowReadonly
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
import {
|
import {
|
||||||
ComponentPublicInstance,
|
ComponentPublicInstance,
|
||||||
@ -347,7 +348,7 @@ function setupStatefulComponent(
|
|||||||
setup,
|
setup,
|
||||||
instance,
|
instance,
|
||||||
ErrorCodes.SETUP_FUNCTION,
|
ErrorCodes.SETUP_FUNCTION,
|
||||||
[instance.props, setupContext]
|
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
|
||||||
)
|
)
|
||||||
resetTracking()
|
resetTracking()
|
||||||
currentInstance = null
|
currentInstance = null
|
||||||
@ -479,17 +480,6 @@ function finishComponentSetup(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const slotsHandlers: ProxyHandler<InternalSlots> = {
|
|
||||||
set: () => {
|
|
||||||
warn(`setupContext.slots is readonly.`)
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
deleteProperty: () => {
|
|
||||||
warn(`setupContext.slots is readonly.`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const attrHandlers: ProxyHandler<Data> = {
|
const attrHandlers: ProxyHandler<Data> = {
|
||||||
get: (target, key: string) => {
|
get: (target, key: string) => {
|
||||||
markAttrsAccessed()
|
markAttrsAccessed()
|
||||||
@ -514,7 +504,7 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext {
|
|||||||
return new Proxy(instance.attrs, attrHandlers)
|
return new Proxy(instance.attrs, attrHandlers)
|
||||||
},
|
},
|
||||||
get slots() {
|
get slots() {
|
||||||
return new Proxy(instance.slots, slotsHandlers)
|
return shallowReadonly(instance.slots)
|
||||||
},
|
},
|
||||||
get emit() {
|
get emit() {
|
||||||
return (event: string, ...args: any[]) => instance.emit(event, ...args)
|
return (event: string, ...args: any[]) => instance.emit(event, ...args)
|
||||||
|
@ -15,7 +15,8 @@ import {
|
|||||||
isArray,
|
isArray,
|
||||||
EMPTY_OBJ,
|
EMPTY_OBJ,
|
||||||
NOOP,
|
NOOP,
|
||||||
hasOwn
|
hasOwn,
|
||||||
|
isPromise
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import { computed } from './apiComputed'
|
import { computed } from './apiComputed'
|
||||||
import { watch, WatchOptions, WatchCallback } from './apiWatch'
|
import { watch, WatchOptions, WatchCallback } from './apiWatch'
|
||||||
@ -316,6 +317,13 @@ export function applyOptions(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
const data = dataOptions.call(ctx, ctx)
|
const data = dataOptions.call(ctx, ctx)
|
||||||
|
if (__DEV__ && isPromise(data)) {
|
||||||
|
warn(
|
||||||
|
`data() returned a Promise - note data() cannot be async; If you ` +
|
||||||
|
`intend to perform data fetching before component renders, use ` +
|
||||||
|
`async setup() + <Suspense>.`
|
||||||
|
)
|
||||||
|
}
|
||||||
if (!isObject(data)) {
|
if (!isObject(data)) {
|
||||||
__DEV__ && warn(`data() should return an object.`)
|
__DEV__ && warn(`data() should return an object.`)
|
||||||
} else if (instance.data === EMPTY_OBJ) {
|
} else if (instance.data === EMPTY_OBJ) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { toRaw, lock, unlock, shallowReadonly } from '@vue/reactivity'
|
import { toRaw, shallowReactive } from '@vue/reactivity'
|
||||||
import {
|
import {
|
||||||
EMPTY_OBJ,
|
EMPTY_OBJ,
|
||||||
camelize,
|
camelize,
|
||||||
@ -114,7 +114,7 @@ export function initProps(
|
|||||||
|
|
||||||
if (isStateful) {
|
if (isStateful) {
|
||||||
// stateful
|
// stateful
|
||||||
instance.props = isSSR ? props : shallowReadonly(props)
|
instance.props = isSSR ? props : shallowReactive(props)
|
||||||
} else {
|
} else {
|
||||||
if (!options) {
|
if (!options) {
|
||||||
// functional w/ optional props, props === attrs
|
// functional w/ optional props, props === attrs
|
||||||
@ -132,9 +132,6 @@ export function updateProps(
|
|||||||
rawProps: Data | null,
|
rawProps: Data | null,
|
||||||
optimized: boolean
|
optimized: boolean
|
||||||
) {
|
) {
|
||||||
// allow mutation of propsProxy (which is readonly by default)
|
|
||||||
unlock()
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
props,
|
props,
|
||||||
attrs,
|
attrs,
|
||||||
@ -186,7 +183,16 @@ export function updateProps(
|
|||||||
// and converted to camelCase (#955)
|
// and converted to camelCase (#955)
|
||||||
((kebabKey = hyphenate(key)) === key || !hasOwn(rawProps, kebabKey)))
|
((kebabKey = hyphenate(key)) === key || !hasOwn(rawProps, kebabKey)))
|
||||||
) {
|
) {
|
||||||
delete props[key]
|
if (options) {
|
||||||
|
props[key] = resolvePropValue(
|
||||||
|
options,
|
||||||
|
rawProps || EMPTY_OBJ,
|
||||||
|
key,
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
delete props[key]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const key in attrs) {
|
for (const key in attrs) {
|
||||||
@ -196,9 +202,6 @@ export function updateProps(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// lock readonly
|
|
||||||
lock()
|
|
||||||
|
|
||||||
if (__DEV__ && rawOptions && rawProps) {
|
if (__DEV__ && rawOptions && rawProps) {
|
||||||
validateProps(props, rawOptions)
|
validateProps(props, rawOptions)
|
||||||
}
|
}
|
||||||
@ -250,25 +253,24 @@ function resolvePropValue(
|
|||||||
key: string,
|
key: string,
|
||||||
value: unknown
|
value: unknown
|
||||||
) {
|
) {
|
||||||
let opt = options[key]
|
const opt = options[key]
|
||||||
if (opt == null) {
|
if (opt != null) {
|
||||||
return value
|
const hasDefault = hasOwn(opt, 'default')
|
||||||
}
|
// default values
|
||||||
const hasDefault = hasOwn(opt, 'default')
|
if (hasDefault && value === undefined) {
|
||||||
// default values
|
const defaultValue = opt.default
|
||||||
if (hasDefault && value === undefined) {
|
value = isFunction(defaultValue) ? defaultValue() : defaultValue
|
||||||
const defaultValue = opt.default
|
}
|
||||||
value = isFunction(defaultValue) ? defaultValue() : defaultValue
|
// boolean casting
|
||||||
}
|
if (opt[BooleanFlags.shouldCast]) {
|
||||||
// boolean casting
|
if (!hasOwn(props, key) && !hasDefault) {
|
||||||
if (opt[BooleanFlags.shouldCast]) {
|
value = false
|
||||||
if (!hasOwn(props, key) && !hasDefault) {
|
} else if (
|
||||||
value = false
|
opt[BooleanFlags.shouldCastTrue] &&
|
||||||
} else if (
|
(value === '' || value === hyphenate(key))
|
||||||
opt[BooleanFlags.shouldCastTrue] &&
|
) {
|
||||||
(value === '' || value === hyphenate(key))
|
value = true
|
||||||
) {
|
}
|
||||||
value = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
|
@ -2,7 +2,12 @@ import { ComponentInternalInstance, Data } from './component'
|
|||||||
import { nextTick, queueJob } from './scheduler'
|
import { nextTick, queueJob } from './scheduler'
|
||||||
import { instanceWatch } from './apiWatch'
|
import { instanceWatch } from './apiWatch'
|
||||||
import { EMPTY_OBJ, hasOwn, isGloballyWhitelisted, NOOP } from '@vue/shared'
|
import { EMPTY_OBJ, hasOwn, isGloballyWhitelisted, NOOP } from '@vue/shared'
|
||||||
import { ReactiveEffect, UnwrapRef, toRaw } from '@vue/reactivity'
|
import {
|
||||||
|
ReactiveEffect,
|
||||||
|
UnwrapRef,
|
||||||
|
toRaw,
|
||||||
|
shallowReadonly
|
||||||
|
} from '@vue/reactivity'
|
||||||
import {
|
import {
|
||||||
ExtractComputedReturns,
|
ExtractComputedReturns,
|
||||||
ComponentOptionsBase,
|
ComponentOptionsBase,
|
||||||
@ -36,8 +41,8 @@ export type ComponentPublicInstance<
|
|||||||
$attrs: Data
|
$attrs: Data
|
||||||
$refs: Data
|
$refs: Data
|
||||||
$slots: Slots
|
$slots: Slots
|
||||||
$root: ComponentInternalInstance | null
|
$root: ComponentPublicInstance | null
|
||||||
$parent: ComponentInternalInstance | null
|
$parent: ComponentPublicInstance | null
|
||||||
$emit: EmitFn<E>
|
$emit: EmitFn<E>
|
||||||
$el: any
|
$el: any
|
||||||
$options: ComponentOptionsBase<P, B, D, C, M, E>
|
$options: ComponentOptionsBase<P, B, D, C, M, E>
|
||||||
@ -57,10 +62,10 @@ const publicPropertiesMap: Record<
|
|||||||
$: i => i,
|
$: i => i,
|
||||||
$el: i => i.vnode.el,
|
$el: i => i.vnode.el,
|
||||||
$data: i => i.data,
|
$data: i => i.data,
|
||||||
$props: i => i.props,
|
$props: i => (__DEV__ ? shallowReadonly(i.props) : i.props),
|
||||||
$attrs: i => i.attrs,
|
$attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs),
|
||||||
$slots: i => i.slots,
|
$slots: i => (__DEV__ ? shallowReadonly(i.slots) : i.slots),
|
||||||
$refs: i => i.refs,
|
$refs: i => (__DEV__ ? shallowReadonly(i.refs) : i.refs),
|
||||||
$parent: i => i.parent && i.parent.proxy,
|
$parent: i => i.parent && i.parent.proxy,
|
||||||
$root: i => i.root && i.root.proxy,
|
$root: i => i.root && i.root.proxy,
|
||||||
$emit: i => i.emit,
|
$emit: i => i.emit,
|
||||||
|
@ -2,20 +2,21 @@
|
|||||||
|
|
||||||
export const version = __VERSION__
|
export const version = __VERSION__
|
||||||
export {
|
export {
|
||||||
effect,
|
|
||||||
ref,
|
ref,
|
||||||
unref,
|
unref,
|
||||||
shallowRef,
|
shallowRef,
|
||||||
isRef,
|
isRef,
|
||||||
|
toRef,
|
||||||
toRefs,
|
toRefs,
|
||||||
|
customRef,
|
||||||
reactive,
|
reactive,
|
||||||
isReactive,
|
isReactive,
|
||||||
readonly,
|
readonly,
|
||||||
isReadonly,
|
isReadonly,
|
||||||
shallowReactive,
|
shallowReactive,
|
||||||
toRaw,
|
shallowReadonly,
|
||||||
markReadonly,
|
markNonReactive,
|
||||||
markNonReactive
|
toRaw
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
export { computed } from './apiComputed'
|
export { computed } from './apiComputed'
|
||||||
export { watch, watchEffect } from './apiWatch'
|
export { watch, watchEffect } from './apiWatch'
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
|
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
|
||||||
import { isArray } from '@vue/shared'
|
import { isArray } from '@vue/shared'
|
||||||
|
|
||||||
const queue: (Function | null)[] = []
|
export interface Job {
|
||||||
|
(): void
|
||||||
|
id?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const queue: (Job | null)[] = []
|
||||||
const postFlushCbs: Function[] = []
|
const postFlushCbs: Function[] = []
|
||||||
const p = Promise.resolve()
|
const p = Promise.resolve()
|
||||||
|
|
||||||
@ -9,20 +14,20 @@ let isFlushing = false
|
|||||||
let isFlushPending = false
|
let isFlushPending = false
|
||||||
|
|
||||||
const RECURSION_LIMIT = 100
|
const RECURSION_LIMIT = 100
|
||||||
type CountMap = Map<Function, number>
|
type CountMap = Map<Job | Function, number>
|
||||||
|
|
||||||
export function nextTick(fn?: () => void): Promise<void> {
|
export function nextTick(fn?: () => void): Promise<void> {
|
||||||
return fn ? p.then(fn) : p
|
return fn ? p.then(fn) : p
|
||||||
}
|
}
|
||||||
|
|
||||||
export function queueJob(job: () => void) {
|
export function queueJob(job: Job) {
|
||||||
if (!queue.includes(job)) {
|
if (!queue.includes(job)) {
|
||||||
queue.push(job)
|
queue.push(job)
|
||||||
queueFlush()
|
queueFlush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invalidateJob(job: () => void) {
|
export function invalidateJob(job: Job) {
|
||||||
const i = queue.indexOf(job)
|
const i = queue.indexOf(job)
|
||||||
if (i > -1) {
|
if (i > -1) {
|
||||||
queue[i] = null
|
queue[i] = null
|
||||||
@ -45,11 +50,9 @@ function queueFlush() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dedupe = (cbs: Function[]): Function[] => [...new Set(cbs)]
|
|
||||||
|
|
||||||
export function flushPostFlushCbs(seen?: CountMap) {
|
export function flushPostFlushCbs(seen?: CountMap) {
|
||||||
if (postFlushCbs.length) {
|
if (postFlushCbs.length) {
|
||||||
const cbs = dedupe(postFlushCbs)
|
const cbs = [...new Set(postFlushCbs)]
|
||||||
postFlushCbs.length = 0
|
postFlushCbs.length = 0
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
seen = seen || new Map()
|
seen = seen || new Map()
|
||||||
@ -63,6 +66,8 @@ export function flushPostFlushCbs(seen?: CountMap) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getId = (job: Job) => (job.id == null ? Infinity : job.id)
|
||||||
|
|
||||||
function flushJobs(seen?: CountMap) {
|
function flushJobs(seen?: CountMap) {
|
||||||
isFlushPending = false
|
isFlushPending = false
|
||||||
isFlushing = true
|
isFlushing = true
|
||||||
@ -70,6 +75,18 @@ function flushJobs(seen?: CountMap) {
|
|||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
seen = seen || new Map()
|
seen = seen || new Map()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort queue before flush.
|
||||||
|
// This ensures that:
|
||||||
|
// 1. Components are updated from parent to child. (because parent is always
|
||||||
|
// created before the child so its render effect will have smaller
|
||||||
|
// priority number)
|
||||||
|
// 2. If a component is unmounted during a parent component's update,
|
||||||
|
// its update can be skipped.
|
||||||
|
// Jobs can never be null before flush starts, since they are only invalidated
|
||||||
|
// during execution of another flushed job.
|
||||||
|
queue.sort((a, b) => getId(a!) - getId(b!))
|
||||||
|
|
||||||
while ((job = queue.shift()) !== undefined) {
|
while ((job = queue.shift()) !== undefined) {
|
||||||
if (job === null) {
|
if (job === null) {
|
||||||
continue
|
continue
|
||||||
@ -88,7 +105,7 @@ function flushJobs(seen?: CountMap) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkRecursiveUpdates(seen: CountMap, fn: Function) {
|
function checkRecursiveUpdates(seen: CountMap, fn: Job | Function) {
|
||||||
if (!seen.has(fn)) {
|
if (!seen.has(fn)) {
|
||||||
seen.set(fn, 1)
|
seen.set(fn, 1)
|
||||||
} else {
|
} else {
|
||||||
|
@ -17,7 +17,7 @@ import {
|
|||||||
ClassComponent
|
ClassComponent
|
||||||
} from './component'
|
} from './component'
|
||||||
import { RawSlots } from './componentSlots'
|
import { RawSlots } from './componentSlots'
|
||||||
import { isReactive, Ref } from '@vue/reactivity'
|
import { isReactive, Ref, toRaw } from '@vue/reactivity'
|
||||||
import { AppContext } from './apiCreateApp'
|
import { AppContext } from './apiCreateApp'
|
||||||
import {
|
import {
|
||||||
SuspenseImpl,
|
SuspenseImpl,
|
||||||
@ -292,6 +292,22 @@ function _createVNode(
|
|||||||
? ShapeFlags.FUNCTIONAL_COMPONENT
|
? ShapeFlags.FUNCTIONAL_COMPONENT
|
||||||
: 0
|
: 0
|
||||||
|
|
||||||
|
if (
|
||||||
|
__DEV__ &&
|
||||||
|
shapeFlag & ShapeFlags.STATEFUL_COMPONENT &&
|
||||||
|
isReactive(type)
|
||||||
|
) {
|
||||||
|
type = toRaw(type)
|
||||||
|
warn(
|
||||||
|
`Vue received a Component which was made a reactive object. This can ` +
|
||||||
|
`lead to unnecessary performance overhead, and should be avoided by ` +
|
||||||
|
`marking the component with \`markNonReactive\` or using \`shallowRef\` ` +
|
||||||
|
`instead of \`ref\`.`,
|
||||||
|
`\nComponent that was made reactive: `,
|
||||||
|
type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const vnode: VNode = {
|
const vnode: VNode = {
|
||||||
_isVNode: true,
|
_isVNode: true,
|
||||||
type,
|
type,
|
||||||
|
@ -134,4 +134,18 @@ describe(`runtime-dom: events patching`, () => {
|
|||||||
expect(fn).toHaveBeenCalledTimes(1)
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
expect(fn2).toHaveBeenCalledWith(event)
|
expect(fn2).toHaveBeenCalledWith(event)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support stopImmediatePropagation on multiple listeners', async () => {
|
||||||
|
const el = document.createElement('div')
|
||||||
|
const event = new Event('click')
|
||||||
|
const fn1 = jest.fn((e: Event) => {
|
||||||
|
e.stopImmediatePropagation()
|
||||||
|
})
|
||||||
|
const fn2 = jest.fn()
|
||||||
|
patchProp(el, 'onClick', null, [fn1, fn2])
|
||||||
|
el.dispatchEvent(event)
|
||||||
|
await timeout()
|
||||||
|
expect(fn1).toHaveBeenCalledTimes(1)
|
||||||
|
expect(fn2).toHaveBeenCalledTimes(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -13,7 +13,6 @@ import {
|
|||||||
VNode,
|
VNode,
|
||||||
warn,
|
warn,
|
||||||
resolveTransitionHooks,
|
resolveTransitionHooks,
|
||||||
toRaw,
|
|
||||||
useTransitionState,
|
useTransitionState,
|
||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
setTransitionHooks,
|
setTransitionHooks,
|
||||||
@ -21,6 +20,7 @@ import {
|
|||||||
onUpdated,
|
onUpdated,
|
||||||
SetupContext
|
SetupContext
|
||||||
} from '@vue/runtime-core'
|
} from '@vue/runtime-core'
|
||||||
|
import { toRaw } from '@vue/reactivity'
|
||||||
|
|
||||||
interface Position {
|
interface Position {
|
||||||
top: number
|
top: number
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { EMPTY_OBJ } from '@vue/shared'
|
import { EMPTY_OBJ, isArray } from '@vue/shared'
|
||||||
import {
|
import {
|
||||||
ComponentInternalInstance,
|
ComponentInternalInstance,
|
||||||
callWithAsyncErrorHandling
|
callWithAsyncErrorHandling
|
||||||
@ -130,7 +130,7 @@ function createInvoker(
|
|||||||
// AFTER it was attached.
|
// AFTER it was attached.
|
||||||
if (e.timeStamp >= invoker.lastUpdated - 1) {
|
if (e.timeStamp >= invoker.lastUpdated - 1) {
|
||||||
callWithAsyncErrorHandling(
|
callWithAsyncErrorHandling(
|
||||||
invoker.value,
|
patchStopImmediatePropagation(e, invoker.value),
|
||||||
instance,
|
instance,
|
||||||
ErrorCodes.NATIVE_EVENT_HANDLER,
|
ErrorCodes.NATIVE_EVENT_HANDLER,
|
||||||
[e]
|
[e]
|
||||||
@ -142,3 +142,19 @@ function createInvoker(
|
|||||||
invoker.lastUpdated = getNow()
|
invoker.lastUpdated = getNow()
|
||||||
return invoker
|
return invoker
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function patchStopImmediatePropagation(
|
||||||
|
e: Event,
|
||||||
|
value: EventValue
|
||||||
|
): EventValue {
|
||||||
|
if (isArray(value)) {
|
||||||
|
const originalStop = e.stopImmediatePropagation
|
||||||
|
e.stopImmediatePropagation = () => {
|
||||||
|
originalStop.call(e)
|
||||||
|
;(e as any)._stopped = true
|
||||||
|
}
|
||||||
|
return value.map(fn => (e: Event) => !(e as any)._stopped && fn(e))
|
||||||
|
} else {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user