组件通信
在 Vue.js
基础内容的讲解中,我们知道了什么是 Vue
组件,了解了它的功能,那么在实际的项目开发中,实现的页面是如何和组件对应起来的呢?下面举一个例子来说明这个问题。
以常见的登录页面为例,一般由用户名输入框、密码输入框以及登录按钮所组成。在用户输入完信息后,可以单击 “登录” 按钮进行登录,注意 “登录” 按钮一开始是不可单击的。
我们可以把输入框抽离成一个组件,“登录” 按钮也抽离成一个组件,它们共同存在于登录页面这个父组件中。这样,这些组件就有了关联,当我们在输入框中输入登录信息之后,就可以告诉 “登录” 按钮组件去更新自己的不可单击状态,当我们单击 “登录” 按钮时,就需要拿到输入框的登录信息来进行登录。所以,我们会发现,组件之间并不是孤立的,它们之间是需要通信的,正是这种组件间的相互通信才构成了页面上用户行为交互的过程。
组件通信概述
我们可以把所有页面都抽象成若干个组件,它们之间有父子关系的组件、兄弟关系的组件,可以使用图 3-9 来表示组件之间的关系。

所有 Vue
组件的关系:
-
A
组件和B
组件、B
组件和C
组件、B
组件和D
组件形成了父子关系。 -
C
组件和D
组件形成了兄弟关系。 -
A
组件和C
组件、A
组件和D
组件形成了隔代关系(其中的层级可能是多级,即隔了多代)。
在明确了它们之间的关系之后,就需要理解它们之间如何通信,或者叫作如何传值。下面就来逐一讲解。
先使用代码来实现上面的 A
、B
、C
、D
四组组件的关系,如示例代码3-2-1所示。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<title>组件通信</title>
<script src="https://unpkg.com/vue@3.2.28/dist/vue.global.js"></script>
<style type="text/css">
#app {
text-align: center;
line-height: 2;
}
</style>
</head>
<body>
<!--根实例挂载的 DOM 对象 app-->
<div id="app">
{{str}}
<component-b />
</div>
<script type="text/javascript">
// 定义一个名为 componentC 的局部子组件
const componentC = {
data() {
return {
str: 'I am C'
}
},
template: '<span>{{str}}</span>'
}
// 定义一个名为 componentD 的局部子组件
const componentD = {
data() {
return {
str: 'I am D'
}
},
template: '<span>{{str}}</span>'
}
// 定义一个名为 componentB 的父组件,也是一个局部组件
const componentB = {
data() {
return {
str: 'I am B'
}
},
template: '<div>'+
'{{str}}'+
'<div>'+
'<component-c />,'+
'<component-d />'+
'</div>'+
'</div>',
// 利用 components 可以将之前定义的C、D组件挂载到B组件上,component-c 和 component-d 是C/D组件的名称,
// 对应 template 中的 <component-c/> 和 <component-d />
components: {
'component-c': componentC,
'component-d': componentD
}
}
// 定义一个根实例 vmA
const vmA = Vue.createApp({
data() {
return {str: "I am A"}
},
// 将父组件B挂载到实例A中component-b对应的#app内的<component-b/>
components: {
'component-b': componentB
}
}).mount("#app")
</script>
</body>
</html>
为了便于理解,上面的代码为完整的 index.html
代码,读者可以直接在浏览器中运行这段代码,运行结果如图3-10所示。

