首页 > 编程笔记

Vue prop属性:父组件向子组件传递数据

在 Vue 中,组件是当作自定义元素来使用的,而元素一般是有属性的,同样组件也可以有属性。

在使用组件时,给元素设置属性,组件内部如何接收呢?首先需要在组件代码中注册一些自定义的属性,称为 prop,这些 prop 是在组件的 props 选项中定义的;之后,在使用组件时,就可以把这些 prop 的名字作为元素的属性名来使用,通过属性向组件传递数据,这些数据将作为组件实例的属性被使用。

prop的基本用法

下面看一个示例,使用 prop 属性向子组件传递数据,这里传递“庭院深深深几许,云窗雾阁春迟。”,在子组件的 props 选项中接收 prop 属性,然后使用插值语法在模板中渲染 prop 属性。
<div id="app">
    <blog-content date-title="庭院深深深几许,云窗雾阁春迟。"></blog-content>
</div>
<!--引入Vue文件-->
<script src="https://unpkg.com/vue@next"></script>
<script>
    //创建一个应用程序实例
    const vm= Vue.createApp({});
    vm.component('blog-content', {
      props: ['dateTitle'],
      //date-title就像data定义的数据属性一样
      template: '<h3>{{ dateTitle }}</h3>',
      //在该组件中可以使用“this.dateTitle”这种形式调用prop属性
      created(){
          console.log(this.dateTitle);
      }
    });
    //在指定的DOM元素上装载应用程序实例的根组件
    vm.mount('#app');
</script>
在 Chrome 浏览器中运行程序,效果如下图所示。


图 1 使用prop属性向子组件传递数据

HTML 中的 attribute 名是不区分大小写的,所以浏览器会把所有大写字符解释为小写字符,prop 属性也适用这种规则。当使用 DOM 中的模板时,dateTitle(驼峰命名法)的 prop 名需要使用其等价的 date-title(短横线分隔命名)命名。

上面的示例中,使用 prop 属性向子组件传递了字符串值,还可以传递数字。这只是它的一个简单用法。通常情况下,可以使用 v-bind 来传递动态的值,传递数组和对象时也需要使用 v-bind 指令。

修改上面的示例,在 Vue 实例中定义 title 属性,以传递到子组件中去。示例代码如下。
<div id="app">
  <blog-content v-bind:date-title="content"></blog-content>
</div>
<!--引入Vue文件-->
<script src="https://unpkg.com/vue@next"></script>
<script>
  //创建一个应用程序实例
   const vm= Vue.createApp({
      //该函数返回数据对象
      data(){
        return{
          content:"玉瘦檀轻无限恨,南楼羌管休吹。"
         }
      }
  });
  vm.component('blog-content', {
      props: ['dateTitle'],
      template: '<h3>{{ dateTitle }}</h3>',
    });
  //在指定的DOM元素上装载应用程序实例的根组件
  vm.mount('#app');
</script>
在 Chrome 浏览器中运行程序,效果如下图所示。


图 2 传递title属性到子组件

在上面的示例中,在 Vue 实例中向子组件中传递数据,通常情况下多用于组件向组件传递数据。下面的示例创建了两个组件,在页面中渲染其中一个组件,而在这个组件中使用另一个组件,并传递 title 属性。
<div id="app">
  <!--使用blog-content组件-->
  <blog-content></blog-content>
</div>
<!--引入Vue文件-->
<script src="https://unpkg.com/vue@next"></script>
<script>
  //创建一个应用程序实例
  const vm= Vue.createApp({ });
  vm.component('blog-content', {
      // 使用blog-title组件,并传递content
      template: '<div><blog-title v-bind:date-title="title"></blog-title>
</div>',
      data:function(){
          return{
              title:"明朝准拟南轩望,洗出庐山万丈青。"
          }
      }
  });
  vm.component('blog-title', {
      props: ['dateTitle'],
      template: '<h3>{{ dateTitle }}</h3>',
  });
  //在指定的DOM元素上装载应用程序实例的根组件
  vm.mount('#app');
</script>
在 Chrome 浏览器中运行程序,效果如下图所示。


图 3 组件之间传递数据

如果组件需要传递多个值,则可以定义多个 prop 属性。
<div id="app">
  <!--使用blog-content组件-->
  <blog-content></blog-content>
</div>
<!--引入Vue文件-->
<script src="https://unpkg.com/vue@next"></script>
<script>
  //创建一个应用程序实例
  const vm= Vue.createApp({ });
  vm.component('blog-content', {
      // 使用blog-title组件,并传递content
      template: '<div><blog-title :name="name" :price="price" :num="num">
</blog-title></div>',
      data:function(){
          return{
              name:"苹果",
              price:"6.88元",
             num:"2800公斤"
          }
      }
  });
  vm.component('blog-title', {
      props: ['name','price','num'],
      template: '<ul><li>{{name}}</li><li>{{price}}</li><li>{{num}}</li></ul> ',
  });
  //在指定的DOM元素上装载应用程序实例的根组件
  vm.mount('#app');
</script>
在 Chrome 浏览器中运行程序,效果如下图所示。


图 4 传递多个值

从上面的示例可以看到,代码以字符串数组形式列出多个 prop 属性:
props: ['name','price','num'],
但是,通常希望每个 prop 属性都有指定的值类型。这时,可以以对象形式列出 prop,这些 property 的名称和值分别是 prop 各自的名称和类型,例如:
props: {
  name: String,
  price: String,
  num: String,
}

单向数据流

所有的 prop 属性传递数据都是单向的。父组件的 prop 属性的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的数据,从而导致应用的数据流向难以理解。

