第 40 题:在 Vue 中,子组件为何不可以修改父组件传递的 Prop,如果修改了,Vue 是如何监控到属性的修改并给出警告的。
IWANABETHATGUY opened this issue · comments
- 子组件为何不可以修改父组件传递的 Prop
单向数据流,易于监测数据的流动,出现了错误可以更加迅速的定位到错误发生的位置。 - 如果修改了,Vue 是如何监控到属性的修改并给出警告的。
if (process.env.NODE_ENV !== 'production') {
var hyphenatedKey = hyphenate(key);
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
("\"" + hyphenatedKey + "\" is a reserved attribute and cannot be used as component prop."),
vm
);
}
defineReactive$$1(props, key, value, function () {
if (!isRoot && !isUpdatingChildComponent) {
warn(
"Avoid mutating a prop directly since the value will be " +
"overwritten whenever the parent component re-renders. " +
"Instead, use a data or computed property based on the prop's " +
"value. Prop being mutated: \"" + key + "\"",
vm
);
}
});
}
在initProps的时候,在defineReactive时通过判断是否在开发环境,如果是开发环境,会在触发set的时候判断是否此key是否处于updatingChildren中被修改,如果不是,说明此修改来自子组件,触发warning提示。
需要特别注意的是,当你从子组件修改的prop属于基础类型时会触发提示。 这种情况下,你是无法修改父组件的数据源的, 因为基础类型赋值时是值拷贝。你直接将另一个非基础类型(Object, array)赋值到此key时也会触发提示(但实际上不会影响父组件的数据源), 当你修改object的属性时不会触发提示,并且会修改父组件数据源的数据。
子组件为何不可以修改父组件传递的 Prop?-> 因为每当父组件属性值修改时,该值都将被覆盖;如果要有不同的改变,可以用基于prop的data或者computed
貌似是 极客时间 里面 那个作者提出来的问题~ 不过楼主你真的很认真哟~
漂亮, 还可以这么玩!
为何不能修改:为了保证数据的单向流动,便于对数据进行追踪,避免数据混乱。官网有详细的信息 prop
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
vue如何知道修改了父组件传来的props?
下面的代码就是实现Vue提示修改props的操作,在组件 initProps
方法的时候,会对props进行defineReactive操作,传入的第四个参数是自定义的set函数,该函数会在触发props的set方法时执行,当props修改了,就会运行这里传入的第四个参数,然后进行判断,如果不是root根组件,并且不是更新子组件,那么说明更新的是props,所以会警告
// src/core/instance/state.js 源码路径
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
defineReactive(props, key, value)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
// src/core/observer/index.js
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
如果传入的props是基本数据类型,子组件修改父组件传的props会警告,并且修改不成功,如果传入的是引用数据类型,那么修改改引用数据类型的某个属性值时,对应的props也会修改,并且vue不会抱警告。
然而 props传入的是对象的话 是可以直接在子组件里更改的, 因为是同一个引用
组件对于data的监听是深度监听
而对于props的监听是浅度监听
这道题不错
个人感觉 就从父组件底下有未知个子组件 如果子组件可以直接修改父组件的值 别的子组件也依赖父组件的这个值的话就乱了 ,不好追踪, 子组件通过$emit方式实际还是父组件修改好了再通过数据流传过来而已
原因很简单,一个父组件下不只有你一个子组件。
同样,使用这份 prop 数据的也不只有你一个子组件。
如果每个子组件都能修改 prop 的话,将会导致修改数据的源头不止一处。
所以我们需要将修改数据的源头统一为父组件,子组件像要改 prop 只能委托父组件帮它。从而保证数据修改源唯一
说子组件不可以修改父组件传递的props是不够严谨的,我们在用this[key]来修改props的时候,完全可以改变该组件的vm._props,只是父组件传递的数据源是不会改变的,保证了数据源的唯一性。
原因就是要看vue源码中对props初始化的函数iniProps上。
function initProps (vm: Component, propsOptions: Object) {
//获取父组件传入的props对象。
const propsData = vm.$options.propsData || {}
/*这里没有用 defineReactive 函数直接处理 propsDatas, 而是用一个新变量来接受props来接受
defineReactive的处理 */
const props = vm._props = {}
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
//当存在父组件并且修改来源于子组件的时候给出警告
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
defineReactive(props, key, value)
}
if (!(key in vm)) {
/*把_props的属性经过属性代理 ,方便我们可以用 this[key] 直接访问 vm下边的_props 里边的属性*/
proxy(vm, `_props`, key)
}
综上,也就是说我们在用 this[key] = newVal来改变组件中的props的时候,实际改变的是vm下边的_props, 而由父组件传来的propsOptions并没有发生改变,这里应该注意的是当this._props[key]是基础数据类型的时候,this._props[key] 就是 propsData[key] 的拷贝值,当this._props[key]为引用类型的时候,this._props[key] 和 propsData[key] 指向的是同一个栈内存,**也就是说我们修改this._props[key]下的属性时候, propsData[key]也发生了改变,也就是父元素的数据发生了改变
那么如果修改了,Vue是怎么监控的呢,就是要看defineReactive函数
然而 props传入的是对象的话 是可以直接在子组件里更改的, 因为是同一个引用
组件对于data的监听是深度监听
而对于props的监听是浅度监听
props也是深度数据响应吧,用的同一个defineReactive方法来实现
vue数据传递是单项数据流,如果父组件传递过来的props为引用类型,直接修改了props的值会导致其他用到此props的子组件的值也发生改变,导致数据混乱,以及难以追踪的bug。如果props是基础类型,不会造成其他影响,但也会是整个组件数据混乱
原因很简单,一个父组件下不只有你一个子组件。
同样,使用这份 prop 数据的也不只有你一个子组件。
如果每个子组件都能修改 prop 的话,将会导致修改数据的源头不止一处。所以我们需要将修改数据的源头统一为父组件,子组件像要改 prop 只能委托父组件帮它。从而保证数据修改源唯一
那如果父级传入到子组件的不是一个引用而是一个copy的对象,是不是子组件就可以改呢? 这样是不是就不会影响到其他的子组件了呢?
我觉得props能不能改只是框架的自己的一种设计,当然它也给了不能修改的理由
- 子组件为何不可以修改父组件传递的 Prop
单向数据流,易于监测数据的流动,出现了错误可以更加迅速的定位到错误发生的位置。- 如果修改了,Vue 是如何监控到属性的修改并给出警告的。
if (process.env.NODE_ENV !== 'production') { var hyphenatedKey = hyphenate(key); if (isReservedAttribute(hyphenatedKey) || config.isReservedAttr(hyphenatedKey)) { warn( ("\"" + hyphenatedKey + "\" is a reserved attribute and cannot be used as component prop."), vm ); } defineReactive$$1(props, key, value, function () { if (!isRoot && !isUpdatingChildComponent) { warn( "Avoid mutating a prop directly since the value will be " + "overwritten whenever the parent component re-renders. " + "Instead, use a data or computed property based on the prop's " + "value. Prop being mutated: \"" + key + "\"", vm ); } }); }在initProps的时候,在defineReactive时通过判断是否在开发环境,如果是开发环境,会在触发set的时候判断是否此key是否处于updatingChildren中被修改,如果不是,说明此修改来自子组件,触发warning提示。
需要特别注意的是,当你从子组件修改的prop属于基础类型时会触发提示。 这种情况下,你是无法修改父组件的数据源的, 因为基础类型赋值时是值拷贝。你直接将另一个非基础类型(Object, array)赋值到此key时也会触发提示(但实际上不会影响父组件的数据源), 当你修改object的属性时不会触发提示,并且会修改父组件数据源的数据。
为什么修改引用类型的数据不会触发提示呢,源码里没看到对基本属性和引用类型的判断
说子组件不可以修改父组件传递的props是不够严谨的,我们在用this[key]来修改props的时候,完全可以改变该组件的vm._props,只是父组件传递的数据源是不会改变的,保证了数据源的唯一性。
原因就是要看vue源码中对props初始化的函数iniProps上。function initProps (vm: Component, propsOptions: Object) { //获取父组件传入的props对象。 const propsData = vm.$options.propsData || {} /*这里没有用 defineReactive 函数直接处理 propsDatas, 而是用一个新变量来接受props来接受 defineReactive的处理 */ const props = vm._props = {} const keys = vm.$options._propKeys = [] const isRoot = !vm.$parent if (!isRoot) { toggleObserving(false) } for (const key in propsOptions) { keys.push(key) const value = validateProp(key, propsOptions, propsData, vm) if (process.env.NODE_ENV !== 'production') { const hyphenatedKey = hyphenate(key) if (isReservedAttribute(hyphenatedKey) || config.isReservedAttr(hyphenatedKey)) { warn( `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`, vm ) } defineReactive(props, key, value, () => { if (!isRoot && !isUpdatingChildComponent) { //当存在父组件并且修改来源于子组件的时候给出警告 warn( `Avoid mutating a prop directly since the value will be ` + `overwritten whenever the parent component re-renders. ` + `Instead, use a data or computed property based on the prop's ` + `value. Prop being mutated: "${key}"`, vm ) } }) } else { defineReactive(props, key, value) } if (!(key in vm)) { /*把_props的属性经过属性代理 ,方便我们可以用 this[key] 直接访问 vm下边的_props 里边的属性*/ proxy(vm, `_props`, key) }综上,也就是说我们在用 this[key] = newVal来改变组件中的props的时候,实际改变的是vm下边的_props, 而由父组件传来的propsOptions并没有发生改变,这里应该注意的是当this._props[key]是基础数据类型的时候,this._props[key] 就是 propsData[key] 的拷贝值,当this._props[key]为引用类型的时候,this._props[key] 和 propsData[key] 指向的是同一个栈内存,**也就是说我们修改this._props[key]下的属性时候, propsData[key]也发生了改变,也就是父元素的数据发生了改变
那么如果修改了,Vue是怎么监控的呢,就是要看defineReactive函数
为什么修改引用类型的数据不会触发提示呢,源码里似乎没看到对基本属性和引用类型的判断
说子组件不可以修改父组件传递的props是不够严谨的,我们在用this[key]来修改props的时候,完全可以改变该组件的vm._props,只是父组件传递的数据源是不会改变的,保证了数据源的唯一性。
原因就是要看vue源码中对props初始化的函数iniProps上。function initProps (vm: Component, propsOptions: Object) { //获取父组件传入的props对象。 const propsData = vm.$options.propsData || {} /*这里没有用 defineReactive 函数直接处理 propsDatas, 而是用一个新变量来接受props来接受 defineReactive的处理 */ const props = vm._props = {} const keys = vm.$options._propKeys = [] const isRoot = !vm.$parent if (!isRoot) { toggleObserving(false) } for (const key in propsOptions) { keys.push(key) const value = validateProp(key, propsOptions, propsData, vm) if (process.env.NODE_ENV !== 'production') { const hyphenatedKey = hyphenate(key) if (isReservedAttribute(hyphenatedKey) || config.isReservedAttr(hyphenatedKey)) { warn( `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`, vm ) } defineReactive(props, key, value, () => { if (!isRoot && !isUpdatingChildComponent) { //当存在父组件并且修改来源于子组件的时候给出警告 warn( `Avoid mutating a prop directly since the value will be ` + `overwritten whenever the parent component re-renders. ` + `Instead, use a data or computed property based on the prop's ` + `value. Prop being mutated: "${key}"`, vm ) } }) } else { defineReactive(props, key, value) } if (!(key in vm)) { /*把_props的属性经过属性代理 ,方便我们可以用 this[key] 直接访问 vm下边的_props 里边的属性*/ proxy(vm, `_props`, key) }综上,也就是说我们在用 this[key] = newVal来改变组件中的props的时候,实际改变的是vm下边的_props, 而由父组件传来的propsOptions并没有发生改变,这里应该注意的是当this._props[key]是基础数据类型的时候,this._props[key] 就是 propsData[key] 的拷贝值,当this._props[key]为引用类型的时候,this._props[key] 和 propsData[key] 指向的是同一个栈内存,**也就是说我们修改this._props[key]下的属性时候, propsData[key]也发生了改变,也就是父元素的数据发生了改变
那么如果修改了,Vue是怎么监控的呢,就是要看defineReactive函数
为什么修改引用类型的数据不会触发提示呢,源码里似乎没看到对基本属性和引用类型的判断
因为set只能监听到值的修改,当值是一个对象时,监听不到对象的属性的修改。