Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

第 40 题:在 Vue 中,子组件为何不可以修改父组件传递的 Prop,如果修改了,Vue 是如何监控到属性的修改并给出警告的。 #60

Open
IWANABETHATGUY opened this issue Mar 25, 2019 · 17 comments
Labels

Comments

@IWANABETHATGUY
Copy link
Contributor

No description provided.

@IWANABETHATGUY
Copy link
Contributor Author

  1. 子组件为何不可以修改父组件传递的 Prop
    单向数据流,易于监测数据的流动,出现了错误可以更加迅速的定位到错误发生的位置。
  2. 如果修改了,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的属性时不会触发提示,并且会修改父组件数据源的数据。

@zeroone001
Copy link

子组件为何不可以修改父组件传递的 Prop?-> 因为每当父组件属性值修改时,该值都将被覆盖;如果要有不同的改变,可以用基于prop的data或者computed

@CodeDreamfy
Copy link

貌似是 极客时间 里面 那个作者提出来的问题~ 不过楼主你真的很认真哟~

@LzOo0oO
Copy link

LzOo0oO commented Apr 15, 2019

漂亮, 还可以这么玩!

@leecin
Copy link

leecin commented Apr 17, 2019

@LzOo0oO,怎么玩?来,come on!举个例子?

@chaijinsong
Copy link

chaijinsong commented Jun 15, 2019

为何不能修改:为了保证数据的单向流动,便于对数据进行追踪,避免数据混乱。官网有详细的信息 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不会抱警告。

我的博客

@Gloomysunday28
Copy link

然而 props传入的是对象的话 是可以直接在子组件里更改的, 因为是同一个引用
组件对于data的监听是深度监听
而对于props的监听是浅度监听

@negativeentropy9
Copy link

这道题不错

@cooldrangon
Copy link

个人感觉 就从父组件底下有未知个子组件 如果子组件可以直接修改父组件的值 别的子组件也依赖父组件的这个值的话就乱了 ,不好追踪, 子组件通过$emit方式实际还是父组件修改好了再通过数据流传过来而已

@yesixuan
Copy link

原因很简单,一个父组件下不只有你一个子组件。
同样,使用这份 prop 数据的也不只有你一个子组件。
如果每个子组件都能修改 prop 的话,将会导致修改数据的源头不止一处。

所以我们需要将修改数据的源头统一为父组件,子组件像要改 prop 只能委托父组件帮它。从而保证数据修改源唯一

@GoodLuckAlien
Copy link

GoodLuckAlien commented Aug 5, 2019

说子组件不可以修改父组件传递的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函数

image

@zzNire
Copy link

zzNire commented Aug 8, 2019

然而 props传入的是对象的话 是可以直接在子组件里更改的, 因为是同一个引用
组件对于data的监听是深度监听
而对于props的监听是浅度监听

props也是深度数据响应吧,用的同一个defineReactive方法来实现

@lvzhiyi
Copy link

lvzhiyi commented Aug 19, 2019

vue数据传递是单项数据流,如果父组件传递过来的props为引用类型,直接修改了props的值会导致其他用到此props的子组件的值也发生改变,导致数据混乱,以及难以追踪的bug。如果props是基础类型,不会造成其他影响,但也会是整个组件数据混乱

@daiyunchao
Copy link

原因很简单,一个父组件下不只有你一个子组件。
同样,使用这份 prop 数据的也不只有你一个子组件。
如果每个子组件都能修改 prop 的话,将会导致修改数据的源头不止一处。

所以我们需要将修改数据的源头统一为父组件,子组件像要改 prop 只能委托父组件帮它。从而保证数据修改源唯一

那如果父级传入到子组件的不是一个引用而是一个copy的对象,是不是子组件就可以改呢? 这样是不是就不会影响到其他的子组件了呢?
我觉得props能不能改只是框架的自己的一种设计,当然它也给了不能修改的理由

@duntiansuo
Copy link

  1. 子组件为何不可以修改父组件传递的 Prop
    单向数据流,易于监测数据的流动,出现了错误可以更加迅速的定位到错误发生的位置。
  2. 如果修改了,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的属性时不会触发提示,并且会修改父组件数据源的数据。

为什么修改引用类型的数据不会触发提示呢,源码里没看到对基本属性和引用类型的判断

@duntiansuo
Copy link

说子组件不可以修改父组件传递的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函数

image

为什么修改引用类型的数据不会触发提示呢,源码里似乎没看到对基本属性和引用类型的判断

@yygmind yygmind added the Vue label Dec 16, 2019
@lovyliu
Copy link

lovyliu commented Mar 12, 2021

说子组件不可以修改父组件传递的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函数
image

为什么修改引用类型的数据不会触发提示呢,源码里似乎没看到对基本属性和引用类型的判断

因为set只能监听到值的修改,当值是一个对象时,监听不到对象的属性的修改。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests