ref和通信

ref

在 Vue 组件中,ref 是一个用于在模板或组件内部获取对 DOM 元素或子组件实例的引用的特殊属性。ref 的使用场景和功能包括:

  1. 访问和操作 DOM 元素:通过给元素添加 ref 属性,可以在组件中直接访问和操作该 DOM 元素的属性、样式、以及调用其方法。
  2. 与子组件进行通信:通过给子组件添加 ref 属性,可以在父组件中获取子组件的实例,并通过实例调用子组件的方法、访问其属性。

下面是一个完整的示例,展示了 ref 的使用场景和示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div>
<h1 ref="title">方式1:DOM元素</h1>
<button @click="changeTitle">更改title</button>
<child-component ref="child1">方式2: 引用子组件实例</child-component>
<child-component ref="child2"></child-component>
</div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
components: {
ChildComponent,
},
methods: {
changeTitle() {
// 访问和修改 DOM 元素
this.$refs.title.innerText = 'New Title';

// 通过子组件的实例进行通信
this.$refs.child1.methodInChild();
this.$refs.child2.methodInChild();
},
},
};
</script>

在上面的示例中,我们首先通过给 <h1> 元素添加 ref="title",在组件中创建了一个对该元素的引用。然后,我们通过点击按钮触发 changeTitle 方法,在方法中使用 this.$refs.title 访问并修改了该元素的内容。

另外,我们在模板中使用了 <child-component> 组件,并给它添加了 ref="child1"。在 changeTitle 方法中,我们通过 this.$refs.child1 获取到了子组件的实例,并调用了子组件的 methodInChild 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
<h2>Child Component</h2>
<button @click="methodInChild">Child Method</button>
</div>
</template>

<script>
export default {
methods: {
methodInChild() {
console.log('Child method called');
},
},
};
</script>

需要注意的是,ref 的作用域限定在当前组件中,无法在子组件中通过父组件的 $refs 访问其他子组件。此外,使用 ref 的组件或元素必须在渲染完成后才能被访问,所以通常在 mounted 生命周期钩子函数或之后的钩子函数中使用 $refs 来保证访问的时机。

父向子

  1. props:通过使用 props 将数据从父组件传递给子组件。子组件通过 props 接收数据并使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!-- 父组件 -->
<template>
<div>
<ChildComponent :message="message" name="bifeng" />
</div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
components: {
ChildComponent,
},
data() {
return {
message: 'Hello from parent!',
};
},
};
</script>

<!-- 子组件 -->
<template>
<div>
<p>{{ message }}</p>
<p>My name is {{name}}</p>
</div>
</template>

<script>
export default {
props: ['message', 'name'],
};
</script>

注意, 组件中属性的定义包含两种方式: 直接定义, 通过v-bind定义, 实际上v-bind最终的效果也是定义一个属性, 只不过其值变为一个表达式.

  1. 使用 $refs:通过给组件添加 ref 属性,可以直接访问子组件的实例, 调用子组件的方法并传递参数实现双向通信, 这是利用UML中的关联关系
  2. 使用$children$parent: 父组件通过使用 $children 访问子组件数组,并可以进一步访问子组件的属性或方法。在父组件的 logChildren 方法中,我们通过 this.$children 输出子组件数组,并通过 this.$children[0].message 访问子组件的 message 属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<h2>Parent Component</h2>
<button @click="logChildren">Log Children</button>
<ChildComponent></ChildComponent>
</div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
components: {
ChildComponent,
},
methods: {
logChildren() {
console.log(this.$children); // 访问子组件数组
console.log(this.$children[0].message); // 访问子组件的属性或方法
},
},
};
</script>

子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
<h3>Child Component</h3>
<p>{{ message }}</p>
<button @click="changeMessage">Change Message</button>
</div>
</template>

<script>
export default {
data() {
return {
message: 'Hello from child component',
};
},
methods: {
changeMessage() {
this.message = 'Updated message from child component';
},
},
};
</script>

需要注意的是,使用 $children 访问子组件是通过索引进行的,并且它只能访问直接子组件,而无法访问嵌套的子组件。此外,$children 是一个响应式属性,当子组件的数量发生变化时,它也会相应地更新

  1. 使用 $attrs$listeners:通过属性继承和事件绑定,实现父组件向子组件传递属性和监听事件

子向父

  1. emit:通过使用自定义事件和 $emit 方法,在子组件中触发事件,并将数据传递给父组件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!-- 子组件 -->
<template>
<div>
<button @click="sendMessage">Send Message to Parent</button>
</div>
</template>