父组件向子组件通信
父组件向子组件通信可以理解成:
-
父组件向子组件传值。
-
父组件调用子组件的方法。
props
利用 props
属性可以实现父组件向子组件传值,如示例代码3-2-2所示。
// 定义一个名为 componentC 的局部子组件
const componentC = {
props: ['info'], // 在子组件中使用props接收
data() {
return {
str: 'I am C'
}
},
template: '<span>{{str}} : {{info}}</span>'
}
...
// 定义一个名为 componentB 的父组件,也是一个局部组件
const componentB = {
data() {
return {
str: 'I am B',
// 传给子组件的值
info: 'data from B'
}
},
template: '<div>'+
'{{str}}'+
'<div>'+
'<component-c :info=\'info\'/>,'+
'<component-d />'+
'</div>'+
'</div>',
// 利用 components 可以将之前定义的C、D组件挂载到B组件上,component-c 和 component-d 是C/D组件的名称,
// 对应 template 中的 <component-c/> 和 <component-d />
components: {
'component-c': componentC,
'component-d': componentD
}
}
在上面的代码中,B
组件内的 data
中定义了 info
属性,准备好数据,在 template
中使用 C
组件时,通过 <component-c :info=\'info\'/>
将值传给 C
组件。这里采用的是 v-bind
的简写指令,等号后面的 info
就是在 data
中定义的 info
,它被称为动态值,它是响应式的。当然,props
也可以直接接收一个静态值,代码如下:
<component-c info='data from B' />
需要注意的是,如果静态值是一个字符串,则可以省去 v-bind
(即可以不用冒号),但是如果静态值是非字符串类型的值,则必须采用 v-bind
来绑定传入。
然后,在 C
组件中使用 props
接收 info
,props:['info']
是一个由字符串组成的数组,表示可以接收多个 props
,数组中的每个值和传入时的值要对应。然后,在子组件中使用 this.info
得到这个值,无须在 data
中定义,最后就可以使用插值表达式 {{info}}
来显示。
props
不仅可以传字符串类型的值,像数组、对象、布尔值都可以传递,在传递时 props
也可以定义成数据校验的形式,以此来限制接收的数据类型,提升规范性,避免得到意想之外的值,代码如下:
props: {
info: {
type: String, // 限制为字符串类型
default: '' // 默认值
}
}
当 props
验证失败时,控制台将会产生一个警告。所以,要么就是用 props:['info']
来接收,要么添加数据格式校验,以严格的数据格式来传值。
type
可以是下列原生构造函数中的一个:
-
String
-
Number
-
Boolean
-
Array
-
Object
-
Date
-
Function
-
Symbol
另外,type
还可以是一个自定义的构造函数,并且通过 instanceof
来进行检查确认。例如,给定下列现成的构造函数,如示例代码3-2-3所示。
class Person (firstName, lastName) {
constructor {
this.firstName = firstName
this.lastName = lastName
}
}
// 在props接收时,这样设置
props: {
author: Person
}
另外,需要说明的是,如果 props
传递的是一个动态值,每次父组件的 info
发生更新时,子组件中接收的 props
都将会刷新为最新的值。这意味着,我们不应该在一个子组件内部改变 props
,如果这样做了,Vue
会在浏览器的控制台中发出警告。例如,在子组件的 mounted
方法中调用:
this.info= 'abc'
可以看到控制台上的警告,如图3-11所示。

