首页 > 编程笔记 > Java笔记 阅读:2

Vue props属性的用法(超级详细)

使用 props 可以方便地向组件传递数据。从功能上讲,props 也可以称为组件的外部属性,通过 props 的传参差异,组件可以有很强的灵活性和扩展性。

对props属性进行验证

JavaScript 是一种非常灵活且自由的编程语言。在 JavaScript 中定义函数时,无需指定参数的类型。这种编程风格虽然为开发者提供了极大的便利,但同时也牺牲了一定的安全性。

例如,在 Vue 组件中,如果一个自定义组件需要通过 props 接收外部传入的数值,但调用方错误地传递了一个字符串类型的数据,这可能导致组件内部出现错误。

为了增强类型安全性,Vue 允许在定义组件的 props 时添加约束,对其类型、默认值、是否可选等进行配置。这有助于确保组件接收到正确类型的数据,并在数据不符合预期时提供明确的错误信息。

新建一个名为 demo.props.html 的测试文件。在其中编写如下核心代码:
<div id="Application">
  <comp1 :count="5"></comp1>
</div>

<script>
const { createApp, ref, computed } = Vue;

const App = createApp({});

const comp1 = {
  props: ["count"],          // 定义外部属性 count
  setup(props) {
    const thisCount = ref(0);

    const click = () => {
      thisCount.value += 1;
    };

    const innerCount = computed(() => {
      return props.count + thisCount.value;
    });

    return { thisCount, click, innerCount };
  },
  template: `
    <button @click="click">单击</button>
    <div>计数: {{ innerCount }}</div>
  `
};

App.component("comp1", comp1);
App.mount("#Application");
</script>
在上述代码中,定义了一个名为 count 的外部属性,这个属性在组件内实际上用于控制组件计数的初始值。需要注意,在外部传递数值类型的数据到组件内部时,必须使用 v-bind 指令的方式进行传递,直接使用 HTML 属性设置的方式会将传递的数据作为字符串传递(而不是 JavaScript 表达式)。

例如,下面的组件的使用方式,最终页面渲染的计数结果将不是预期的:
<comp1 count="5"></comp1>

虽然 count 属性的本意是作为组件内部计数的初始值,但调用方不一定能完全理解组件内部的逻辑,调用此组件时极有可能会传递非数值类型的数据,例如:
<comp1 :count="{}"></comp1>
页面渲染效果如下图所示:


图 1 组件渲染示例

可以看到,其渲染结果并不正常。在 Vue 中,我们可以对定义的 props 进行约束来显式地指定其类型。当将组件的 props 配置项配置为列表时,表示当前定义的属性没有任何约束控制,如果将其配置为对象,则可以进行更多约束设置。

修改上面代码中 props 的定义如下:
props: {
  count: {
    // 定义此属性的类型为数值类型
    type: Number,
    // 设置此属性是否必传
    required: false,
    // 设置默认值
    default: 10
  }
}
此时,在调用此组件时,如果设置 count 属性的值不符合要求,则控制台会有警告信息输出,例如 count 设置的值不是数值类型,则会抛出如下警告:
[Vue warn]: Invalid prop: type check failed for prop "count". Expected Number with value NaN, got Object
在实际开发中,我们建议所有的 props 都采用对象的方式定义,显式地设置其类型、默认值等,这样不仅可以使组件调用时更加安全,也侧面为开发者提供了组件的参数使用文档。

如果只需要指定属性的类型,而不需要指定更加复杂的性质,可以使用如下方式定义:
props: {
  // 数值类型
  count: Number,
  // 字符串类型
  count2: String,
  // 布尔值类型
  count3: Boolean,
  // 数组类型
  count4: Array,
  // 对象类型
  count5: Object,
  // 函数类型
  count6: Function
}

如果一个属性可能是多种类型,可以如下定义:
props:{
    // 指定属性类型为字符串或数值
    param:[String, Number]
}

在对属性的默认值进行配置时,如果默认值的获取方式比较复杂,也可以将其定义为函数,函数执行的结果会被作为当前属性的默认值,示例代码如下:
props: {
  count: {
    default: function () {
      return 10
    }
  }
}

Vue 中 props 的定义支持进行自定义验证。以上述代码为例,假设组件内需要接收的 count 属性的值必须大于数值 10,则可以通过自定义验证函数实现:
props: {
  count: {
    validator: function (value) {
      if (typeof value !== 'number' || value <= 10) {
        return false;
      }
      return true;
    }
  }
}
当组件的 count 属性被赋值时,会自动调用验证函数进行验证,如果验证函数返回 true,则表明此赋值是有效的,如果验证函数返回 false,则控制台会输出异常信息。

props的只读性质

你可能已经发现了,对于组件内部来说,props 是只读的。也就是说,我们不能在组件的内部修改 props 属性的值,可以尝试运行如下代码:
<script>
   const { createApp, ref, computed } = Vue;

   const App = createApp({});

   const comp1 = {
     props: {
       count: {
         validator: function (value) {
           if (typeof value !== 'number' || value <= 10) {
             return false;
           }
           return true;
         }
       }
     },
     setup(props) {
       // 只读解构,不可直接修改 props
       const { count } = props;

       // 需要响应式可改的值,应使用 ref
       const localCount = ref(count);

       const click = () => {
         localCount.value += 1;
       };

       return { localCount, click };
     },
     template: `
       <button @click="click">点击</button>
       <div>计数:{{ localCount }}</div>
     `
   };

   App.component('comp1', comp1);
   App.mount('#Application');
