Vue3 组件化开发(5)——组件间的通信(2)

210 阅读5分钟

「这是我参与2022首次更文挑战的第26天,活动详情查看:2022首次更文挑战」。

父传子 - props 对象用法

前面 props 选项使用一个字符串数组时,只能说明传入的 attribute 的名称,并不能对其进行任何形式的限制,接下来我们来看一下使用对象的写法是如何让 props 变得更加完善的。

当使用对象语法时,我们可以对传入的内容做更多限制:

  • 比如指定传入的 attribute 值的类型
  • 比如指定传入的 attribute 值是否要求必传
  • 比如设置某个 attribute 的值没有传入时的(该 attribute 的)默认值

所以在真实开发中,我们注册 props 时,一般不写字符串数组,而是会写对象:

<template>
  <div>
    <h2>{{ title }}</h2>
    <p>{{ content }}</p>
  </div>
</template>

<script>
  export default {
    // props: ['title', 'content']
    props: {
      // 1. 指定类型(null 和 undefined 值会通过任何类型验证)
      title: String,
      // 2. 指定多个可能的类型
      propA: [String, Number],
      // 3. 指定类型,同时指定是否要求必传
      content: {
        type: String,
        required: true
      },
      // 4. 指定类型,同时指定默认值
      propB: {
        type: String,
        default: '我是内容'
      }
    }
  }
</script>

<style scoped>

</style>

requireddefault 一般设置其中一个即可,要么是必传的,如果没有传就会报警告,要么不传值,那么就会使用默认值。