Vue
中父传子的方式形成了一个单向下行绑定,叫作 单向数据流。父级 props
的更新会向下流动到接收这个 props
的子组件中,触发对应的更新,但是反过来则不行。这样可以防止有多个子组件的父组件内的值被修改时,无法查找到哪个子组件修改的场景,从而导致应用中的数据流向无法清晰地追溯。
如果需要在子组件中监听 props
的变化,可以直接在子组件中使用监听器 watch
,代码如下:
props: ['info'],
watch: {
info(v) {
console.log(v)
}
}
如果遇到确实需要改变 props
值的应用场合,则可以采用下面的解决办法:
-
使用
props
来传递一个初始值,该子组件接下来希望将其作为一个本地的props
数据来使用,在这种情况下,最好定义一个本地的data
属性,并将这个props
用作其初始值,代码如下:props: ['info'], data() { return { myInfo: this.info } }
-
使用
props
时,把它当作初始值,使用的时候需要进行一下转换。在这种情况下,最好使用这个props
的值来定义一个计算属性:props: ['info'], computed: { myInfo() { return this.info.trim().toLowerCase() } }
props
机制是在 Vue
中非常常用的传值方法,所以掌握好是非常重要的,那么如何实现父组件调用子组件的方法呢?
$refs
利用 Vue
实例的 $refs
属性可以实现父组件调用子组件的方法,在之前的完整代码中,我们只修改部分代码,如示例代码3-2-4所示。
// 定义一个名为 componentD 的局部子组件
const componentD = {
data() {
return {
str: 'I am D'
}
},
template: '<span>{{str}}</span>',
methods: {
dFunc() {
console.log('D的方法')
}
}
}
// 定义一个名为 componentB 的父组件,也是一个局部组件
const componentB = {
data() {
return {
str: 'I am B',
// 传给子组件的值
info: 'data from B'
}
},
template: '<div>'+
'{{str}}'+
'<div>'+
'<component-c :info=\'info\'/>,'+
'<component-d ref=\'componentD\'/>'+
'</div>'+
'</div>',
// 利用 components 可以将之前定义的C、D组件挂载到B组件上,component-c 和 component-d 是C/D组件的名称,
// 对应 template 中的 <component-c/> 和 <component-d />
components: {
'component-c': componentC,
'component-d': componentD
},
mounted() {
this.$refs.componentD.dFunc()
}
}
当运行这段代码时,若在控制台上看到 console.log('D的方法')
,就说明运行正常。当父组件想要调用子组件的方法时,首先需要给子组件绑定一个 ref
值(即 componentD
),然后就可以在父组件当前的实例中通过 this.$refs.componentD
得到子组件的实例,拿到子组件的实例之后,就可以调用子组件定义在 methods
中的方法了。
需要说明的是,在 Vue
中,也可以给原生的 DOM
元素绑定 ref
值,这样通过 this.$refs
拿到的就是原生的 DOM
对象。代码如下:
<button ref="btn"></button>
使用选项式 API,引用将被注册在组件的
使用组合式 API,引用将存储在与名字匹配的
注意:变量名称要一致。 |
父组件可以访问子组件的 所有公开的 实例属性 和 方法,其中包括子组件的 |
子组件向父组件通信
在前面的代码中,我们曾经尝试了直接修改父组件的 props
,但是会报错,所以需要有一个新的机制来实现子组件向父组件通信,可以理解为下面两点:
-
子组件向父组件传值。
-
子组件调用父组件的方法。
与父组件向子组件通信不同的是,子组件调用父组件方法的同时就可以向父组件传值,使用 $emit
方法和自定义事件。
$emit
$emit
方法的主要作用是触发当前组件实例上的事件,所以子组件调用父组件方法就可以理解成子组件触发了绑定在父组件上的自定义事件。在之前的完整代码中,我们只修改部分代码,如示例代码3-2-5所示。
// 定义一个名为 componentC 的局部子组件
const componentC = {
data() {
return {
str: 'I am C'
}
},
template: '<span>{{str}}</span>',
// 在子组件的 mounted 方法中调用 this.$emit 来触发自定义事件
mounted() {
this.$emit('myFunction', 'hi')
}
}
// 定义一个名为 componentB 的父组件,也是一个局部组件
const componentB = {
template: '<div>'+
'<div>'+
// 将 myFunction 方法通过 v-on 传入子组件
'<component-c @myFunction=\'myFunction\' />,'+
'</div>'+
'</div>',
components: {
'component-c': componentC
},
methods: {
// 定义父组件需要被子组件调用的方法
myFunction(data) {
console.log('来自子组件的调用', data)
}
}
}
首先需要在父组件的 methods
中定义 myFunction
方法,然后在 template
中使用 <component-c/>
组件时,将 myFunction
传入子组件,这里采用的是 v-on
指令(即简写 @myFunction
)。在前面的章节中,我们使用 v-on
来监听原生 DOM
绑定的事件,例如 @click
,这里的 @myFunction
实际上就是一个自定义事件。
然后,在子组件 C
中,通过 this.$emit('myFunction','hi')
就可以通知父组件对应的 myFunction
方法,第一个参数就是父组件中 v-on
指令的参数值(即 @myFunction
),第二个参数是需要传给父组件的数据。如果在控制台中看到有 console.log('来自子组件的调用','hi')
,就说明调用成功了。
这样,在完成子组件调用父组件方法的同时,也向父组件传递了数据,这里是使用 $emit
方法来实现的。子组件调用父组件还可以用其他方式来实现,接下来继续介绍。
$parent
这种方法比较直观,可以直接操作父子组件的实例,在子组件中直接通过 this.$parent
获取父组件的实例,从而调用父组件中定义的方法,类似于前文介绍的通过 $refs
获取子组件的实例。在之前的完整代码中,我们只修改部分代码,如示例代码3-2-6所示。
// 定义一个名为 componentC 的局部子组件
const componentC = {
data() {
return {
str: 'I am C'
}
},
template: '<span>{{str}}</span>',
mounted() {
// 直接采用 $parent 方法进行调用
this.$parent.myFunction('$parent 方法调用')
}
}
// 定义一个名为 componentB 的父组件,也是一个局部组件
const componentB = {
template: '<div>'+
'<div>'+
'<component-c />,'+
'</div>'+
'</div>',
components: {
'component-c': componentC
},
methods: {
// 定义父组件需要被子组件调用的方法
myFunction(data) {
console.log('来自子组件的调用', data)
}
}
}
需要注意的是,采用 $parent
方法调用时,在父组件的 template
中使用 <component-c />
时,无须采用 v-on
方法传入 myFunction
,因为 this.$parent
可以获取父组件的实例,所以其内定义的方法都可以调用。
但是,Vue
并不推荐以这种方法来实现子组件调用父组件,由于一个父组件可能会有多个子组件,因此这种方法对父组件中的状态维护是非常不利的,当父组件的某个属性被改变时,无法以循规溯源的方式去查找到底是哪个子组件改变了这个属性。因此,请有节制地使用 $parent
方法,它的主要目的是作为访问组件的应急方法。推荐使用 $emit
方法实现子组件向父组件的通信。
下面使用一张图来大致总结一下父子组件通信的方式,如图3-12所示。