</script>
当 click 函数被触发时,页面上的计数并没有改变,并且控制台会抛出 Vue 警告信息。

props 的这种只读性是Vue单向数据流特性的一种体现。对于所有的外部属性,props 都只允许父组件的数据流动到子组件中,子组件的数据则不允许流向父组件。因此,在组件内部修改 props 的值是无效的。

以计数器页面为例,如果我们定义 props 只是为了设置组件某些属性的初始值,完全可以使用计算属性来进行桥接,也可以将外部属性的初始值映射到组件的内部属性上,示例代码如下:
setup(props) {
  const { count } = props;
  const thisCount = ref(count);

  const click = () => {
    thisCount.value += 1;
  };

  const innerCount = computed(() => {
    return count + thisCount.value;
  });

  return { innerCount, thisCount, click };
}

组件数据注入

数据注入是一种便捷的组件间的数据传递方式。一般情况下,当父组件需要传递数据到子组件时,我们会使用 props,但是当组件的嵌套层级很多,子组件需要使用到多层之外的父组件的数据时,就非常麻烦了,数据需要一层一层地进行传递。

新建一个名为 demo.provide.html 的测试文件,在其中编写如下核心示例代码:
<div id="Application">
  <my-list :count="5"></my-list>
</div>

<script>
   const { createApp, ref } = Vue;

   const App = createApp({});

   // 列表组件
   const listCom = {
     props: {
       count: Number          // 定义外部属性 count
     },
     // 组件模板:渲染一个列表容器
     template: `
       <div style="border: red solid 10px;">
         <my-item v-for="i in count"
                  :key="i"
                  :list-count="count"
                  :index="i">
         </my-item>
       </div>
     `
   };

   // 列表项组件
   const itemCom = {
     props: {
       listCount: Number,    // 列表项总数
       index: Number         // 当前列表项下标
     },
     template: `
       <div style="border: blue solid 10px;">
         <my-label :list-count="listCount" :index="index"></my-label>
       </div>
     `
   };

   // 标签组件
   const labelCom = {
     props: {
       listCount: Number,
       index: Number
     },
     template: `<div>{{ index }}/{{ listCount }}</div>`
   };

   // 注册组件
   App.component("my-list", listCom);
   App.component("my-item", itemCom);
   App.component("my-label", labelCom);

   App.mount("#Application");
</script>
在上述代码中,我们创建了 3 个自定义组件,my-list 组件用来创建一个列表视图,其中每一行的元素为 my-item 组件,在 my-item 组件中又使用了 my-label 组件进行文本显示。列表中的每一行会渲染出当前的行数以及总行数。

运行上述代码,页面效果如下图所示:


图 2 自定义列表组件

运行上述代码本身没有什么问题,烦琐的地方在于 my-label 组件中需要使用到 my-list 组件中的 count 属性,通过使用 my-item 组件,数据才能顺利进行传递。随着组件嵌套层数的增多,数据的传递将越来越复杂。对于这种场景,我们可以使用数据注入的方式来跨层级进行数据传递。

所谓数据注入,是指父组件可以向其所有子组件提供数据,不论在层级结构上此子组件的层级有多深。以上面的代码为例,my-label 组件可以跳过 my-item 组件,直接使用 my-list 组件中提供的数据。

实现数据注入,需要使用 provide 与 inject 两个函数,提供数据的父组件需要设置 provide 配置项来提供数据,子组件需要设置 inject 配置项来获取数据。修改上述代码如下:
<script>
   const { createApp, ref, provide, inject } = Vue;

   const App = createApp({});

   const listCom = {
     props: {
       count: Number
     },
     setup(props) {
       // 这里需要提供组件内共享的数据
       provide("listCount", props.count);
     },
     template: `
       <div style="border: red solid 10px;">
         <my-item v-for="i in count" :key="i" :index="i"></my-item>
       </div>
     `
   };

   const itemCom = {
     props: {
       index: Number
     },
     template: `
       <div style="border: blue solid 10px;">
         <my-label :index="index"></my-label>
       </div>
     `
   };

   const labelCom = {
     props: {
       index: Number
     },
     setup() {
       // 此组件需要使用 provide 提供的数据,使用 inject 进行注入
       const listCount = inject("listCount");
       return { listCount };
     },
     template: `
       <div>{{ index }}/{{ listCount }}</div>
     `
   };

   App.component("my-list", listCom);
   App.component("my-item", itemCom);
   App.component("my-label", labelCom);

   App.mount("#Application");
</script>
运行代码,程序依然可以很好地运行。使用数据注入的方式传递数据时,父组件不需要了解哪些子组件要使用这些数据,同样子组件也无须关心所使用的数据来自哪里。一定程度来说,这使代码的可控性降低了。因此,在实际开发中,我们要根据场景来决定使用怎样的方式来传递数据,而不是滥用注入技术。

相关文章