本文都将采用Vue3的写法来演示。
props和emits
props
传递静态或动态的 Prop
<blog-post title="My journey with Vue"></blog-post>
<blog-post :title="post.title"></blog-post>
如果想要将一个对象的所有 property 都作为 prop 传入,可以使用不带参数的 v-bind (用 v-bind 代替 :prop-name)。例如,对于一个给定的对象 post:
post: {
id: 1,
title: 'My Journey with Vue'
}
<blog-post v-bind="post"></blog-post>
等价于:
<blog-post v-bind:id="post.id" v-bind:title="post.title"></blog-post>
单向数据流 所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。
注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态,且 Vue 无法为此向你发出警告。作为一个通用规则,应该避免修改任何 prop,包括对象和数组,因为这种做法无视了单向数据绑定,且可能会导致意料之外的结果。
这里有两种常见的试图变更一个 prop 的情形:
- 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。
- 这个 prop 以一种原始的值传入且需要进行转换。
props: ['initialCounter', 'size'],
data() {
return {
counter: this.initialCounter
}
}
computed: {
normalizedSize() {
return this.size.trim().toLowerCase()
}
}
emits
<template>
<div>
<p>{{ text }}</p>
<button v-on:click="$emit('accepted')">OK</button>
</div>
</template>
<script>
export default {
props: ['text'],
emits: ['accepted']
}
</script>
注意:强烈建议使用 emits 记录每个组件所触发的所有事件。这尤为重要,因为我们移除了 .native 修饰符。任何未在 emits 中声明的事件监听器都会被算入组件的 $attrs,并将默认绑定到组件的根节点上。
ref和$parent
父组件访问子组件实例
尽管存在 prop 和事件,但有时你可能仍然需要在 JavaScript 中直接访问子组件。为此,可以使用 ref attribute 为子组件或 HTML 元素指定引用 ID。
在 composition API 中,使用 template refs,在 setup 中创建 ref 对象返回,在 template 中添加同名的 ref 属性。
<template>
<div ref="root">This is a root element</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
const root = ref(null)
onMounted(() => {
// DOM 元素将在初始渲染后分配给 ref
console.log(root.value) // <div>This is a root element</div>
})
return {
root
}
}
}
</script>
Provide/Inject
Provide
在 setup() 中使用 provide 时,我们首先从 vue 显式导入 provide 方法。这使我们能够调用 provide 来定义每个 property。provide 函数允许你通过两个参数定义 property:name和value。
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import { provide } from 'vue'
import MyMarker from './MyMarker.vue'
export default {
components: {
MyMarker
},
setup() {
provide('location', 'North Pole')
provide('geolocation', {
longitude: 90,
latitude: 135
})
}
}
</script>
inject
在 setup() 中使用 inject 时,也需要从 vue 显式导入。导入以后,我们就可以调用它来定义暴露给我们的组件方式。
<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'
export default {
setup() {
const userLocation = inject('location', 'The Universe')
const userGeolocation = inject('geolocation')
return {
userLocation,
userGeolocation
}
}
}
</script>
响应性
为了增加 provide 值和 inject 值之间的响应性,我们可以在 provide 值时使用ref或reactive]。使用 MyMap 组件,我们的代码可以更新如下:
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
provide('location', location)
provide('geolocation', geolocation)
}
现在,如果这两个 property 中有任何更改,MyMarker 组件也将自动更新!
修改响应式 property
当使用响应式 provide / inject 值时,建议尽可能将对响应式 property 的所有修改限制在定义 provide 的组件内部。
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
provide('location', location)
provide('geolocation', geolocation)
return {
location
}
},
methods: {
updateLocation() {
this.location = 'South Pole'
}
}
然而,有时我们需要在注入数据的组件内部更新 inject 的数据。在这种情况下,我们建议 provide 一个方法来负责改变响应式 property。
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
const updateLocation = () => {
location.value = 'South Pole'
}
provide('location', location)
provide('geolocation', geolocation)
provide('updateLocation', updateLocation)
最后,如果要确保通过 provide 传递的数据不会被 inject 的组件更改,我们建议对提供者的 property 使用 readonly。
provide('location', readonly(location))
provide('geolocation', readonly(geolocation))
事件总线
有一些特殊的情形,我们需要使用事件监听器完成父子通讯。比如:父组件中有 slot,子组件是以 slot 形式存在的,没法添加 ref。
在 2.x 中,Vue 实例可用于触发由事件触发器 API 通过指令式方式添加的处理函数 ($on,$off 和 $once)。这可以用于创建一个事件总线,以创建在整个应用中可用的全局事件监听器:
// eventBus.js
const eventBus = new Vue()
export default eventBus
// ChildComponent.vue
import eventBus from './eventBus'
export default {
mounted() {
// 添加 eventBus 监听器
eventBus.$on('custom-event', () => {
console.log('Custom event triggered!')
})
},
beforeDestroy() {
// 移除 eventBus 监听器
eventBus.$off('custom-event')
}
}
// ParentComponent.vue
import eventBus from './eventBus'
export default {
methods: {
callGlobalCustomEvent() {
eventBus.$emit('custom-event') // 当 ChildComponent 已被挂载时,控制台中将显示一条消息
}
}
}
在vue3中,我们从实例中完全移除了 $on、$off 和 $once 方法。事件总线模式可以被替换为使用外部的、实现了事件触发器接口的库,例如 mitt 或 tiny-emitter。
// eventBus.js
import emitter from 'tiny-emitter/instance'
export default {
$on: (...args) => emitter.on(...args),
$once: (...args) => emitter.once(...args),
$off: (...args) => emitter.off(...args),
$emit: (...args) => emitter.emit(...args),
}
它提供了与 Vue 2 相同的事件触发器 API。在绝大多数情况下,不鼓励使用全局的事件总线在组件之间进行通信。虽然在短期内往往是最简单的解决方案,但从长期来看,它维护起来总是令人头疼。
$attrs
官方描述:包含了父作用域中不作为组件props或自定义事件的 attribute 绑定和事件。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定,并且可以通过 v-bind="$attrs" 传入内部组件——这在创建高阶的组件时会非常有用。
在vue2中有listeners,但是在vue3中只有attrs包含了vue2中的listeners。
$attrs的作用可以类似于props和emits的候补。
比如A组件向B组件传递了很多props和方法,但是B组件中的props和emits只写了其中的一个,那么其他的属性和方法就都放在attrs向下传递,那么只需要这么写:v-bind="$attrs"。
注意:多余的属性可能会放在组件的根标签作为标签的属性存在,如果不想这样可是设置inheritAttrs: false。