props 对应的值写成对象时,还有几点细节:

  1. type 的类型可以是下面原生构造函数中的一个:

    • String
    • Number
    • Boolean
    • Array
    • Object
    • Date
    • Function
    • Symbol

    还可以是一个自定义的构造函数

  2. 还有一些其它写法:

    <script>
      export default {
        props: {
          // 指定为对象类型,同时指定默认值
          propC: {
            type: Object,
            // 对象或数组的默认值必须从一个工厂函数返回
            default() {
              return { message: '你好~' }
            }
          },
          // 自定义验证函数
          propD: {
            validator(value) {
              // 传入的值必须是下列字符串中的其中一个
              return ['success', 'warning', 'danger'].includes(value)
            }
          },
          // 指定为函数类型,同时指定默认函数
          propE: {
            type: Function,
            // 与对象或数组的默认值不同,这不是一个工厂函数,而是一个用作默认值的函数
            default() {
              return '我是默认函数'
            }
          }
        }
      }
    </script>
    

    注意:prop 指定为对象类型同时指定默认值时,默认值不能直接是一个对象:

    <script>
      export default {
        props: {
          info: {
            type: Object,
            // 存在问题的写法:default 直接用一个对象赋值
            default: { name: 'zhj' }
          }
        }
      }
    </script>
    

    像上面这样写虽然不会报错,但是会存在问题:假如上面的这个 info 属性是在 ShowMessage.vue 组件中注册的,而我们很有可能会多次使用到这个组件,比如我们使用了 3ShowMessage.vue 组件,那就意味着到时候会根据该组件创建出 3 个该组件的实例对象,而这 3 个组件实例中其实都会有一个 props 属性,而且 props 属性中还都有一个 info 属性。那么,如果这 3 次使用 ShowMessage.vue 组件时都没有给组件的 info 属性(attribute)传值,此时它们就都会使用默认值对应的这个对象,这时相当于引用赋值,即这 3 个组件实例的 props.info指向了同一个对象,那么这时假如修改了其中一个组件实例中的 props.info 对象中的 name 属性的值,其它两个组件实例中的 props.info 对象中的 name 属性的值也会被修改掉。

    简单来说,default 直接用一个对象赋值的问题是:我们明明修改的是 ShowMessage.vue 组件的实例 A 中的某个 prop 的值,结果却连同 ShowMessage.vue 组件的其它实例中的相应 prop 的值(前提是没有传值,使用了默认值对象)也都被修改掉了。

    因此,type 为引用类型(这里指 ObjectArray)时,如果要指定默认值,默认值应该从一个工厂函数返回(默认值要写成一个函数,然后在函数中返回真正的默认值):

    <script>
      export default {
        props: {
          info: {
            type: Object,
            // 对象或数组的默认值必须从一个工厂函数返回
            default() {
              return { name: 'zhj' }
            }
          }
        }
      }
    </script>
    

    像上面这样将 default 属性设置成一个函数,再在函数中返回默认值,这样就没问题了。因为之后在多次使用该组件时,如果组件上没有给 info 属性传值,就会执行组件内 info prop 中的这个 default() 函数,而每次执行该函数,都会返回一个新的对象{ name: 'zhj' }),也就意味着多次使用该组件时,各个组件实例中使用的都是自己的对象({ name: 'zhj' }),所以修改时就只会修改自己的对象。

  3. prop 的大小写(camelCase vs kebab-case

    • 因为 HTML 中的 attribute 名称是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符;

      • 举个例子:

        <div class="container"></div>
        <div CLASS="container"></div>
        <div ClAsS="container"></div>
        

        HTML 中,上面的三个 <div> 元素中大小写各不相同的 class 属性经过浏览器解析后都会变成小写形式:

        image-20220103160519171

    • 这意味着当你使用 DOM 中的模板时驼峰命名法camelCase)的 prop 名称需要使用其等价的短横线分隔命名法kebab-case)命名;

      • DOM 中的模板指的是直接在 DOM 中编写的模板,比如:

        <template id="my-app">
          <show-message message-info="abc"></show-message>
        </template>
        

        不包括下面三种情况:

        1. 字符串模板(比如直接把模板内容以字符串的形式赋值给 template 选项);
        2. 模板编写在单文件组件(即 .vue 文件)中;
        3. 模板编写在 <script type="x-template"></script> 中;
      • 举个例子:

        假如你在 ShowMessage 组件中注册了一个名为 messageInfo 的属性:

        <script src="../js/vue.js"></script>
        <script>
          const ShowMessage = {
            props: {
              messageInfo: {
                type: String
              }
            },
            template: '#show-message'
          };
          const App = {
            template: '#my-app',
            // 注册局部组件
            components: {
              ShowMessage
            },
            data() {
              return {
                message: 'Hello World'
              }
            }
          };
          const app = Vue.createApp(App)
          app.mount('#app');
        </script>
        

        那么在将 Vue 模板直接编写在 DOM时,你需要这样使用:

        <template id="my-app">
          <!-- messageInfo 需要写成小写字母并用短横线分隔的形式 -->
          <show-message message-info="abc"></show-message>
        </template>
        

        这样被 Vue 解析后,ShowMessage 组件内部接收到的就会是 messageInfo,之后才能正常使用。而如果使用 ShowMessage 组件时,给 messageInfo 属性传值时还是写成 messageInfo,那么由于浏览器原生的 HTML 解析行为,HTML attribute 名不区分大小写,因此浏览器会将所有大写字符解释为小写,所以 ShowMessage 组件内部接收到的就会是 messageinfo,而不是 messageInfo,这就会导致 messageInfo prop 没有成功接收到传入的值。

        当然,开发中我们一般不会直接在 DOM 中编写模板,而是会.vue 文件中编写模板。这时假如你在 ShowMessage.vue 组件中注册了一个名为 messageInfo 的属性:

        <script>
          export default {
            props: {
              messageInfo: {
                type: String
              }
            }
          }
        </script>
        

        那么在使用这个组件时,在给 messageInfo 属性传值时,messageInfo 这个属性名既可以写成 messageInfo(驼峰命名法),也可以写成 message-info(短横线分隔命名法),因为这里 .vue 文件会交给 vue-loader 解析,而不是交给浏览器解析(浏览器解析的话就会把大写字母转换成小写字母)。但在开发中,如果注册的属性名是驼峰格式的,给其传值时建议将其改写成短横线分隔的格式(如 message-info),因为这种写法也是直接在 DOM 中编写模板时的正确写法。

        <template>
          <div>
            <show-message title="哈哈哈" content="我是哈哈哈" :message-info="'hhh'"></show-message>
            <show-message title="哈哈哈" content="我是哈哈哈" :messageInfo="'hhh'"></show-message>
          </div>
        </template>