父子组件的双向数据绑定与自定义 v-model
在前面的章节中我们曾经讲过,父组件可以使用 props
给子组件传值,当父组件 props
更新时也会同步给子组件,但是子组件无法直接修改父组件的 props
,这其实是一个单向的过程。但是在一些情况下,我们可能会需要对一个 props
进行 “双向绑定”,即子组件的值更改后,父组件也同步进行更改。
在之前的 2.2 节中,我么了解到 v-model
指令主要是结合一些原生的表单元素 <input>
等使用,对于我们 自定义的组件,也可以用 v-model
来实现组件通信,在之前的完整代码中,我们只修改部分代码,如示例代码3-2-7所示。
// 子组件
const componentD = {
props: ['info'],
template: '<span>'+
'子组件的 info:{{info}}'+
'<button @click="clickCallback">点我换Tom</button>'+
'</span>',
methods: {
clickCallback() {
this.$emit('update:info', 'Tom')
}
}
}
// 父组件
const componentB = {
data() {
return {
info: 'Jack'
}
},
template: '<div>'+
'父组件的info:{{info}}'+
'<div>'+
'<component-d v-model:info="info"/>'+
'</div>'+
'</div>',
components: {
'component-d': componentD
}
}
在父组件的 data
中定义了 info
属性,并且通过 v-model
的方式传给了子组件,代码如下:
<component-d v-model:info="info" />
这里使用了 $emit
方法,在子组件中,给按钮 button
绑定了一个单击事件,在事件回调函数中,采用如下代码:
this.$emit('update:info','Tom')
这样更新就会同步到父组件的 props
中,调用 $emit
方法实际上就是触发一个父组件的方法,这里的 update
是固定写法,代表更新,而 :info
表示更新 info
这个 prop
,第二个参数 Tom
表示更新的值。其中,v-model
的配置含义如图 3-13 所示。

