目录
一、组件注册
一个 Vue 组件在使用前需要先被“注册”,这样 Vue 才能在渲染模板时找到其对应的实现。组件注册有两种方式:全局注册和局部注册。
1.全局注册
我们可以使用Vue应用实例的app.component()方法,让组件在当前Vue应用中全局可用。
import { createApp } from 'vue'
const app = createApp({})
app.component(
// 注册的名字
'MyComponent',
// 组件的实现
{
/* ... */
}
)
如果使用单文件组件,你可以注册被导入的.vue文件:
import MyComponent from './App.vue'
app.component('MyComponent', MyComponent)
app.component()方法可以被链式调用:
app
.component('ComponentA', ComponentA)
.component('ComponentB', ComponentB)
.component('ComponentC', ComponentC)
全局注册的组件可以在此应用的任意组件的模板中使用,所有的子组件也可以使用全局注册的组件,这意味着这三个组件也都可以在彼此内部使用。
2.局部注册
全局注册虽然很方便,但有以下几个问题:
- 全局注册,但并没有被使用的组件无法在生产打包时被自动移除(也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后JS文件中。
- 全局注册在大型项目中使用项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。
相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对tree-shaking更加友好。
在使用<script setup>的单文件组件中,导入的组件可以直接在模板中使用,无需注册:
<script setup>
import ComponentA from './ComponentA.vue'
</script>
<template>
<ComponentA />
</template>
如果没有使用<script setup>,则需要使用<components>选项来显式注册:
import ComponentA from './ComponentA.js'
export default {
components: {
ComponentA
},
setup() {
// ...
}
}
对于每个components对象里的属性,它们的key名就是注册的组件名,而值就是相应组件的实现。上面的例子中使用的是ES2015的缩写语法。且局部注册的组件在后代组件中并不可用。
二、Props
一个组合需要显式声明它所接受的props,这样Vue才能知道外部传入的哪些是props,哪些是透传attribute。
在使用<script setup>的单文件组件中,props可以使用definePorps()宏来声明:
<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)
}
}
注意传递给defineProps()的参数和提供给props选项的值是相同的,两种声明方式背后其实使用的都是prop选项。
除了使用字符串数组来声明prop处,还可以使用对象的形式:
// 使用 <script setup>
defineProps({
title: String,
likes: Number
})
// 非 <script setup>
export default {
props: {
title: String,
likes: Number
}
}
对于以对象形式声明中的每个属性,key是prop的名称,而值则是该prop预期类型的构造函数。比如,如果要求一个prop的值是number类型,则可使用number构造函数作为其声明的值。
对象形式的props声明不仅可以一定程度上作为组件的文档,而且如果其他开发者在使用你的组件时传递了错误的类型,也会在浏览器控制台中抛出警告。
使用一个对象绑定多个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" />
单向数据流
所有的props都遵循着单向绑定原则,props因父组件的更新而变化,自然地将新的状态向下流入子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱。
另外,每次父组件更新后,所有的子组件中的props都会被更新到最新值,这意味着不应该在子组件中去更改一个prop。若去更改,会警告:
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())
更改对象/数组类型 的props
当对象或数组作为props被传入时,虽然子组件无法更改props绑定,但仍然可以更改对象或数组内部的值。这是因为JS的对象和数组是按引用传递,而对Vue来说,禁止这样的改动,虽然可能生效,但有很大的性能损耗,比较得不偿失。
这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该抛出一个事件来通知父组件做出改变。