From 6f57c9605241174ddf085411e75df4346017d5a9 Mon Sep 17 00:00:00 2001 From: yantene Date: Fri, 3 Feb 2017 18:18:07 +0000 Subject: [PATCH 1/5] add $props --- flow/component.js | 1 + src/core/instance/state.js | 34 +++++++++---------- .../unit/features/instance/properties.spec.js | 17 ++++++++++ 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/flow/component.js b/flow/component.js index f4a614dd9de..1a9c27c42c3 100644 --- a/flow/component.js +++ b/flow/component.js @@ -28,6 +28,7 @@ declare interface Component { $scopedSlots: { [key: string]: () => VNodeChildren }; $vnode: VNode; $isServer: boolean; + $props: Object; // public methods $mount: (el?: Element | string, hydrating?: boolean) => Component; diff --git a/src/core/instance/state.js b/src/core/instance/state.js index 4aeaaf4997b..3978fcd1649 100644 --- a/src/core/instance/state.js +++ b/src/core/instance/state.js @@ -39,6 +39,7 @@ const isReservedProp = { key: 1, ref: 1, slot: 1 } function initProps (vm: Component, props: Object) { const propsData = vm.$options.propsData || {} + vm.$props = {} // cache prop keys so that future props updates can iterate using Array // instead of dyanmic object key enumeration. const keys = vm.$options._propKeys = [] @@ -55,7 +56,7 @@ function initProps (vm: Component, props: Object) { vm ) } - defineReactive(vm, key, validateProp(key, props, propsData, vm), () => { + defineReactive(vm.$props, key, validateProp(key, props, propsData, vm), () => { if (vm.$parent && !observerState.isSettingProps) { warn( `Avoid mutating a prop directly since the value will be ` + @@ -67,8 +68,9 @@ function initProps (vm: Component, props: Object) { } }) } else { - defineReactive(vm, key, validateProp(key, props, propsData, vm)) + defineReactive(vm.$props, key, validateProp(key, props, propsData, vm)) } + proxy(vm, '$props', key) } observerState.shouldConvert = true } @@ -97,8 +99,8 @@ function initData (vm: Component) { `Use prop default value instead.`, vm ) - } else { - proxy(vm, keys[i]) + } else if (!isReserved(keys[i])) { + proxy(vm, '_data', keys[i]) } } // observe data @@ -233,17 +235,15 @@ export function stateMixin (Vue: Class) { } } -function proxy (vm: Component, key: string) { - if (!isReserved(key)) { - Object.defineProperty(vm, key, { - configurable: true, - enumerable: true, - get: function proxyGetter () { - return vm._data[key] - }, - set: function proxySetter (val) { - vm._data[key] = val - } - }) - } +function proxy (vm: Component, proxyName: '$props' | '_data', key: string) { + Object.defineProperty(vm, key, { + configurable: true, + enumerable: true, + get: function proxyGetter () { + return vm[proxyName][key] + }, + set: function proxySetter (val) { + vm[proxyName][key] = val + } + }) } diff --git a/test/unit/features/instance/properties.spec.js b/test/unit/features/instance/properties.spec.js index 7a539a85ffe..251a3088ae5 100644 --- a/test/unit/features/instance/properties.spec.js +++ b/test/unit/features/instance/properties.spec.js @@ -79,4 +79,21 @@ describe('Instance properties', () => { }).$mount() expect(calls).toEqual(['outer:undefined', 'middle:outer', 'inner:middle', 'next:undefined']) }) + + it('$props', () => { + var Comp = Vue.extend({ + props: ['msg'], + template: '
{{ msg }}
' + }) + var vm = new Comp({ + propsData: { + msg: 'foo' + } + }) + // check existence + expect(vm.$props.msg).toBe('foo') + // check change + Vue.set(vm, 'msg', 'bar') + expect(vm.$props.msg).toBe('bar') + }) }) From 7bb1a86bd2ec7b42e2b7644f4402e100d262f009 Mon Sep 17 00:00:00 2001 From: yantene Date: Sun, 5 Feb 2017 05:30:53 +0900 Subject: [PATCH 2/5] add the type of props in typescript definitions --- types/vue.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/types/vue.d.ts b/types/vue.d.ts index 823828a6d5d..3723485e74f 100644 --- a/types/vue.d.ts +++ b/types/vue.d.ts @@ -42,6 +42,7 @@ export declare class Vue { readonly $slots: { [key: string]: VNode[] }; readonly $scopedSlots: { [key: string]: ScopedSlot }; readonly $isServer: boolean; + $props: Object; $mount(elementOrSelector?: Element | String, hydrating?: boolean): this; $forceUpdate(): void; From dd1f158423c9612fe212d4ee183d14b0f48e8a3c Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 14 Feb 2017 10:44:29 -0500 Subject: [PATCH 3/5] $props type improvements --- types/vue.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/vue.d.ts b/types/vue.d.ts index 3723485e74f..5b6ce494633 100644 --- a/types/vue.d.ts +++ b/types/vue.d.ts @@ -42,7 +42,7 @@ export declare class Vue { readonly $slots: { [key: string]: VNode[] }; readonly $scopedSlots: { [key: string]: ScopedSlot }; readonly $isServer: boolean; - $props: Object; + readonly $props: any; $mount(elementOrSelector?: Element | String, hydrating?: boolean): this; $forceUpdate(): void; From f165fff5c965430a26712996de4de97e7a6e9026 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 14 Feb 2017 11:15:06 -0500 Subject: [PATCH 4/5] tweaks --- flow/component.js | 1 + src/core/instance/lifecycle.js | 4 +++- src/core/instance/state.js | 31 ++++++++++++++++++------------- src/core/util/props.js | 8 ++++---- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/flow/component.js b/flow/component.js index 1a9c27c42c3..088c7692d55 100644 --- a/flow/component.js +++ b/flow/component.js @@ -53,6 +53,7 @@ declare interface Component { _watcher: Watcher; _watchers: Array; _data: Object; + _props: Object; _events: Object; _inactive: boolean; _isMounted: boolean; diff --git a/src/core/instance/lifecycle.js b/src/core/instance/lifecycle.js index 34f3ed5fa48..efe7ad6415f 100644 --- a/src/core/instance/lifecycle.js +++ b/src/core/instance/lifecycle.js @@ -143,15 +143,17 @@ export function lifecycleMixin (Vue: Class) { if (process.env.NODE_ENV !== 'production') { observerState.isSettingProps = true } + const props = vm._props const propKeys = vm.$options._propKeys || [] for (let i = 0; i < propKeys.length; i++) { const key = propKeys[i] - vm[key] = validateProp(key, vm.$options.props, propsData, vm) + props[key] = validateProp(key, vm.$options.props, propsData, vm) } observerState.shouldConvert = true if (process.env.NODE_ENV !== 'production') { observerState.isSettingProps = false } + // keep a copy of raw propsData vm.$options.propsData = propsData } // update listeners diff --git a/src/core/instance/state.js b/src/core/instance/state.js index 3978fcd1649..dc7ae87ac11 100644 --- a/src/core/instance/state.js +++ b/src/core/instance/state.js @@ -37,17 +37,18 @@ export function initState (vm: Component) { const isReservedProp = { key: 1, ref: 1, slot: 1 } -function initProps (vm: Component, props: Object) { +function initProps (vm: Component, propsOptions: Object) { const propsData = vm.$options.propsData || {} - vm.$props = {} + const props = vm._props = {} // cache prop keys so that future props updates can iterate using Array // instead of dyanmic object key enumeration. const keys = vm.$options._propKeys = [] const isRoot = !vm.$parent // root instance props should be converted observerState.shouldConvert = isRoot - for (const key in props) { + for (const key in propsOptions) { keys.push(key) + const value = validateProp(key, propsOptions, propsData, vm) /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { if (isReservedProp[key]) { @@ -56,7 +57,7 @@ function initProps (vm: Component, props: Object) { vm ) } - defineReactive(vm.$props, key, validateProp(key, props, propsData, vm), () => { + defineReactive(props, key, value, () => { if (vm.$parent && !observerState.isSettingProps) { warn( `Avoid mutating a prop directly since the value will be ` + @@ -68,9 +69,9 @@ function initProps (vm: Component, props: Object) { } }) } else { - defineReactive(vm.$props, key, validateProp(key, props, propsData, vm)) + defineReactive(props, key, value) } - proxy(vm, '$props', key) + proxy(vm, props, key) } observerState.shouldConvert = true } @@ -100,7 +101,7 @@ function initData (vm: Component) { vm ) } else if (!isReserved(keys[i])) { - proxy(vm, '_data', keys[i]) + proxy(vm, data, keys[i]) } } // observe data @@ -200,9 +201,9 @@ export function stateMixin (Vue: Class) { // when using Object.defineProperty, so we have to procedurally build up // the object here. const dataDef = {} - dataDef.get = function () { - return this._data - } + dataDef.get = function () { return this._data } + const propsDef = {} + propsDef.get = function () { return this._props } if (process.env.NODE_ENV !== 'production') { dataDef.set = function (newData: Object) { warn( @@ -211,8 +212,12 @@ export function stateMixin (Vue: Class) { this ) } + propsDef.set = function () { + warn(`$props is readonly.`, this) + } } Object.defineProperty(Vue.prototype, '$data', dataDef) + Object.defineProperty(Vue.prototype, '$props', propsDef) Vue.prototype.$set = set Vue.prototype.$delete = del @@ -235,15 +240,15 @@ export function stateMixin (Vue: Class) { } } -function proxy (vm: Component, proxyName: '$props' | '_data', key: string) { +function proxy (vm: Component, source: Object, key: string) { Object.defineProperty(vm, key, { configurable: true, enumerable: true, get: function proxyGetter () { - return vm[proxyName][key] + return source[key] }, set: function proxySetter (val) { - vm[proxyName][key] = val + source[key] = val } }) } diff --git a/src/core/util/props.js b/src/core/util/props.js index 88ed0c1c5f0..11c0245c095 100644 --- a/src/core/util/props.js +++ b/src/core/util/props.js @@ -54,8 +54,8 @@ function getPropDefaultValue (vm: ?Component, prop: PropOptions, key: string): a } const def = prop.default // warn against non-factory defaults for Object & Array - if (isObject(def)) { - process.env.NODE_ENV !== 'production' && warn( + if (process.env.NODE_ENV !== 'production' && isObject(def)) { + warn( 'Invalid default value for prop "' + key + '": ' + 'Props with type Object/Array must use a factory function ' + 'to return the default value.', @@ -66,8 +66,8 @@ function getPropDefaultValue (vm: ?Component, prop: PropOptions, key: string): a // return previous default value to avoid unnecessary watcher trigger if (vm && vm.$options.propsData && vm.$options.propsData[key] === undefined && - vm[key] !== undefined) { - return vm[key] + vm._props[key] !== undefined) { + return vm._props[key] } // call factory function for non-Function types return typeof def === 'function' && prop.type !== Function From 89f5ba2f433512c75b9a8b1d033ed4b8e370e261 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 14 Feb 2017 11:31:30 -0500 Subject: [PATCH 5/5] improve $props test case --- .../unit/features/instance/properties.spec.js | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/test/unit/features/instance/properties.spec.js b/test/unit/features/instance/properties.spec.js index 251a3088ae5..e74a1ca1f76 100644 --- a/test/unit/features/instance/properties.spec.js +++ b/test/unit/features/instance/properties.spec.js @@ -80,20 +80,49 @@ describe('Instance properties', () => { expect(calls).toEqual(['outer:undefined', 'middle:outer', 'inner:middle', 'next:undefined']) }) - it('$props', () => { - var Comp = Vue.extend({ + it('$props', done => { + const Comp = Vue.extend({ props: ['msg'], - template: '
{{ msg }}
' + template: '
{{ msg }} {{ $props.msg }}
' }) - var vm = new Comp({ + const vm = new Comp({ propsData: { msg: 'foo' } - }) + }).$mount() + // check render + expect(vm.$el.textContent).toContain('foo foo') + // warn set + vm.$props = {} + expect('$props is readonly').toHaveBeenWarned() // check existence expect(vm.$props.msg).toBe('foo') // check change - Vue.set(vm, 'msg', 'bar') + vm.msg = 'bar' expect(vm.$props.msg).toBe('bar') + waitForUpdate(() => { + expect(vm.$el.textContent).toContain('bar bar') + }).then(() => { + vm.$props.msg = 'baz' + expect(vm.msg).toBe('baz') + }).then(() => { + expect(vm.$el.textContent).toContain('baz baz') + }).then(done) + }) + + it('warn mutating $props', () => { + const Comp = { + props: ['msg'], + render () {}, + mounted () { + expect(this.$props.msg).toBe('foo') + this.$props.msg = 'bar' + } + } + new Vue({ + template: ``, + components: { Comp } + }).$mount() + expect(`Avoid mutating a prop`).toHaveBeenWarned() }) })