另外,每次父级组件发生变更时,子组件中所有的 prop 属性都将会刷新为最新的值。这意味着不应该在一个子组件内部改变 prop 属性。如果这样做,Vue 会在浏览器的控制台中发出警告。

有两种情况可能需要改变组件的 prop 属性。第一种情况是定义一个 prop 属性,以方便父组件传递初始值,在子组件内将这个 prop 作为一个本地的 prop 数据来使用。遇到这种情况,解决办法是在本地的 data 选项中定义一个属性,然后将 prop 属性值作为其初始值,后续操作只访问这个 data 属性。示例代码如下:
props: ['initDate'],
data: function () {
  return {
      title: this.initDate
  }
}
第二种情况是 prop 属性接收数据后需要转换后使用。这种情况可以使用计算属性来解决。示例代码如下:
props: ['size'],
computed: {
  nowSize:function(){
      return this.size.trim().toLowerCase()
  }
}
后续的内容直接访问计算属性 nowSize 即可。

在 JavaScript 中,对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 属性来说,在子组件中改变这个对象或数组本身将会影响父组件的状态。

prop验证

当开发一个可复用的组件时,父组件希望通过 prop 属性传递的数据类型符合要求。例如,组件定义一个 prop 属性是一个对象类型,结果父组件传递的是一个字符串的值,这明显不合适。

因此,Vue.js 提供了prop属性的验证规则,在定义 props 选项时,使用一个带验证需求的对象来代替之前使用的字符串数组(props: ['name','price','city'])。代码如下:
vm.component('my-component', {
  props: {
      // 基础的类型检查 ('null'和'undefined' 会通过任何类型验证)
      name: String,
      // 多个可能的类型
      price: [String, Number],
      // 必填的字符串
      city: {
          type: String,
          required: true
      },
      // 带有默认值的数字
      prop1: {
          type: Number,
          default: 100
      },
      // 带有默认值的对象
      prop2: {
          type: Object,
          // 对象或数组默认值必须从一个工厂函数获取
          default: function () {
              return { message: 'hello' }
          }
      },
      // 自定义验证函数
      prop3: {
          validator: function (value) {
              // 这个值必须匹配下列字符串中的一个
              return ['success', 'warning', 'danger'].indexOf(value) !== -1
          }
      }
  }
})
为组件的 prop 指定验证要求后,如果有一个需求没有被满足,则 Vue 会在浏览器控制台中发出警告。

上面代码验证的 type 可以是下面原生构造函数中的一个:
String
Number
Boolean
Array
Object
Date
Function
Symbol
另外,type 还可以是一个自定义的构造函数,并且通过 instanceof 来进行检查确认。例如,给定下列现成的构造函数:
function Person (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}
可以通过以下代码验证name的值是不是通过new Person创建的。
vm.component('blog-content', {
  props: {
      name: Person
  }
})

非prop的属性

在使用组件的时候,父组件可能会向子组件传入未定义 prop 的属性值,这样也是可以的。组件可以接收任意的属性,而这些外部设置的属性会被添加到子组件的根元素上。

示例代码如下:
<style>
  .bg1{
      background: #C1FFE4;
  }
  .bg2{
      width: 120px;
  }
</style>
<div id="app">
  <!--使用blog-content组件-->
  <input-con class="bg2" type="text"></input-con>
</div>
<!--引入Vue文件-->
<script src="https://unpkg.com/vue@next"></script>
<script>
  //创建一个应用程序实例
  const vm= Vue.createApp({ });
  vm.component('input-con', {
     template: '<input class="bg1">',
  });
  //在指定的DOM元素上装载应用程序实例的根组件
  vm.mount('#app');
</script>
在 Chrome 浏览器中运行程序,输入“九曲黄河万里沙”,打开控制台,效果如下图所示。


图 5 非prop的属性

从上面的示例可以看出,input-con 组件没有定义任何 prop,根元素是 <input>,在 DOM 模板中使用 <input-con> 元素时设置了 type 属性,这个属性将被添加到 input-con 组件的根元素 input 上,渲染结果为 <input type="text">。

另外,在 input-con 组件的模板中还使用了 class 属性 bg1,同时在 DOM 模板中也设置了 class 属性 bg2,这种情况下,两个 class 属性的值会被合并,最终渲染的结果为 <input class="bg1 bg2" type="text">。

要注意的是,只有 class 和 style 属性的值会合并,对于其他属性而言,从外部提供给组件的值会替换掉组件内容设置好的值。假设 input-con 组件的模板是 <input type="text">,如果父组件传入 type="password",就会替换掉 type="text",最后渲染结果就会变成 <input type="password">。

例如修改上面的示例:
<div id="app">
  <!--使用blog-content组件-->
  <input-con class="bg2" type=" password "></input-con>
</div>
<!--引入Vue文件-->
<script src="https://unpkg.com/vue@next"></script>
<script>
  //创建一个应用程序实例
  const vm= Vue.createApp({ });
  vm.component('input-con', {
     template: '<input class="bg1" type="text">',
  });
  //在指定的DOM元素上装载应用程序实例的根组件
  vm.mount('#app');
</script>
在 Chrome 浏览器中运行程序,然后输入“12345678”,可以发现 input 的类型为“password”,效果如图 6 所示。


图 6 外部组件的值替换掉组件设置好的值

如果不希望组件的根元素继承外部设置的属性,可以在组件的选项中设置 inheritAttrs: false。例如修改上面的示例代码:
Vue.component('input-con', {
  template: '<input class="bg1" type="text">',
  inheritAttrs: false,
});
再次运行项目,可以发现父组件传递的 type="password" 子组件并没有继承。

推荐阅读