Merge remote-tracking branch 'github/master' into changing_unwrap_ref

This commit is contained in:
pikax 2020-04-15 15:54:26 +01:00
commit dcb5985c00
23 changed files with 361 additions and 318 deletions

View File

@ -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

View File

@ -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)
})
}) })
}) })

View File

@ -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)
})
}) })

View File

@ -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
} }
if (isRef(res)) {
if (targetIsArray) {
!isReadonly && track(target, TrackOpTypes.GET, key)
return res
} else {
// ref unwrapping, only for Objects, not for Arrays. // ref unwrapping, only for Objects, not for Arrays.
if (isRef(res) && !isArray(target)) {
return res.value 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(
`Delete operation on key "${String( `Set operation on key "${String(key)}" failed: target is readonly.`,
key
)}" failed: target is readonly.`,
target target
) )
} }
return true return true
} else { },
return deleteProperty(target, key) 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
} }

View File

@ -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,6 +160,7 @@ 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
!isReadonly &&
track( track(
target, target,
TrackOpTypes.ITERATE, TrackOpTypes.ITERATE,
@ -187,12 +187,8 @@ 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(
@ -201,9 +197,6 @@ function createReadonlyMethod(
) )
} }
return type === TriggerOpTypes.DELETE ? false : this return type === TriggerOpTypes.DELETE ? false : this
} else {
return method.apply(this, args)
}
} }
} }
@ -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)
} }

View File

@ -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

View File

@ -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'

View File

@ -1,10 +0,0 @@
// global immutability lock
export let LOCKED = true
export function lock() {
LOCKED = true
}
export function unlock() {
LOCKED = false
}

View File

@ -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 {

View File

@ -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]> {

View File

@ -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 () => {

View File

@ -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
) )

View File

@ -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'])
})
}) })

View File

@ -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)

View File

@ -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) {

View File

@ -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,9 +183,18 @@ 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)))
) { ) {
if (options) {
props[key] = resolvePropValue(
options,
rawProps || EMPTY_OBJ,
key,
undefined
)
} else {
delete props[key] delete props[key]
} }
} }
}
for (const key in attrs) { for (const key in attrs) {
if (!rawProps || !hasOwn(rawProps, key)) { if (!rawProps || !hasOwn(rawProps, key)) {
delete attrs[key] delete attrs[key]
@ -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,10 +253,8 @@ 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') const hasDefault = hasOwn(opt, 'default')
// default values // default values
if (hasDefault && value === undefined) { if (hasDefault && value === undefined) {
@ -271,6 +272,7 @@ function resolvePropValue(
value = true value = true
} }
} }
}
return value return value
} }

View File

@ -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,

View File

@ -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'

View File

@ -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 {

View File

@ -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,

View File

@ -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)
})
}) })

View File

@ -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

View File

@ -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
}
}