Vue源码分析 子组件直接修改props会不会出现警告

1,260 阅读2分钟

直接抛结论

直接修改第一层级的props会出现警告。如下面不管props是基础类型还是复杂类型,子组件中对其直接修改this.str = xxx, this.obj = xxx都是会警告的。 但修改更深层级,即props是复杂类型,直接修改其中的某个属性是不会出现警告的。例如props是obj: {a: 'i am obj.a'},子组件中直接修改obj.a(深层级)this.obj.a = 'new aaa'是不会出现警告的。

其实是与props递归响应式监听有关。

具体看下面的例子:

// 父组件
<template>
    <child :str="str" :obj="obj" />
</template>
<script>
    export default {
        data: {
            str: "i am string",
            obj: {
                a: "i am obj.a"
            }
        },
        components: {child: () => import('./child')}
    }    
</script>
// 子组件
<template>
    <div>
        <div>str: {{str}}</div>
        <div>obj: {{obj.a}}</div>
        <div @click="changeStr">改变str</div>
        <div @click="changeObj">改变obj</div>
    </div>
</template>
<script>
    export default {
        props: ['str', 'obj'],
        methods: {
            changeStr() {
              this.str = 'new str' //出现警告
            },
            changeObj() {
              this.obj = { a: 'new aaa' } // 出现警告
            },
        },
    }
</script>

上面例子中, 不管props是基础类型还是object,我们直接修改它都是会出现警告的

但直接修改obj.a却不会出现警告

changeObj() {
    this.obj.a = 'new aaa' // 不出现警告
},

看源码研究下这是为啥呢

vue在initProps时, 在非生产环境下,defineReactive会收到第四个参数(警告函数)。

isUpdatingChildComponent 默认是false, 当父组件修改时,会触发prepatch,会把isUpdatingChildComponent修改为true(所以通过父组件修改props,警告函数也会执行但什么也不输出)。

function initProps(vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  const isRoot = !vm.$parent
  for (const key in propsOptions) {
    const value = validateProp(key, propsOptions, propsData, vm) // 验证 prop
    if (process.env.NODE_ENV !== 'production') {
      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
          )
        }
      })
    }
  }
}

defineReactive 在进行defineProperty时,如果 val 是对象的话进行递归监听。而此时是用obeseve去监听的。

也就是上面例子中porps.obj又是对象,此时会对obj使用observe去监听。而observe监听方式不会传第四个参数(警告函数), 所以直接修改obj.a不会出现警告this.obj.a = "new obj.a"

其实递归监听与监听data是一样的

function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  val = obj[key]
  // 如果 val 是对象的话递归监听
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
      ........
  })

下面是对data监听过程, 可以发现defineReactive时候是不会传第四个参数(警告函数)的

  1. initData
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // observe data
  observe(data, true /* asRootData */)
}
  1. observe
export function observe (value: any, asRootData: ?boolean): Observer | void {
  let ob: Observer | void
  ob = new Observer(value) // 创建一个监听者
  return ob
}
  1. new Observer
class Observer {
  value: any;
  dep: Dep;
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    // 判断数组是否有原型 在该处重写数组的一些方法,因为 Object.defineProperty 函数
    // 对于数组的数据变化支持的不好,
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * 监听所有属性。当值类型为Object时才应调用此方法
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * 观测数组中的每个元素
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}