Răsfoiți Sursa

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

pikax 6 ani în urmă
părinte
comite
dcb5985c00

+ 1 - 1
CHANGELOG.md

@@ -30,7 +30,7 @@
 * **compiler:** compiler options have been adjusted.
     - new option `decodeEntities` is added.
     - `namedCharacterReferences` option has been removed.
-    - `maxCRNameLength` option has been rmeoved.
+    - `maxCRNameLength` option has been removed.
 * **asyncComponent:** `retryWhen` and `maxRetries` options for
 `defineAsyncComponent` has been replaced by the more flexible `onError`
 option, per https://github.com/vuejs/rfcs/pull/148

+ 15 - 126
packages/reactivity/__tests__/readonly.spec.ts

@@ -5,9 +5,6 @@ import {
   isReactive,
   isReadonly,
   markNonReactive,
-  markReadonly,
-  lock,
-  unlock,
   effect,
   ref,
   shallowReadonly
@@ -91,22 +88,7 @@ describe('reactivity/readonly', () => {
       ).toHaveBeenWarnedLast()
     })
 
-    it('should allow mutation when unlocked', () => {
-      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', () => {
+    it('should not trigger effects', () => {
       const observed: any = readonly({ a: 1 })
       let dummy
       effect(() => {
@@ -118,20 +100,6 @@ describe('reactivity/readonly', () => {
       expect(dummy).toBe(1)
       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', () => {
@@ -183,23 +151,7 @@ describe('reactivity/readonly', () => {
       expect(`target is readonly.`).toHaveBeenWarnedTimes(5)
     })
 
-    it('should allow mutation when unlocked', () => {
-      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', () => {
+    it('should not trigger effects', () => {
       const observed: any = readonly([{ a: 1 }])
       let dummy
       effect(() => {
@@ -215,30 +167,6 @@ describe('reactivity/readonly', () => {
       expect(dummy).toBe(1)
       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]
@@ -276,23 +204,6 @@ describe('reactivity/readonly', () => {
         ).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) {
         test('should retrieve readonly values on iteration', () => {
           const key1 = {}
@@ -347,22 +258,6 @@ describe('reactivity/readonly', () => {
         ).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) {
         test('should retrieve readonly values on iteration', () => {
           const original = new Collection([{}, {}])
@@ -401,6 +296,19 @@ describe('reactivity/readonly', () => {
     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', () => {
     const original = { foo: 1 }
     const observed = readonly(original)
@@ -424,17 +332,6 @@ describe('reactivity/readonly', () => {
     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', () => {
     const n: any = readonly(ref(1))
     n.value = 2
@@ -470,13 +367,5 @@ describe('reactivity/readonly', () => {
         `Set operation on key "foo" failed: target is readonly.`
       ).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)
-    })
   })
 })

+ 61 - 1
packages/reactivity/__tests__/ref.spec.ts

@@ -3,12 +3,13 @@ import {
   effect,
   reactive,
   isRef,
+  toRef,
   toRefs,
   Ref,
   isReactive
 } from '../src/index'
 import { computed } from '@vue/runtime-dom'
-import { shallowRef, unref } from '../src/ref'
+import { shallowRef, unref, customRef } from '../src/ref'
 
 describe('reactivity/ref', () => {
   it('should hold a value', () => {
@@ -168,6 +169,34 @@ describe('reactivity/ref', () => {
     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', () => {
     const a = reactive({
       x: 1,
@@ -208,4 +237,35 @@ describe('reactivity/ref', () => {
     expect(dummyX).toBe(4)
     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)
+  })
 })

+ 38 - 41
packages/reactivity/src/baseHandlers.ts

@@ -1,7 +1,6 @@
 import { reactive, readonly, toRaw } from './reactive'
 import { TrackOpTypes, TriggerOpTypes } from './operations'
 import { track, trigger, ITERATE_KEY } from './effect'
-import { LOCKED } from './lock'
 import { isObject, hasOwn, isSymbol, hasChanged, isArray } from '@vue/shared'
 import { isRef } from './ref'
 
@@ -12,7 +11,7 @@ const builtInSymbols = new Set(
 )
 
 const get = /*#__PURE__*/ createGetter()
-const shallowReactiveGet = /*#__PURE__*/ createGetter(false, true)
+const shallowGet = /*#__PURE__*/ createGetter(false, true)
 const readonlyGet = /*#__PURE__*/ createGetter(true)
 const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)
 
@@ -36,23 +35,32 @@ const arrayInstrumentations: Record<string, Function> = {}
 
 function createGetter(isReadonly = false, shallow = false) {
   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)
     }
     const res = Reflect.get(target, key, receiver)
+
     if (isSymbol(key) && builtInSymbols.has(key)) {
       return res
     }
+
     if (shallow) {
-      track(target, TrackOpTypes.GET, key)
-      // TODO strict mode that returns a shallow-readonly version of the value
+      !isReadonly && track(target, TrackOpTypes.GET, key)
       return res
     }
-    // ref unwrapping, only for Objects, not for Arrays.
-    if (isRef(res) && !isArray(target)) {
-      return res.value
+
+    if (isRef(res)) {
+      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)
       ? isReadonly
         ? // 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 shallowReactiveSet = /*#__PURE__*/ createSetter(false, true)
-const readonlySet = /*#__PURE__*/ createSetter(true)
-const shallowReadonlySet = /*#__PURE__*/ createSetter(true, true)
+const shallowSet = /*#__PURE__*/ createSetter(true)
 
-function createSetter(isReadonly = false, shallow = false) {
+function createSetter(shallow = false) {
   return function set(
     target: object,
     key: string | symbol,
     value: unknown,
     receiver: object
   ): 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]
     if (!shallow) {
       value = toRaw(value)
@@ -141,30 +137,32 @@ export const mutableHandlers: ProxyHandler<object> = {
 
 export const readonlyHandlers: ProxyHandler<object> = {
   get: readonlyGet,
-  set: readonlySet,
   has,
   ownKeys,
-  deleteProperty(target: object, key: string | symbol): boolean {
-    if (LOCKED) {
-      if (__DEV__) {
-        console.warn(
-          `Delete operation on key "${String(
-            key
-          )}" failed: target is readonly.`,
-          target
-        )
-      }
-      return true
-    } else {
-      return deleteProperty(target, key)
+  set(target, key) {
+    if (__DEV__) {
+      console.warn(
+        `Set operation on key "${String(key)}" failed: target is readonly.`,
+        target
+      )
+    }
+    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> = {
   ...mutableHandlers,
-  get: shallowReactiveGet,
-  set: shallowReactiveSet
+  get: shallowGet,
+  set: shallowSet
 }
 
 // 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.
 export const shallowReadonlyHandlers: ProxyHandler<object> = {
   ...readonlyHandlers,
-  get: shallowReadonlyGet,
-  set: shallowReadonlySet
+  get: shallowReadonlyGet
 }

+ 19 - 26
packages/reactivity/src/collectionHandlers.ts

@@ -1,7 +1,6 @@
 import { toRaw, reactive, readonly } from './reactive'
 import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect'
 import { TrackOpTypes, TriggerOpTypes } from './operations'
-import { LOCKED } from './lock'
 import {
   isObject,
   capitalize,
@@ -142,7 +141,7 @@ function createForEach(isReadonly: boolean) {
     const observed = this
     const target = toRaw(observed)
     const wrap = isReadonly ? toReadonly : toReactive
-    track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
+    !isReadonly && track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
     // important: create sure the callback is
     // 1. invoked with the reactive map as `this` and 3rd arg
     // 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 innerIterator = getProto(target)[method].apply(target, args)
     const wrap = isReadonly ? toReadonly : toReactive
-    track(
-      target,
-      TrackOpTypes.ITERATE,
-      isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
-    )
+    !isReadonly &&
+      track(
+        target,
+        TrackOpTypes.ITERATE,
+        isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
+      )
     // return a wrapped iterator which returns observed versions of the
     // values emitted from the real iterator
     return {
@@ -187,23 +187,16 @@ function createIterableMethod(method: string | symbol, isReadonly: boolean) {
   }
 }
 
-function createReadonlyMethod(
-  method: Function,
-  type: TriggerOpTypes
-): Function {
+function createReadonlyMethod(type: TriggerOpTypes): Function {
   return function(this: CollectionTypes, ...args: unknown[]) {
-    if (LOCKED) {
-      if (__DEV__) {
-        const key = args[0] ? `on key "${args[0]}" ` : ``
-        console.warn(
-          `${capitalize(type)} operation ${key}failed: target is readonly.`,
-          toRaw(this)
-        )
-      }
-      return type === TriggerOpTypes.DELETE ? false : this
-    } else {
-      return method.apply(this, args)
+    if (__DEV__) {
+      const key = args[0] ? `on key "${args[0]}" ` : ``
+      console.warn(
+        `${capitalize(type)} operation ${key}failed: target is readonly.`,
+        toRaw(this)
+      )
     }
+    return type === TriggerOpTypes.DELETE ? false : this
   }
 }
 
@@ -230,10 +223,10 @@ const readonlyInstrumentations: Record<string, Function> = {
     return size((this as unknown) as IterableCollections)
   },
   has,
-  add: createReadonlyMethod(add, TriggerOpTypes.ADD),
-  set: createReadonlyMethod(set, TriggerOpTypes.SET),
-  delete: createReadonlyMethod(deleteEntry, TriggerOpTypes.DELETE),
-  clear: createReadonlyMethod(clear, TriggerOpTypes.CLEAR),
+  add: createReadonlyMethod(TriggerOpTypes.ADD),
+  set: createReadonlyMethod(TriggerOpTypes.SET),
+  delete: createReadonlyMethod(TriggerOpTypes.DELETE),
+  clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
   forEach: createForEach(true)
 }
 

+ 5 - 1
packages/reactivity/src/effect.ts

@@ -12,6 +12,7 @@ const targetMap = new WeakMap<any, KeyToDepMap>()
 export interface ReactiveEffect<T = any> {
   (...args: any[]): T
   _isEffect: true
+  id: number
   active: boolean
   raw: () => T
   deps: Array<Dep>
@@ -21,7 +22,7 @@ export interface ReactiveEffect<T = any> {
 export interface ReactiveEffectOptions {
   lazy?: boolean
   computed?: boolean
-  scheduler?: (job: () => void) => void
+  scheduler?: (job: ReactiveEffect) => void
   onTrack?: (event: DebuggerEvent) => void
   onTrigger?: (event: DebuggerEvent) => void
   onStop?: () => void
@@ -74,6 +75,8 @@ export function stop(effect: ReactiveEffect) {
   }
 }
 
+let uid = 0
+
 function createReactiveEffect<T = any>(
   fn: (...args: any[]) => T,
   options: ReactiveEffectOptions
@@ -96,6 +99,7 @@ function createReactiveEffect<T = any>(
       }
     }
   } as ReactiveEffect
+  effect.id = uid++
   effect._isEffect = true
   effect.active = true
   effect.raw = fn

+ 11 - 3
packages/reactivity/src/index.ts

@@ -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 {
   reactive,
   isReactive,
@@ -7,7 +17,6 @@ export {
   isReadonly,
   shallowReadonly,
   toRaw,
-  markReadonly,
   markNonReactive
 } from './reactive'
 export {
@@ -31,5 +40,4 @@ export {
   ReactiveEffectOptions,
   DebuggerEvent
 } from './effect'
-export { lock, unlock } from './lock'
 export { TrackOpTypes, TriggerOpTypes } from './operations'

+ 0 - 10
packages/reactivity/src/lock.ts

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

+ 18 - 35
packages/reactivity/src/reactive.ts

@@ -2,14 +2,14 @@ import { isObject, toRawType } from '@vue/shared'
 import {
   mutableHandlers,
   readonlyHandlers,
-  shallowReadonlyHandlers,
-  shallowReactiveHandlers
+  shallowReactiveHandlers,
+  shallowReadonlyHandlers
 } from './baseHandlers'
 import {
   mutableCollectionHandlers,
   readonlyCollectionHandlers
 } from './collectionHandlers'
-import { UnwrapRef, Ref, isRef } from './ref'
+import { UnwrapRef, Ref } from './ref'
 import { makeMap } from '@vue/shared'
 
 // 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
 // observable creation.
-const readonlyValues = new WeakSet<any>()
 const nonReactiveValues = new WeakSet<any>()
 
 const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
@@ -47,13 +46,6 @@ export function reactive(target: object) {
   if (readonlyToRaw.has(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(
     target,
     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>(
   target: 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(
     target,
     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(
   target: unknown,
   toProxy: WeakMap<any, any>,
@@ -153,12 +140,8 @@ export function isReadonly(value: unknown): boolean {
 }
 
 export function toRaw<T>(observed: T): T {
-  return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed
-}
-
-export function markReadonly<T>(value: T): T {
-  readonlyValues.add(value)
-  return value
+  observed = readonlyToRaw.get(observed) || observed
+  return reactiveToRaw.get(observed) || observed
 }
 
 export function markNonReactive<T extends object>(value: T): T {

+ 27 - 2
packages/reactivity/src/ref.ts

@@ -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
 }
 
+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>(
   object: T
 ): { [K in keyof T]: Ref<T[K]> } {
@@ -78,12 +103,12 @@ export function toRefs<T extends object>(
   }
   const ret: any = {}
   for (const key in object) {
-    ret[key] = toProxyRef(object, key)
+    ret[key] = toRef(object, key)
   }
   return ret
 }
 
-function toProxyRef<T extends object, K extends keyof T>(
+export function toRef<T extends object, K extends keyof T>(
   object: T,
   key: K
 ): Ref<T[K]> {

+ 9 - 1
packages/runtime-core/__tests__/componentProps.spec.ts

@@ -175,9 +175,17 @@ describe('component props', () => {
     expect(proxy.foo).toBe(2)
     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.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 () => {

+ 6 - 5
packages/runtime-core/__tests__/componentProxy.spec.ts

@@ -3,7 +3,8 @@ import {
   render,
   getCurrentInstance,
   nodeOps,
-  createApp
+  createApp,
+  shallowReadonly
 } from '@vue/runtime-test'
 import { mockWarn } from '@vue/shared'
 import { ComponentInternalInstance } from '../src/component'
@@ -85,10 +86,10 @@ describe('component: proxy', () => {
     }
     render(h(Comp), nodeOps.createElement('div'))
     expect(instanceProxy.$data).toBe(instance!.data)
-    expect(instanceProxy.$props).toBe(instance!.props)
-    expect(instanceProxy.$attrs).toBe(instance!.attrs)
-    expect(instanceProxy.$slots).toBe(instance!.slots)
-    expect(instanceProxy.$refs).toBe(instance!.refs)
+    expect(instanceProxy.$props).toBe(shallowReadonly(instance!.props))
+    expect(instanceProxy.$attrs).toBe(shallowReadonly(instance!.attrs))
+    expect(instanceProxy.$slots).toBe(shallowReadonly(instance!.slots))
+    expect(instanceProxy.$refs).toBe(shallowReadonly(instance!.refs))
     expect(instanceProxy.$parent).toBe(
       instance!.parent && instance!.parent.proxy
     )

+ 16 - 0
packages/runtime-core/__tests__/scheduler.spec.ts

@@ -262,4 +262,20 @@ describe('scheduler', () => {
     // job2 should be called only once
     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'])
+  })
 })

+ 4 - 14
packages/runtime-core/src/component.ts

@@ -3,7 +3,8 @@ import {
   reactive,
   ReactiveEffect,
   pauseTracking,
-  resetTracking
+  resetTracking,
+  shallowReadonly
 } from '@vue/reactivity'
 import {
   ComponentPublicInstance,
@@ -347,7 +348,7 @@ function setupStatefulComponent(
       setup,
       instance,
       ErrorCodes.SETUP_FUNCTION,
-      [instance.props, setupContext]
+      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
     )
     resetTracking()
     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> = {
   get: (target, key: string) => {
     markAttrsAccessed()
@@ -514,7 +504,7 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext {
         return new Proxy(instance.attrs, attrHandlers)
       },
       get slots() {
-        return new Proxy(instance.slots, slotsHandlers)
+        return shallowReadonly(instance.slots)
       },
       get emit() {
         return (event: string, ...args: any[]) => instance.emit(event, ...args)

+ 9 - 1
packages/runtime-core/src/componentOptions.ts

@@ -15,7 +15,8 @@ import {
   isArray,
   EMPTY_OBJ,
   NOOP,
-  hasOwn
+  hasOwn,
+  isPromise
 } from '@vue/shared'
 import { computed } from './apiComputed'
 import { watch, WatchOptions, WatchCallback } from './apiWatch'
@@ -316,6 +317,13 @@ export function applyOptions(
       )
     }
     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)) {
       __DEV__ && warn(`data() should return an object.`)
     } else if (instance.data === EMPTY_OBJ) {

+ 30 - 28
packages/runtime-core/src/componentProps.ts

@@ -1,4 +1,4 @@
-import { toRaw, lock, unlock, shallowReadonly } from '@vue/reactivity'
+import { toRaw, shallowReactive } from '@vue/reactivity'
 import {
   EMPTY_OBJ,
   camelize,
@@ -114,7 +114,7 @@ export function initProps(
 
   if (isStateful) {
     // stateful
-    instance.props = isSSR ? props : shallowReadonly(props)
+    instance.props = isSSR ? props : shallowReactive(props)
   } else {
     if (!options) {
       // functional w/ optional props, props === attrs
@@ -132,9 +132,6 @@ export function updateProps(
   rawProps: Data | null,
   optimized: boolean
 ) {
-  // allow mutation of propsProxy (which is readonly by default)
-  unlock()
-
   const {
     props,
     attrs,
@@ -186,7 +183,16 @@ export function updateProps(
           // and converted to camelCase (#955)
           ((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) {
@@ -196,9 +202,6 @@ export function updateProps(
     }
   }
 
-  // lock readonly
-  lock()
-
   if (__DEV__ && rawOptions && rawProps) {
     validateProps(props, rawOptions)
   }
@@ -250,25 +253,24 @@ function resolvePropValue(
   key: string,
   value: unknown
 ) {
-  let opt = options[key]
-  if (opt == null) {
-    return value
-  }
-  const hasDefault = hasOwn(opt, 'default')
-  // default values
-  if (hasDefault && value === undefined) {
-    const defaultValue = opt.default
-    value = isFunction(defaultValue) ? defaultValue() : defaultValue
-  }
-  // boolean casting
-  if (opt[BooleanFlags.shouldCast]) {
-    if (!hasOwn(props, key) && !hasDefault) {
-      value = false
-    } else if (
-      opt[BooleanFlags.shouldCastTrue] &&
-      (value === '' || value === hyphenate(key))
-    ) {
-      value = true
+  const opt = options[key]
+  if (opt != null) {
+    const hasDefault = hasOwn(opt, 'default')
+    // default values
+    if (hasDefault && value === undefined) {
+      const defaultValue = opt.default
+      value = isFunction(defaultValue) ? defaultValue() : defaultValue
+    }
+    // boolean casting
+    if (opt[BooleanFlags.shouldCast]) {
+      if (!hasOwn(props, key) && !hasDefault) {
+        value = false
+      } else if (
+        opt[BooleanFlags.shouldCastTrue] &&
+        (value === '' || value === hyphenate(key))
+      ) {
+        value = true
+      }
     }
   }
   return value

+ 12 - 7
packages/runtime-core/src/componentProxy.ts

@@ -2,7 +2,12 @@ import { ComponentInternalInstance, Data } from './component'
 import { nextTick, queueJob } from './scheduler'
 import { instanceWatch } from './apiWatch'
 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 {
   ExtractComputedReturns,
   ComponentOptionsBase,
@@ -36,8 +41,8 @@ export type ComponentPublicInstance<
   $attrs: Data
   $refs: Data
   $slots: Slots
-  $root: ComponentInternalInstance | null
-  $parent: ComponentInternalInstance | null
+  $root: ComponentPublicInstance | null
+  $parent: ComponentPublicInstance | null
   $emit: EmitFn<E>
   $el: any
   $options: ComponentOptionsBase<P, B, D, C, M, E>
@@ -57,10 +62,10 @@ const publicPropertiesMap: Record<
   $: i => i,
   $el: i => i.vnode.el,
   $data: i => i.data,
-  $props: i => i.props,
-  $attrs: i => i.attrs,
-  $slots: i => i.slots,
-  $refs: i => i.refs,
+  $props: i => (__DEV__ ? shallowReadonly(i.props) : i.props),
+  $attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs),
+  $slots: i => (__DEV__ ? shallowReadonly(i.slots) : i.slots),
+  $refs: i => (__DEV__ ? shallowReadonly(i.refs) : i.refs),
   $parent: i => i.parent && i.parent.proxy,
   $root: i => i.root && i.root.proxy,
   $emit: i => i.emit,

+ 5 - 4
packages/runtime-core/src/index.ts

@@ -2,20 +2,21 @@
 
 export const version = __VERSION__
 export {
-  effect,
   ref,
   unref,
   shallowRef,
   isRef,
+  toRef,
   toRefs,
+  customRef,
   reactive,
   isReactive,
   readonly,
   isReadonly,
   shallowReactive,
-  toRaw,
-  markReadonly,
-  markNonReactive
+  shallowReadonly,
+  markNonReactive,
+  toRaw
 } from '@vue/reactivity'
 export { computed } from './apiComputed'
 export { watch, watchEffect } from './apiWatch'

+ 25 - 8
packages/runtime-core/src/scheduler.ts

@@ -1,7 +1,12 @@
 import { ErrorCodes, callWithErrorHandling } from './errorHandling'
 import { isArray } from '@vue/shared'
 
-const queue: (Function | null)[] = []
+export interface Job {
+  (): void
+  id?: number
+}
+
+const queue: (Job | null)[] = []
 const postFlushCbs: Function[] = []
 const p = Promise.resolve()
 
@@ -9,20 +14,20 @@ let isFlushing = false
 let isFlushPending = false
 
 const RECURSION_LIMIT = 100
-type CountMap = Map<Function, number>
+type CountMap = Map<Job | Function, number>
 
 export function nextTick(fn?: () => void): Promise<void> {
   return fn ? p.then(fn) : p
 }
 
-export function queueJob(job: () => void) {
+export function queueJob(job: Job) {
   if (!queue.includes(job)) {
     queue.push(job)
     queueFlush()
   }
 }
 
-export function invalidateJob(job: () => void) {
+export function invalidateJob(job: Job) {
   const i = queue.indexOf(job)
   if (i > -1) {
     queue[i] = null
@@ -45,11 +50,9 @@ function queueFlush() {
   }
 }
 
-const dedupe = (cbs: Function[]): Function[] => [...new Set(cbs)]
-
 export function flushPostFlushCbs(seen?: CountMap) {
   if (postFlushCbs.length) {
-    const cbs = dedupe(postFlushCbs)
+    const cbs = [...new Set(postFlushCbs)]
     postFlushCbs.length = 0
     if (__DEV__) {
       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) {
   isFlushPending = false
   isFlushing = true
@@ -70,6 +75,18 @@ function flushJobs(seen?: CountMap) {
   if (__DEV__) {
     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) {
     if (job === null) {
       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)) {
     seen.set(fn, 1)
   } else {

+ 17 - 1
packages/runtime-core/src/vnode.ts

@@ -17,7 +17,7 @@ import {
   ClassComponent
 } from './component'
 import { RawSlots } from './componentSlots'
-import { isReactive, Ref } from '@vue/reactivity'
+import { isReactive, Ref, toRaw } from '@vue/reactivity'
 import { AppContext } from './apiCreateApp'
 import {
   SuspenseImpl,
@@ -292,6 +292,22 @@ function _createVNode(
             ? ShapeFlags.FUNCTIONAL_COMPONENT
             : 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 = {
     _isVNode: true,
     type,

+ 14 - 0
packages/runtime-dom/__tests__/patchEvents.spec.ts

@@ -134,4 +134,18 @@ describe(`runtime-dom: events patching`, () => {
     expect(fn).toHaveBeenCalledTimes(1)
     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)
+  })
 })

+ 1 - 1
packages/runtime-dom/src/components/TransitionGroup.ts

@@ -13,7 +13,6 @@ import {
   VNode,
   warn,
   resolveTransitionHooks,
-  toRaw,
   useTransitionState,
   getCurrentInstance,
   setTransitionHooks,
@@ -21,6 +20,7 @@ import {
   onUpdated,
   SetupContext
 } from '@vue/runtime-core'
+import { toRaw } from '@vue/reactivity'
 
 interface Position {
   top: number

+ 18 - 2
packages/runtime-dom/src/modules/events.ts

@@ -1,4 +1,4 @@
-import { EMPTY_OBJ } from '@vue/shared'
+import { EMPTY_OBJ, isArray } from '@vue/shared'
 import {
   ComponentInternalInstance,
   callWithAsyncErrorHandling
@@ -130,7 +130,7 @@ function createInvoker(
     // AFTER it was attached.
     if (e.timeStamp >= invoker.lastUpdated - 1) {
       callWithAsyncErrorHandling(
-        invoker.value,
+        patchStopImmediatePropagation(e, invoker.value),
         instance,
         ErrorCodes.NATIVE_EVENT_HANDLER,
         [e]
@@ -142,3 +142,19 @@ function createInvoker(
   invoker.lastUpdated = getNow()
   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
+  }
+}