<script>
export default {
methods: {
sendMessage() {
this.$emit('message', 'Hello from child!');
},
},
};
</script>

<!-- 父组件, 其中v-bind:message为自定义绑定的事件 -->
<template>
<div>
<ChildComponent @message="handleMessage" />
<p>{{ receivedMessage }}</p>
</div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
components: {
ChildComponent,
},
data() {
return {
receivedMessage: '',
};
},
methods: {
handleMessage(message) {
this.receivedMessage = message;
},
},
};
</script>
  1. slot: 通过在父组件中定义插槽,子组件可以将内容插入到指定位置,实现通信和组合。

兄弟

通过使用一个共享的父组件,将数据传递给父组件,再由父组件将数据传递给其他子组件, 实际上其是复用了emit和props实现的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!-- 共享的父组件 -->
<template>
<div>
<ChildComponentA :message="message" />
<ChildComponentB :message="message" />
</div>
</template>

<script>
import ChildComponentA from './ChildComponentA.vue';
import ChildComponentB from './ChildComponentB.vue';

export default {
components: {
ChildComponentA,
ChildComponentB,
},
data() {
return {
message: 'Hello from parent!',
};
},
};
</script>

<!-- 子组件 A -->
<template>
<div>
<p>{{ message }}</p>
</div>
</template>

<script>
export default {
props: ['message'],
};
</script>

<!-- 子组件 B -->
<template>
<div>
<p>{{ message }}</p>
</div>
</template>

<script>
export default {
props: ['message'],
};
</script>

全局通信

  1. 使用事件总线(Event Bus):创建一个空的 Vue 实例作为事件中心,其他组件通过该实例来触发和监听事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// EventBus.js
import Vue from 'vue';
export default new Vue();

// 组件 A
import EventBus from './EventBus.js';

export default {
methods: {
sendMessage() {
EventBus.$emit('message', 'Hello from component A!');
},
},
};

// 组件 B
import EventBus from './EventBus.js';

export default {
data() {
return {
receivedMessage: '',
};
},
mounted() {
EventBus.$on('message', (message) => {
this.receivedMessage = message;
});
},
};

另外一个定义全局事件总线的方式, 通过原型链来实现:

1
2
3
4
5
6
7
8
9
10
11
// main.js

import Vue from 'vue';

// 创建全局事件总线
Vue.prototype.$bus = new Vue();

new Vue({
// ...
}).$mount('#app');

  1. 使用 Vuex(Vue 的状态管理库):在全局状态中存储和管理数据,不同组件可以通过访问共享的状态来实现通信。

订阅和发布

时间总线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- ComponentA.vue -->

<template>
<div>
<h2>Component A</h2>
<button @click="emitEvent">Emit Event</button>
</div>
</template>

<script>
export default {
methods: {
emitEvent() {
// 触发自定义事件
this.$bus.$emit('custom-event', 'Custom event emitted from Component A');
},
},
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- ComponentB.vue -->

<template>
<div>
<h2>Component B</h2>
<p>{{ message }}</p>
</div>
</template>

<script>
export default {
data() {
return {
message: '',
};
},
created() {
// 订阅自定义事件
this.$bus.$on('custom-event', (data) => {
this.message = data;
});
},
};
</script>

ComponentA.vue 中,我们使用 $bus.$emit 触发了一个自定义事件,并传递了相关的数据。在 ComponentB.vue 中,我们使用 $bus.$on 订阅了这个自定义事件,并在回调函数中更新了组件的 message 数据。

通过定义全局事件总线,我们可以在任何组件中进行事件的发布和订阅,实现组件间的通信。需要注意的是,全局事件总线的缺点是它将所有的事件都集中在一个地方,可能导致事件的管理变得复杂。因此,在大型应用中,建议使用更灵活的状态管理方案,如 Vuex。

pubsub

PubSubJS 是一个简单的 JavaScript 消息发布-订阅库,用于实现组件间的解耦和消息传递。它提供了一个中央的消息中心,允许订阅者(subscribers)订阅特定的主题(topics),并在消息发布者(publishers)发布消息时接收通知。

下面是一个基于 PubSubJS 的消息订阅和通知示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 安装 PubSubJS(使用 npm 或 yarn)
// npm install pubsub-js

import PubSub from 'pubsub-js';

// 订阅消息
const token = PubSub.subscribe('my-topic', (topic, data) => {
console.log('Received message:', data);
});

// 发布消息
PubSub.publish('my-topic', 'Hello, subscribers!');

// 取消订阅
PubSub.unsubscribe(token);

在上面的示例中,我们首先通过 PubSub.subscribe 订阅了名为 'my-topic' 的主题,回调函数将在该主题有消息发布时被触发。然后,我们使用 PubSub.publish 发布了一条消息到 'my-topic' 主题,所有订阅了该主题的回调函数都将接收到通知并执行相应的操作。最后,我们使用 PubSub.unsubscribe 取消了订阅。

通过使用 PubSubJS,我们可以在应用程序中实现松散耦合的组件通信,使得组件之间不直接依赖和调用彼此,而是通过订阅和发布消息进行通信。这种方式可以提高代码的可维护性和可扩展性,使得组件之间的交互更加灵活和解耦。

vuex

当使用 Vuex 进行状态管理时,可以通过共享的 Vuex 存储来实现多个组件之间的通信。下面是一个简单的示例,演示了如何在多个组件之间进行状态的读取和更新:

  1. 创建 Vuex 存储:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// store.js

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
state: {
counter: 0,
},
mutations: {
increment(state) {
state.counter++;
},
},
});

