18. Props
18. Props
18.1 Props 声明
一个组件需要显式声明它所接受的 props。
在使用 <script setup> 的单文件组件中,props 可以使用 defineProps() 宏来声明:
<script setup>
const props = defineProps(['foo'])
console.log(props.foo)
</script>
没有使用 <script setup> 的组件中,prop 可以使用 props 选项来声明:
export default {
  props: ['foo'],
  setup(props) {
    // setup() 接收 props 作为第一个参数
    console.log(props.foo)
  }
}
除了使用字符串数组来声明 prop 外,还可以使用对象的形式:
// 使用 <script setup>
defineProps({
  title: String,
  likes: Number
})
// 非 <script setup>
export default {
  props: {
    title: String,
    likes: Number
  }
}
key 是 prop 的名称,而值则是该 prop 预期类型的构造函数
18.2 传递 prop 的细节
18.2.1 Prop 名字格式
如果一个 prop 的名字很长,应使用 camelCase 形式
defineProps({
  greetingMessage: String
})
<span>{{ greetingMessage }}</span>
虽然理论上也可以在向子组件传递 props 时使用 camelCase 形式 (使用 DOM 模板时例外),但实际上为了和 HTML attribute 对齐,通常会将其写为 kebab-case 形式:
<MyComponent greeting-message="hello" />
对于组件名推荐使用 PascalCase,因为这提高了模板的可读性,能帮助我们区分 Vue 组件和原生 HTML 元素。
然而对于传递 props 来说,使用 camelCase 并没有太多优势,因此推荐更贴近 HTML 的书写风格。
18.2.2 静态 vs. 动态 Prop
静态值形式的 props:
<BlogPost title="My journey with Vue" />
使用 v-bind 或缩写 : 来进行动态绑定的 props:
<!-- 根据一个变量的值动态传入 -->
<BlogPost :title="post.title" />
<!-- 根据一个更复杂表达式的值动态传入 -->
<BlogPost :title="post.title + ' by ' + post.author.name" />
18.2.3 传递不同的值类型
任何类型的值都可以作为 props 的值被传递。
Number
<!-- 虽然 `42` 是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :likes="42" />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :likes="post.likes" />
Boolean
<!-- 仅写上 prop 但不传值,会隐式转换为 `true` -->
<BlogPost is-published />
<!-- 虽然 `false` 是静态的值,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :is-published="false" />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :is-published="post.isPublished" />
Array
<!-- 虽然这个数组是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost :comment-ids="[234, 266, 273]" />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :comment-ids="post.commentIds" />
Object
<!-- 虽然这个对象字面量是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JavaScript 表达式而不是一个字符串 -->
<BlogPost
  :author="{
    name: 'Veronica',
    company: 'Veridian Dynamics'
  }"
 />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :author="post.author" />
18.2.4 使用一个对象绑定多个 prop
如果想要将一个对象的所有属性都当作 props 传入,可以使用没有参数的 v-bind
只使用 v-bind 而非 :prop-name。例如,这里有一个 post 对象:
const post = {
  id: 1,
  title: 'My Journey with Vue'
}
<BlogPost v-bind="post" />
等价于:
<BlogPost :id="post.id" :title="post.title" />
18.3 单向数据流
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。
每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着不应该**在子组件中去更改一个 prop。若这么做,Vue 会在控制台上向你抛出警告:
const props = defineProps(['foo'])
// ❌ 警告!prop 是只读的!
props.foo = 'bar'
更改一个 prop 的需求通常来源于以下两种场景:
prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性。在这种情况下,最好是新定义一个局部数据属性,从 props 上获取初始值即可:
const props = defineProps(['initialCounter']) // 计数器只是将 props.initialCounter 作为初始值 // 像下面这样做就使 prop 和后续更新无关了 const counter = ref(props.initialCounter)需要对传入的 prop 值做进一步的转换。在这种情况中,最好是基于该 prop 值定义一个计算属性:
const props = defineProps(['size']) // 该 prop 变更时计算属性也会自动更新 const normalizedSize = computed(() => props.size.trim().toLowerCase())
18.3.1 更改对象 / 数组类型的 props
当对象或数组作为 props 被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。
因为 JavaScript 的对象和数组是按引用传递,而对 Vue 来说,禁止这样的改动,虽然可能生效,但有很大的性能损耗,比较得不偿失。
主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。
在最佳实践中,应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该抛出一个事件来通知父组件做出改变。
18.4 Prop 校验
Vue 组件可以更细致地声明对传入的 props 的校验要求。
如果传入的值不满足类型要求,Vue 会在浏览器控制台中抛出警告来提醒使用者。
要声明对 props 的校验,你可以向 defineProps() 宏提供一个带有 props 校验选项的对象,例如:
defineProps({
  // 基础类型检查
  // (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
  propA: Number,
  // 多种可能的类型
  propB: [String, Number],
  // 必传,且为 String 类型
  propC: {
    type: String,
    required: true
  },
  // Number 类型的默认值
  propD: {
    type: Number,
    default: 100
  },
  // 对象类型的默认值
  propE: {
    type: Object,
    // 对象或数组的默认值
    // 必须从一个工厂函数返回。
    // 该函数接收组件所接收到的原始 prop 作为参数。
    default(rawProps) {
      return { message: 'hello' }
    }
  },
  // 自定义类型校验函数
  propF: {
    validator(value) {
      // The value must match one of these strings
      return ['success', 'warning', 'danger'].includes(value)
    }
  },
  // 函数类型的默认值
  propG: {
    type: Function,
    // 不像对象或数组的默认,这不是一个
    // 工厂函数。这会是一个用来作为默认值的函数
    default() {
      return 'Default function'
    }
  }
})
提示
defineProps() 宏中的参数不可以访问 <script setup> 中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中。
- 所有 prop 默认都是可选的,除非声明了 
required: true。 - 除 
Boolean外的未传递的可选 prop 将会有一个默认值undefined。 Boolean类型的未传递 prop 将被转换为false。这可以通过为它设置default来更改——例如:设置为default: undefined将与非布尔类型的 prop 的行为保持一致。- 如果声明了 
default值,那么在 prop 的值被解析为undefined时,无论 prop 是未被传递还是显式指明的undefined,都会改为default值。 
当 prop 的校验失败后,Vue 会抛出一个控制台警告 (在开发模式下)。
18.4.1 运行时类型检查
校验选项中的 type 可以是下列这些原生构造函数:
StringNumberBooleanArrayObjectDateFunctionSymbol
另外,type 也可以是自定义的类或构造函数,Vue 将会通过 instanceof 来检查类型是否匹配。例如下面这个类:
class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
}
你可以将其作为一个 prop 的类型:
defineProps({
  author: Person
})
Vue 会通过 instanceof Person 来校验 author prop 的值是否是 Person 类的一个实例。
18.5 Boolean 类型转换
为了更贴近原生 boolean attributes 的行为,声明为 Boolean 类型的 props 有特别的类型转换规则。
以带有如下声明的 <MyComponent> 组件为例:
defineProps({
  disabled: Boolean
})
该组件可以被这样使用:
<!-- 等同于传入 :disabled="true" -->
<MyComponent disabled />
<!-- 等同于传入 :disabled="false" -->
<MyComponent />
当一个 prop 被声明为允许多种类型时,例如:
defineProps({
  disabled: [Boolean, Number]
})
无论声明类型的顺序如何,Boolean 类型的特殊转换规则都会被应用。
