# Vue2.x 的通信
前言
介绍 Vue2.x 中 17 种组件之间的通信方式,在开发时灵活运用不同的通信方式,找到最佳应用场景
# 1.父子组件通信
# 1.1 props
可以实现向子组件传递数据,也可以向父组件传递数据
# 1.1.1 基本数据类型
子组件 props 为响应式数据,data 接收后失去响应
案例 1:子组件不修改 props 值
刷新全屏/自适应可以得出结论:props 为响应式数据,父组件传递的数据修改,子组件会跟着修改
案例 2:子组件修改 props 值
刷新全屏/自适应可以得出结论:props 可以修改,但是 Vue 会报错,直接修改 props 是不规范的不建议
案例 3:子组件通过 data 接收 props 值
刷新全屏/自适应可以得出结论:props 被 data 接收后,父组件修改,data 数据不会响应变化
案例 4:子组件通过 data 接收 props 值,然后通过 watch 监听 props 变化,重新赋值 data
刷新全屏/自适应
# 1.1.2 复杂数据类型
1.子组件 props 为响应式数据,data 接收后由于指向同一个地址空间,仍然有响应式
2.数据类型如果是函数,可以修改父组件相关逻辑,也可以修改其状态,向父组件传递数据
案例 1.props 传递方法,子组件向父组件传递数据
刷新全屏/自适应
# 1.1.3 原理
# 1.2 $emit
用于子组件向父组件传递数据
# 1.2.1 案例
# 1.2.2 原理
# 1.3 ref
用于实现父组件向子组件传递数据,父组件获取子组件数据(dom)
1.获取组件的 dom
this.$refs.contain
12.执行子组件的方法
this.$refs.contain.show(val) //此处传递参数,实现向子组件传递数据
1
# 1.3.1 用法
parent.vue
<template> <child ref="contain"></child> </template> <script> import child from "./child" export default { components: { child, }, mounted() { console.log(this.$refs.contain) // 父组件获取子组件的dom console.log(this.$refs.contain.show()) // 父组件调用子组件的方法 }, } </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15child.vue
<template> <div>my-component</div> </template> <script> export default { methods: { show() { alert(1) }, }, } </script>
1
2
3
4
5
6
7
8
9
10
11
12
主要是为了解决子组件给父组件传递数据或方法的通信问题
parent.vue
<template>
<child @childfn="childfn" />
</template>
<script>
export default {
methods: {
childfn(val) {
console.log("子组件传递给父组件的数据", val)
},
},
}
</script>
2
3
4
5
6
7
8
9
10
11
12
child.vue
<template>
<button @click="handleClick">点我给父组件传递数据:123</button>
</template>
<script>
export default {
methods: {
handleClick() {
this.$emit("childfn", "123") // 第一个参数为方法名,和父组件上事件名称对应,第二个参数为需要传递的值
},
},
}
</script>
2
3
4
5
6
7
8
9
10
11
12
# 1.3.2 原理
# 1.4 $parent
1.执行父组件的方法
this.$parent.close()
12.执行子组件的方法
this.$children[0].close()
1
智能组件和木偶组件 collapse-item.vue
<template>
<div>
<div class="title" @click="change">{{ title }}</div>
<div v-show="show">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
props: ["title"],
data() {
return { show: false }
},
methods: {
change() {
this.$parent.cut(this._uid)
this.show = !this.show
},
},
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
collapse.vue
<template>
<div class="wrap">
<slot></slot>
</div>
</template>
<script>
export default {
methods: {
cut(childId) {
// 只要儿子点击了 我就知道当前点击的是谁
this.$children.forEach((child) => {
if (child._uid !== childId) {
child.show = false
}
})
},
},
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
parent.vue
<template>
<collapse>
<collapse-item title="react">内容1</collapse-item>
<collapse-item title="vue">内容2</collapse-item>
<collapse-item title="node">内容3</collapse-item>
</collapse>
</template>
<script>
import collapse from "./collapse"
import collapseItem from "./collapse-item"
export default {
components: {
collapse,
collapseItem,
},
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
平级通信 $parent 获取父组件的实列 $children 获取所有的儿子
# 1.5 $children
1.执行父组件的方法
this.$parent.close()
12.执行子组件的方法
this.$children[0].close()
1
智能组件和木偶组件 collapse-item.vue
<template>
<div>
<div class="title" @click="change">{{ title }}</div>
<div v-show="show">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
props: ["title"],
data() {
return { show: false }
},
methods: {
change() {
this.$parent.cut(this._uid)
this.show = !this.show
},
},
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
collapse.vue
<template>
<div class="wrap">
<slot></slot>
</div>
</template>
<script>
export default {
methods: {
cut(childId) {
// 只要儿子点击了 我就知道当前点击的是谁
this.$children.forEach((child) => {
if (child._uid !== childId) {
child.show = false
}
})
},
},
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
parent.vue
<template>
<collapse>
<collapse-item title="react">内容1</collapse-item>
<collapse-item title="vue">内容2</collapse-item>
<collapse-item title="node">内容3</collapse-item>
</collapse>
</template>
<script>
import collapse from "./collapse"
import collapseItem from "./collapse-item"
export default {
components: {
collapse,
collapseItem,
},
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
平级通信 $parent 获取父组件的实列 $children 获取所有的儿子
# 1.6 v-slot
# 1.6.1 匿名插槽
也叫默认插槽,没有命名,有且只有一个
- 方式一:
- 方式二:
# 1.6.2 具名插槽
相对匿名插槽组件 slot 标签带 name 命名的
# 1.6.2 作用域插槽
子组件内数据可以被父页面拿到(解决了数据只能从父页面传递给子组件)
// 父组件
<todo-list>
<template v-slot:todo="slotProps" >
{{slotProps.user.firstName}}
</template>
</todo-list>
//slotProps 可以随意命名
//slotProps 接取的是子组件标签slot上属性数据的集合所有v-bind:user="user"
// 子组件
<slot name="todo" :user="user" :test="test">
{{ user.lastName }}
</slot>
data() {
return {
user:{
lastName:"Zhang",
firstName:"yue"
},
test:[1,2,3,4]
}
},
// {{ user.lastName }}是默认数据 v-slot:todo 当父页面没有(="slotProps")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1.7 sync
// 父组件
<home :title.sync="title" />
//编译时会被扩展为
<home :title="title" @update:title="val => title = val"/>
// 子组件
// 所以子组件可以通过$emit 触发 update 方法改变
mounted(){
this.$emit("update:title", '这是新的title')
}
2
3
4
5
6
7
8
9
10
# 1.8 $attrs
主要是为了解决父组件向子组件批量传递属性($attrs)或方法($listeners)的问题
$listeners
首先来一个父组件 parent.vue
<template>
<child @click="change"></child>
</template>
<script>
import child from "./child"
export default {
components: {
child,
},
methods: {
change() {
alert(1)
},
},
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
然后来一个子组件 child.vue
<template>
<button>点我啊</button>
</template>
2
3
点击子组件没有触发父组件的方法
原因:如果想在父级中 绑定原生事件给组件 需要加.native 不加就认为是一个普通属性
1.native
将父组件添加.native:
<template> <child @click.native="change"></child> </template> <script> import child from "./child" export default { components: { child, }, methods: { change() { alert(1) }, }, } </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16现在子组件可以正常调用父组件的方法了
如果上述子组件外面嵌套有其他元素用.native 就失效了
2.@click='$listeners.click()
父组件不用加.native,子组件外层嵌套其他元素可以用这种方式在 children.vue 中调用父组件的方法
<template> <div> <button @click="$listeners.click()">点我啊</button> </div> </template>
1
2
3
4
5现在子组件可以正常调用父组件的方法了
如果上述父组件中有多个方法,这种方式又不行了
3.v-on='$listeners'
父组件中存在多个方法
<template> <child @click="change" @mouseup="change"></child> </template> <script> import child from "./child" export default { components: { child, }, methods: { change() { alert(1) }, }, } </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16子组件可以这样写:
<template> <div> <button v-on="$listeners">点我啊</button> </div> </template>
1
2
3
4
5可以正常调用父组件中的方法
# 1.9 $listeners
# 1.10 v-model
主要是为了解决父组件向子组件批量传递属性($attrs)或方法($listeners)的问题
$listeners
首先来一个父组件 parent.vue
<template>
<child @click="change"></child>
</template>
<script>
import child from "./child"
export default {
components: {
child,
},
methods: {
change() {
alert(1)
},
},
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
然后来一个子组件 child.vue
<template>
<button>点我啊</button>
</template>
2
3
点击子组件没有触发父组件的方法
原因:如果想在父级中 绑定原生事件给组件 需要加.native 不加就认为是一个普通属性
1.native
将父组件添加.native:
<template> <child @click.native="change"></child> </template> <script> import child from "./child" export default { components: { child, }, methods: { change() { alert(1) }, }, } </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16现在子组件可以正常调用父组件的方法了
如果上述子组件外面嵌套有其他元素用.native 就失效了
2.@click='$listeners.click()
父组件不用加.native,子组件外层嵌套其他元素可以用这种方式在 children.vue 中调用父组件的方法
<template> <div> <button @click="$listeners.click()">点我啊</button> </div> </template>
1
2
3
4
5现在子组件可以正常调用父组件的方法了
如果上述父组件中有多个方法,这种方式又不行了
3.v-on='$listeners'
父组件中存在多个方法
<template> <child @click="change" @mouseup="change"></child> </template> <script> import child from "./child" export default { components: { child, }, methods: { change() { alert(1) }, }, } </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16子组件可以这样写:
<template> <div> <button v-on="$listeners">点我啊</button> </div> </template>
1
2
3
4
5可以正常调用父组件中的方法
# 2.兄弟组件通信
# 2.1 任意组件查找
组件为树状结构,可以通过组件名称,向上向下查找到具体组件
- 1.向上找到最近的指定组件
// assist.js
// 由一个组件,向上找到最近的指定组件
function findComponentUpward(context, componentName) {
let parent = context.$parent
let name = parent.$options.name
while (parent && (!name || [componentName].indexOf(name) < 0)) {
parent = parent.$parent
if (parent) name = parent.$options.name
}
return parent
}
export { findComponentUpward }
2
3
4
5
6
7
8
9
10
11
12
13
- 2.向上找到所有的指定组件
// assist.js
// 由一个组件,向上找到所有的指定组件
function findComponentsUpward(context, componentName) {
let parents = []
const parent = context.$parent
if (parent) {
if (parent.$options.name === componentName) parents.push(parent)
return parents.concat(findComponentsUpward(parent, componentName))
} else {
return []
}
}
export { findComponentsUpward }
2
3
4
5
6
7
8
9
10
11
12
13
14
- 3.向下找到最近的指定组件
// assist.js
// 由一个组件,向下找到最近的指定组件
function findComponentDownward(context, componentName) {
const childrens = context.$children
let children = null
if (childrens.length) {
for (const child of childrens) {
const name = child.$options.name
if (name === componentName) {
children = child
break
} else {
children = findComponentDownward(child, componentName)
if (children) break
}
}
}
return children
}
export { findComponentDownward }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 4.向下找到所有指定的组建
// assist.js
// 由一个组件,向下找到所有指定的组件
function findComponentsDownward(context, componentName) {
return context.$children.reduce((components, child) => {
if (child.$options.name === componentName) components.push(child)
const foundChilds = findComponentsDownward(child, componentName)
return components.concat(foundChilds)
}, [])
}
export { findComponentsDownward }
2
3
4
5
6
7
8
9
10
- 5.找到指定组件中的兄弟组件
// assist.js
// 由一个组件,找到指定组件的兄弟组件
function findBrothersComponents(context, componentName, exceptMe = true) {
let res = context.$parent.$children.filter((item) => {
return item.$options.name === componentName
})
let index = res.findIndex((item) => item._uid === context._uid)
if (exceptMe) res.splice(index, 1)
return res
}
export { findBrothersComponents }
2
3
4
5
6
7
8
9
10
11
# 3.跨级组件通信
# 3.1 Vuex
state:定义存贮数据的仓库 ,可通过this.$store.state 或mapState访问
getter:获取 store 值,可认为是 store 的计算属性,可通过this.$store.getter 或
mapGetters访问
mutation:同步改变 store 值,为什么会设计成同步,因为mutation是直接改变 store 值,
vue 对操作进行了记录,如果是异步无法追踪改变.可通过mapMutations调用
action:异步调用函数执行mutation,进而改变 store 值,可通过 this.$dispatch或mapActions
访问
modules:模块,如果状态过多,可以拆分成模块,最后在入口通过...解构引入
2
3
4
5
6
7
8
# 3.2 $root
// 父组件
mounted(){
console.log(this.$root) //获取根实例,最后所有组件都是挂载到根实例上
console.log(this.$root.$children[0]) //获取根实例的一级子组件
console.log(this.$root.$children[0].$children[0]) //获取根实例的二级子组件
}
2
3
4
5
6
# 3.3 provide & inject
主要是为了解决跨级组件间的通信问题
父组件中通过 provide
来提供变量,然后在子组件或后代组件中通过 inject
来注入变量
# 1.1 用法
父组件:提供数据
export default { provide: { name: "Aresn", }, }
1
2
3
4
5子组件:使用数据
export default { inject: ["name"], mounted() { console.log(this.name) //Aresn }, }
1
2
3
4
5
6注意
provide 和 inject 绑定的数据不是可响应的,这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
# 1.2 应用
- 1.替代 Vuex:将用户的登录信息保存起来
用户的登录信息维护、通知信息维护等全局的状态和数据,通过inject
获取,我们把整个 app.vue 实例通过porvide
对外提供
任何组件路只要通过inject
注入 app.vue 的 app,都可以直接通过this.app.xxx来访问 app.vue 的data
、computed
、methods
。
app.vue:提供数据,由于只会渲染一次,利用这个特性,很适合做一次性全局的状态数据管理
<script> export default { provide() { return { app: this, } }, data() { return { userInfo: null, } }, methods: { getUserInfo() { $.ajax("/user/info", (data) => { this.userInfo = data // 这里通过 ajax 获取用户信息后,赋值给 this.userInfo }) }, }, mounted() { this.getUserInfo() }, } </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访问数据:只要通过
inject
注入app
后,就可以直接访问userInfo
的数据了<template> <div> {{ app.userInfo }} </div> </template> <script> export default { inject: ["app"], } </script>
1
2
3
4
5
6
7
8
9
10修改数据:调用方法,页面修改个人资料,需要重新获取
userInfo
<template> <div> {{ app.userInfo }} </div> </template> <script> export default { inject: ["app"], methods: { changeUserInfo() { $.ajax("/user/update", () => { this.app.getUserInfo() // 直接通过 this.app 就可以调用 app.vue 里的方法 }) }, }, } </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 3.4 $dispatch & $broadcast
主要解决:1.父组件向子组件(支持跨级)传递数据、2.子组件向父组件(支持跨级)传递数据的问题
emitter.js
function broadcast(componentName, eventName, params) { this.$children.forEach((child) => { const name = child.$options.name if (name === componentName) { child.$emit.apply(child, [eventName].concat(params)) } else { broadcast.apply(child, [componentName, eventName].concat([params])) } }) } export default { methods: { dispatch(componentName, eventName, params) { let parent = this.$parent || this.$root let name = parent.$options.name while (parent && (!name || name !== componentName)) { parent = parent.$parent if (parent) { name = parent.$options.name } } if (parent) { parent.$emit.apply(parent, [eventName].concat(params)) } }, broadcast(componentName, eventName, params) { broadcast.call(this, componentName, eventName, params) }, }, }
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注意
其底层本质就是一个发布订阅的应用场景
# 2.1 用法
子组件调用
dispatch
方法,向上级指定的组件实例(最近的)上触发自定义事件,并传递数据,且该上级组件已预先通过$on
监听了这个事件父组件调用
broadcast
方法,向下级指定的组件实例(最近的)上触发自定义事件,并传递数据,且该下级组件已预先通过$on
监听了这个事件import Emitter from "../mixins/emitter.js" export default { mixins: [Emitter], methods: { handleDispatch() { this.dispatch("组件名称", "函数名称", "函数参数") // ① }, handleBroadcast() { this.broadcast("组件名称", "函数名称", "函数参数") // ② }, }, }
1
2
3
4
5
6
7
8
9
10
11
12
# 2.2 应用
broadcast
<template> <button @click="handleClick">触发事件</button> </template> <script> import Emitter from "../mixins/emitter.js" export default { name: "componentA", mixins: [Emitter], methods: { handleClick() { this.broadcast("componentB", "on-message", "Hello Vue.js") }, }, } </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// B.vue export default { name: "componentB", created() { this.$on("on-message", this.showMessage) }, methods: { showMessage(text) { window.alert(text) }, }, }
1
2
3
4
5
6
7
8
9
10
11
12dispatch
<!-- A.vue --> <template> <button @click="handleClick">触发事件</button> </template> <script> import Emitter from "../mixins/emitter.js" export default { name: "componentA", mixins: [Emitter], methods: { handleClick() { this.dispatch("componentB", "on-message", "Hello Vue.js") }, }, } </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// B.vue export default { name: "componentB", created() { this.$on("on-message", this.showMessage) }, methods: { showMessage(text) { window.alert(text) }, }, }
1
2
3
4
5
6
7
8
9
10
11
12
# 4.非组件间通信
# 4.1 bus
- vue 版的发布订阅模式,平级组件数据传递,这种情况下可以使用中央事件总线的方式
// 在 main.js
Vue.prototype.$eventBus = new Vue()
// 传值组件
this.$eventBus.$emit("eventTarget", "这是eventTarget传过来的值")
// 接收组件
this.$eventBus.$on("eventTarget", (v) => {
console.log("eventTarget", v) //这是eventTarget传过来的值
})
2
3
4
5
6
7
8
9
10
# 4.2 observable
// 文件路径 - /store/store.js
import Vue from "vue"
export const store = Vue.observable({ count: 0 })
export const mutations = {
setCount(count) {
store.count = count
},
}
2
3
4
5
6
7
8
<template>
<div>
<label for="bookNum">数 量</label>
<button @click="setCount(count + 1)">+</button>
<span>{{ count }}</span>
<button @click="setCount(count - 1)">-</button>
</div>
</template>
<script>
import { store, mutations } from "../store/store" // Vue2.6新增API Observable
export default {
name: "Add",
computed: {
count() {
return store.count
},
},
methods: {
setCount: mutations.setCount,
},
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22