export default store;
  1. 在根组件中注册 Vuex 存储:
1
2
3
4
5
6
7
8
9
10
// main.js

import Vue from 'vue';
import App from './App.vue';
import store from './store';

new Vue({
store,
render: (h) => h(App),
}).$mount('#app');
  1. 在组件中使用 Vuex 状态:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<h2>Counter: {{ counter }}</h2>
<button @click="incrementCounter">Increment</button>
</div>
</template>

<script>
export default {
computed: {
counter() {
return this.$store.state.counter;
},
},
methods: {
incrementCounter() {
this.$store.commit('increment');
},
},
};
</script>

在上面的示例中,我们首先创建了一个名为 counter 的状态,并在 mutations 中定义了 increment 方法来更新该状态。然后,在根组件中通过 new Vuex.Store() 创建了 Vuex 存储,并将其注册到根实例中。接下来,在子组件中,通过计算属性 counter 来读取 counter 状态的值,并使用 incrementCounter 方法来调用 increment mutation 来更新状态。

通过使用 Vuex,我们可以实现多个组件之间共享的状态管理,使得组件之间的通信更加便捷和可维护。在实际应用中,可以根据需求定义不同的状态和相应的 mutations,然后在组件中读取和更新这些状态,实现组件间的数据共享和通信。

mixin

mixin 是 Vue 中一种用于复用组件选项的方式。它允许我们在多个组件之间共享相同的逻辑、方法和数据。通过 mixin,我们可以将通用的功能提取出来,然后在多个组件中混入这些功能。mixin主要包括以下几个方面:

  1. 代码复用:mixin 可以将通用的代码逻辑封装成可复用的部分,避免重复编写相同的代码。
  2. 组件选项混入:mixin 可以将其包含的选项混入到组件中,例如生命周期钩子、计算属性、方法等。
  3. 数据合并:当多个 mixin 应用到同一个组件时,它们的数据会被合并到组件的数据中,避免命名冲突。

使用场景:

  • 当多个组件有相同的方法或逻辑时,可以将这部分代码提取为 mixin,然后在这些组件中混入该 mixin。
  • 当需要在多个组件中共享一些通用的计算属性时,可以将这些计算属性定义在 mixin 中,然后在组件中混入。
  • 当希望在组件中复用某些数据和方法时,可以将它们提取到 mixin 中,然后在多个组件中混入该 mixin。

下面是一个 mixin 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// mixin.js

export default {
data() {
return {
count: 0,
};
},
methods: {
increment() {
this.count++;
},
decrement() {
this.count--;
},
},
};

然后,在组件中使用 mixin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>
<h2>Count: {{ count }}</h2>
<button @click="increment">Increment</button>
<button @click="decrement">Decrement</button>
</div>
</template>

<script>
import mixin from './mixin';

export default {
mixins: [mixin],
};
</script>

在上面的示例中,我们定义了一个名为 mixin 的 mixin,它包含了一个 count 数据和 incrementdecrement 方法。然后,在组件中使用 mixins 选项将该 mixin 混入到组件中,这样组件就可以访问到 mixin 中的数据和方法。通过这种方式,我们实现了在多个组件中共享相同的数据和方法。

需要注意的是,mixin 的使用需要谨慎,因为它可能导致命名冲突和不可预测的行为。在使用 mixin 时,需要确保 mixin 中的选项和组件中的选项不会发生冲突,并且需要清楚地知道 mixin 中的选项会被混入到组件中。