在单击按钮之后,可以看到父组件中的 info
被更新成了 Tom
,子组件的 info
也更新成了 Tom
,这就完成了父子组件的 “双向绑定”。
非父子关系组件的通信
对于父子组件之间的通信,前面介绍的两种方式是完全可以实现的,但是对于不是父子关系的两个组件,那么又该如何实现通信呢?非父子关系组件的通信分为两种方式:
-
拥有同一父组件的两个兄弟组件的通信。
-
没有任何关系的两个独立组件的通信。
兄弟组件的通信
对于具有同一个父组件 B
的兄弟组件 C
和 D
而言,可以借助父组件 B
这个桥梁,实现兄弟组件的通信。在之前的完整代码中,我们只修改部分代码,如示例代码 3-2-8 所示。
// 定义一个名为 componentC 的局部子组件
const componentC = {
props: ['infoFromD'],
template: '<span>收到来自D的消息:{{infoFromD}}</span>'
}
// 定义一个名为 componentD 的局部子组件
const componentD = {
template: '<span><button @click="clickCallback">点我换通知C</button></span>',
methods: {
clickCallback() {
// 先通知父元素
this.$emit('saidToC', 'I am D')
}
}
}
// 定义一个名为 componentB 的父组件,也是一个局部组件
const componentB = {
data() {
return {
infoFromD: ''
}
},
template: '<div>'+
'<div>'+
'<component-c :infoFromD="infoFromD" />,'+
'<component-d @saidToC="saidToC" />'+
'</div>'+
'</div>',
components: {
'component-c': componentC,
'component-d': componentD
},
methods: {
saidToC(data) {
console.log('来自D组件的调用', data)
// 在父元素中通过 props 的更新来更新 C 组件的数据
this.infoFromD = data;
}
}
}
在 D
组件中通过 $emit
调用父组件的方法,同时在父组件中修改 data
中的 infoFromD
,同时也影响到了作为 props
传递给 C
组件的 infoFromD
,这就实现了兄弟组件的通信。
但是,这种方法总让人觉得比较绕,假如两个组件没有兄弟关系,那么又该采用什么方法来通信呢?
事件总线 EventBus 和 mitt
在 Vue 2
中,可以采用 EventBus
这种方法,实际上就是将沟通的桥梁换成自己,同样需要有桥梁作为通信中继。就像是所有组件共用相同的事件中心,可以向该中心发送事件或接收事件,所有组件都可以上下平行地通知其他组件。在之前的完整代码中,我们只修改部分代码,并用 Vue 2
的语法来写,如示例代码 3-2-9 所示。
// 定义一个名为 componentC 的局部子组件
const componentC = {
data() {
return {
infoFromD: ''
}
},
template: '<span>收到来自D的消息:{{infoFromD}}</span>',
mounted: function () {
this.$EventBus.$on('eventBusEvent', function (data) {
this.infoFromD = data;
}).bind(this)
}
}
// 定义一个名为 componentD 的局部子组件
const componentD = {
template: '<span><button @click="clickCallback">点我换通知C</button></span>',
methods: {
clickCallback() {
// 先通知父元素
this.$EventBus.$emit('eventBusEvent', 'I am D')
}
}
}
// 定义一个名为 componentB 的父组件,也是一个局部组件
const componentB = {
data() {
return {
str: 'I am B'
}
},
template: '<div>'+
'<component-c />,'+
'<component-d />'+
'</div>',
components: {
'component-c': componentC,
'component-d': componentD
}
}
// 定义中央事件总线
var EventBus = new Vue(); // vue2语法
// 将中央事件总线赋值给 Vue.prototype,这样所有组件都能访问到了
Vue.prototype.$EventBus = EventBus;
// 定义一个根实例 vmA
var vmA = new Vue({ // vue2语法
el: '#app',
components: {
'component-b': componentB
}
})
在上面的代码中用到的 C
、D
组件,它们之间没有任何关系,在 C
组件的 mounted
方法中通过 this.$EventBus.$on('eventBusEvent',function(){…})
实现了事件的监听,然后在 D
组件的单击回调事件中通过 this.$EventBus.$emit('eventBusEvent')
实现了事件的触发,eventBusEvent
是一个全局的事件名。
接着,通过 new Vue()
实例化了一个 Vue
的实例,这个实例是一个没有任何方法和属性的空实例,称其为:中央事件总线(EventBus
),然后将其赋值给 Vue.prototype.$EventBus
,使得所有的组件都能够访问到。
$on
方法和 $emit
方法其实都是 Vue
实例提供的方法,这里的关键点就是利用一个空的 Vue
实例来作为桥梁,实现事件分发,它的工作原理是发布/订阅方法,通常称为 Pub/Sub
,也就是发布和订阅的模式。
在 Vue 3
中,由于取消了 Vue
中全局变量 Vue.prototype.$EventBus
这种写法,所以采用 EventBus
这种事件总线来进行通信已经无法使用,取而代之,可以采用第三方事件总线库 mitt
。
在页面中引入 mitt
的 JavaScript 文件或者在项目中采用 import
方式引入 mitt
,代码如下:
<script src="https://unpkg.com/mitt/dist/mitt.umd.js"></script>
或
import mitt from 'mitt'
mitt
的使用方法和 EventBus
非常类似,同样是基于 Pub/Sub
模式,并且更加简单,可以在需要进行通信的地方直接使用,如示例代码3-2-10所示。
const emitter = mitt()
// 子组件
const componentC = {
data () {
return {
infoFromD: ''
}
},
template: '<span>收到来自D的消息:{{infoFromD}}</span>',
mounted() {
emitter.on('eventBusEvent', (data)=> {
this.infoFromD = data;
})
}
}
// 子组件
const componentD = {
template: '<span><button @click="clickCallback">点我换通知C</button></span>',
methods: {
clickCallback() {
emitter.emit('eventBusEvent', 'I am D')
}
}
}
从上面的代码可以看到,与 EventBus
相比,mitt
的方式无须创建全局变量,使用起来更加简单。
事件总线的方式进行通信使用起来非常简单,可以实现任意组件之间的通信,其中没有多余的业务逻辑,只需要在状态变化组件触发一个事件,随后在处理逻辑组件监听该事件即可。这种方法非常适合小型的项目,但是对于一些大型的项目,要实现复杂的组件通信和状态管理,就需要使用 Vuex
了。
在 Vue 3
中,$on
方法被移除,因此如果想在 Vue 3
中实现类似于 Vue 2
中的 EventBus
(事件总线)功能,可以通过使用 mitt
库实现 EventBus
方式来实现跨组件之间的事件通信:
mitt
是一个轻量级的事件总线库,它非常适合在 Vue 3
中实现事件通信。可以通过它来实现跨组件的事件监听和触发。
-
安装 mitt
npm install mitt
-
创建 EventBus
你可以创建一个单独的
EventBus
文件,并在Vue 3
应用中使用它来触发和监听事件。eventBus.jsimport mitt from 'mitt'; const eventBus = mitt(); export default eventBus;
-
在组件中使用 EventBus
<template>
<button @click="sendMessage">Send Message</button>
</template>
<script>
import eventBus from './eventBus';
export default {
methods: {
sendMessage() {
eventBus.emit('message', 'Hello from Component A');
}
}
}
</script>
<template>
<p>{{ message }}</p>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue';
import eventBus from './eventBus';
export default {
setup() {
const message = ref('');
// 监听事件
const handleMessage = (msg) => {
message.value = msg;
};
onMounted(() => {
eventBus.on('message', handleMessage);
});
onUnmounted(() => {
eventBus.off('message', handleMessage);
});
return { message };
}
}
</script>
在这个示例中:
-
组件
A
通过eventBus.emit
触发了一个事件。 -
组件
B
通过eventBus.on
监听事件,当事件发生时更新message
。
这种方式和 Vue 2
中的事件总线模式非常相似,但更加简洁和轻量。
provide/inject
通常,当需要从父组件向子组件传递数据时,我们使用 props
。想象一下这样的结构:有一些深度嵌套的组件,深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 props
沿着组件链逐级传递下去,可能会很麻烦。
对于这种情况,我们可以使用一对 provide
(提供)和 inject
(注入)。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide
选项来提供数据,子组件有一个 inject
选项来开始使用这些数据,如图 3-14 所示。

