||

Vue组件间对象传递时需要注意的一些点及建议

这个问题简单来说,就是组件间传值,如果传递的是对象,应该怎么处理这个对象更加合理。之所以将它当做问题,是因为在我了解的开发中,很多开发者对对象传值使用的过分随意,当多人开发一个大需求的时候,经常会遇到对象的值莫名其妙的被改变。

问题

这是个问题,或者说也不是问题,因人而异,但本着对代码的高要求,本文将它当做问题。

Vue中虽然崇尚的单向数据流,即数据从父组件单向传递给子组件,尽量避免双向或者逆向传递。这里所说的双向或者逆向传递,是只在开发过程中,直接通过字面量的形式改变 props 中的数据。

单向数据流是 Vue 推荐的设计原则,这是建议,不是强制,所以当我们对 props 中的数据进行改变时,开发模式下 Vue 会发出警告,告诉我们这是不合理的,以便引起我们的注意。

但很多开发者不知道,这种拦截,只是对第一层链条进行的拦截,而无法深层次的拦截。

举个例子:

<template>
  <child-component :data="fromData"></child-component>
</template>

<script>
export default {
  data() {
    return {
      formData: {
        country: 'xxx',
        other: 'xxx'
      },
    };
  }
}
</script>

上面的代码中,父组件给子组件传递了一个 formData 对象,子组件通过 props 中的 data 字段来接收。

假如子组件有如下代码:

<script>
export default {
  props: {
    data: {
      type: Object,
      default: null;
    },
  },
  created() {
    this.data = 'some new data here';
  }
}
</script>

那么,很明显,在组件初始化时控制台会打出警告,告诉你不能改变 data 的数据,因为它是从 props 定义,是一个接收入参的接口。

但是,假如改变的不是 data 本身,而是它的子字段,那么就不会产生警告,例如如下代码:

// ...
created() {
  this.data.other = 'some new data here';
}
// ...

因为上述代码没有涉及到访问 this.data 的 setter,而是访问到了 this.data 的 getter 和 this.data.other 的 setter 。这就出现了一个问题,在不知不觉间,我们改变了父组件中的数据,假如这个数据有 watch 监听,或者 computed 计算属性,那么这个变动将直接导致父组件莫名其妙的更新,很多时候,这既不符合交互要求,也不满足设计规范。

理解组件

总的来说,两种不同类型的传值产生不用的效果:

  • 引用类型传值,例如对象,数组等;
  • 非引用类型传值,例如数字,字符串,布尔值等;

对于非引用类型的传值,Vue中对 props 的修改拦截只能在第一层引用上完成,即通过 props 传入的那一层,当你尝试修改 props.xxx 值得时候,会被 Vue 监测到并给出警告。但是,对于 props.xxx.key 这种修改 Vue 是无法监测到的,因为这个对象的观察者并不在当前实例中。

处理此类问题,首先可以思考为什么要改变 props 的值。Vue 设计之初不建议逆向改变赋值就是为了能让数据驱动的流程清晰明了,所以但凡需要直接通过修改 props 逆向赋值的行为,都是不合理的。

Vue 中提供的逆向赋值的方案是通过事件来处理,为此 Vue 也提供了便捷的指令和语法糖,例如常见的两种:

  • v-bind.sync 指令加修饰符;
  • v-model 指令;

Vue 组件本身就是一个实例,可以将组件理解为一个函数,所以组件间的传值就是函数间的传值。函数有出参入参,组件也有出参入参。对组件而言,props 中的定义就是入参,需要进行逆向改变数据而触发的事件就是出参。

解决数据见的传递问题,可以先从组件本身来思考,为此我将组件分为三类:

  • 展示型组件:有入参无出参;
  • 功能型组件:有入参有出参,但出入参数据往往有直接对应关系;
  • 业务性组件:有入参有出参,但出参与入参可能没有直接对应关系;

建议

非引用类型传值

非引用类型的赋值常见于展示型组件和功能型组件。

展示型组件如 alert 弹框,error message 等。

功能型组件如 input 、select 组件等都是功能型组件。这类组件往往输入就是输出,输出也就是输入,输入输出有直接关系且需要直接绑定并同步。

展示型组件只需要展示信息,不需要更改信息,也就是只需要使用 props 中的数据做展示,无需修改数据。这类组件直接使用 props 传值即可,无需过多处理。

功能型组件的初始值是 props 中传入的值,但是修改值却不在 props 上,对于这类组件,需要在组件内部先截断 props 的引用,一般处理方式是在组建中通过 data 重新生成一个内部使用的值,这个值的初始值从 props 中获取,并监听 props 中数据的变化。

以 input 组件为例:

<script>
export default {
  props: {
    value: {
      type: [String, Number],
      default: '';
    },
  },
  data() {
    return {
      innerValue: this.value, // 将直接修改 value 的值变成修改 innerValue 的值
    };
  },
  watch: {
    value(newVal) {
      this.innerValue = newVal;
    },
  },
}
</script>

引用类型传值

引用类型传值常见于业务性组件。

业务型组件具备一定的业务能力,完成一定的业务功能,所以往往入参也更多,常见的如 form 表单组件。

业务型组件常常会使用对象作为入参。但常见的问题是,这个入参对象在其他组件或者父组件中也有使用,那么在子组件中对对象的改变,可能会影响到其他组件,这个使用建议在 data 中对 props 的对象入参进行深拷贝,以便切断和父组件的联系,这时组件内部对入参的改变不应影响到其他组件。修改完毕后,如果需要通知到父组件,可以借助事件进行传递。

<script>
export default {
  props: {
    formData: {
      type: Object,
      default: () => null;
    },
  },
  data() {
    return {
      innerFormData: simplyDeepCopy(this.formData), // 将入参对象进行深拷贝,防止后续对 props 入参的修改
    };
  },
}
</script>

引申

其实在本篇博文中也隐含了一个开发者常见的问题,就是对数据的无意修改。

例如,在之前的开发中,遇到同事处理一个表格渲染,无论这么排查,渲染数据都不是后台返回的数据,这里面其实就是另一个组件中业务中需要处理这个数据,结果处理的数据之间被体现在表格中,导致数据意外的改变。

后面的有空可以介绍一下平时 debug 的一些技巧。

类似文章