我们有这样的层次结构:
Root
└─ TodoList
├─ TodoItem
└─ TodoListFooter
├─ ClearTodosButton
└─ TodoListStatistics
如果要将 Todo-iTem
的长度直接传递给 TodoListStatistics
,则要将 prop
逐级传递下去:TodoList→TodoListFooter→TodoListStatistics
。通过 provide/inject
方法,我们可以直接执行以下操作,如示例代码 3-2-11 所示。
const app = Vue.createApp({})
app.component('todo-list', {
data() {
return {
todos: ['Feed a cat', 'Buy tickets']
}
},
provide: {
user: 'John Doe'
},
template: `
<div>
{{ todos.length }}
<!-- 模板的其余部分 -->
</div>
`
})
app.component('todo-list-statistics', {
inject: ['user'],
created() {
console.log(this.user) // 注入 property: John Doe
}
})
但是,如果我们尝试在此处提供一些组件的实例 data
属性,将是不起作用的,代码如下:
app.component('todo-list', {
data() {
return {
todos: ['Feed a cat', 'Buy tickets']
}
},
provide: {
todoLength: this.todos.length
// 将会导致错误 `Cannot read property 'length' of undefined`
},
...
})
要访问组件实例 data
中的属性,我们需要将 provide
转换为返回对象的函数,代码如下:
app.component('todo-list', {
data() {
return {
todos: ['Feed a cat', 'Buy tickets']
}
},
provide() {
return {
todoLength: this.todos.length
}
},
...
})
这使我们能够更安全地继续开发该组件,而不必担心可能会更改/删除子组件所依赖的某些内容。这些组件之间的接口仍然是明确定义的,就像 props
一样。
实际上,可以将 inject
注入看作是 long range(跨组件)props
,除了:
-
父组件不需要知道哪些子组件使用它提供的
data
属性。 -
子组件不需要知道注入的
data
属性来自哪里。
在上面的例子中,如果我们更改了 todos
的列表,这个变化并不会反映在注入 todoLength
属性中。这是因为默认情况下,provide/inject
绑定并不是响应式的。我们可以通过传递一个 ref
属性或 reactive
对象给 provide
来改变这种行为。在我们的例子中,如果想对祖先组件中的更改做出响应,则需要为提供的 todoLength
分配一个组合式 API computed
属性,如示例代码 3-2-12 所示。
app.component('todo-list', {
...
provide() {
return {
todoLength: Vue.computed(() => this.todos.length)
}
}
})
app.component('todo-list-statistics', {
inject: ['todoLength'],
created() {
console.log(this.todoLength.value) // Injected property: 5
}
})
在这种情况下,任何对 todos.length
的改变都会被正确地反映在注入 todoLength
的组件中,我们会在后面的章节详细介绍组合式 API 的使用。