组件通信
props/$emit
在 Vue 中,父子组件的关系可以总结为 prop 向下传递,事件向上传递。父组件通过 prop 给子组件下发数据,子组件通过事件给父组件发送消息。
父组件向子组件传递数据
代码示例
父组件
在父组件里引入子组件,然后通过子组件标签属性来传递数据
<template>
<div>
<h1>Parent</h1>
<my-child my-message="from Parent Component"></my-child>
</div>
</template>
<script>
import MyChild from './Child'
export default {
components: {
MyChild,
}
}
</script>
<style>
</style>
子组件
在子组件显式地用 props
选项声明它预期的数据并使用
<template>
<div>
<h1>Child</h1>
<h3>{{ myMessage }}</h3>
</div>
</template>
<script>
export default {
props: {
myMessage: {
type: String,
default: ''
}
}
}
</script>
<style>
</style>
注意事项
camelCase vs. kebab-case
HTML 特性是不区分大小写的。所以,当使用的不是字符串模板时,camelCase (驼峰式命名) 的 prop 需要转换为相对应的 kebab-case (短横线分隔式命名)。
动态props
与绑定到任何普通的 HTML 特性相类似,我们可以用 v-bind
来动态地将 prop
绑定到父组件的数据。每当父组件的数据变化时,该变化也会传导给子组件:
<my-child :my-message="'from Parent Component'"></my-child>
以上是使用v-bind
的简写方式。
值得注意的是,使用动态绑定的数据时,第一层引号内的数据是一个表达式:
<my-child my-message="1"></my-child>
<my-child :my-message="1"></my-child>
上面两个传递的数据是不同的,第一个传递的是字符串1,后面的才是传递数字1。
单向数据流
Prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是反过来不会。这是为了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得难以理解。
另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop。如果你这么做了,Vue 会在控制台给出警告。
在两种情况下,我们很容易忍不住想去修改 prop 中数据:
- Prop 作为初始值传入后,子组件想把它当作局部数据来用
- Prop 作为原始数据传入,由子组件处理成其它数据输出
对这两种情况,正确的应对方式是:
定义一个局部变量,并用 prop 的值初始化它:
export default { data() { return { message: this.myMessage } }, props: { myMessage: { type: String, default: '' } } }
定义一个计算属性,处理 prop 的值并返回:
export default { computed: { message() { return 'I am' + this.myMessage } } props: { myMessage: { type: String, default: '' } } }
注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。即便引用类型可以,也不要利用这个特性,记住一个原则:组件的数据状态在组件内部管理维护,不要在其他位置去修改它。
子组件向父组件传递数据
代码示例
父组件
在父组件中提供一个子组件内部发布的事件处理函数
在使用子组件的模板的标签上订阅子组件内部发布的事件
<template>
<div>
<h1>Parent</h1>
<my-child @transMessage="showMessage"></my-child>
<p>{{ message }}</p>
</div>
</template>
<script>
import myChild from './Child'
export default {
data() {
return {
message: ''
}
},
methods: {
showMessage(val) {
this.message = val
},
},
components: {
myChild
}
}
</script>
<style>
</style>
子组件
- 在子组件中调用
$emit()
方法发布一个事件
<template>
<div>
<h2>Child</h2>
</div>
</template>
<script>
export default {
mounted() {
this.$emit('transMessage', 'I am from Child component')
}
}
</script>
<style>
</style>
$parent/$children
代码示例
子组件
<template>
<div>
<h2>Child</h2>
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: ''
}
},
mounted() {
// 通过 $parent 获取当前实例的父实例
this.message = this.$parent.parentMessage
}
}
</script>
<style>
</style>
父组件
<template>
<div>
<h1>Parent</h1>
<my-child></my-child>
<p>{{ message }}</p>
</div>
</template>
<script>
import myChild from './Child'
export default {
data() {
return {
message: '',
parentMessage: 'parent message'
}
},
mounted() {
// 通过 $children 获取当前实例的直接子组件
// 获取的数据是个数组,每个子组件是该数组的一个元素
this.message = this.$children[0].childMessage
},
components: {
myChild
}
}
</script>
<style>
</style>
节制地使用
$parent
和$children
- 它们的主要目的是作为访问组件的应急方法。更推荐用 props 和 events 实现父子组件通信
provide/inject(祖先组件向后代组件传递)
2.2.0 新增
provide
和inject
主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
父组件
<template>
<div>
<h1>Parent</h1>
<my-child></my-child>
</div>
</template>
<script>
import myChild from './Child'
export default {
// 父组件提供一个 foo 对象
provide: {
foo: {
name: 'Jack',
age: 18
}
},
components: {
myChild
}
}
</script>
<style>
</style>
子组件
<template>
<div>
<h2>Child</h2>
<ul v-for="item in foo">
<li>{{ item }}</li>
</ul>
<hr>
</div>
</template>
<script>
export default {
// 子组件注入 foo
inject: ['foo'],
data() {
return {
message: null,
}
},
mounted() {
this.message = this.foo
}
}
</script>
<style>
</style>
注意: 这里不论子组件嵌套有多深, 只要调用了
inject
那么就可以注入provide
中的数据,而不局限于只能从当前父组件的props属性中回去数据
ref/$ref(子组件向父组件传递)
ref
被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的$refs
对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
父组件
- 在使用子组件的模板的标签上通过
ref
为子组件赋予一个 ID 引用 - 通过
$ref
获取子组件实例
<template>
<div>
<h1>Parent</h1>
<my-child ref="child"></my-child>
</div>
</template>
<script>
import myChild from './Child'
export default {
components: {
myChild
},
mounted() {
console.log(this.$refs.child)
}
}
</script>
<style>
</style>
子组件
<template>
<div>
<h2>Child</h2>
<hr>
</div>
</template>
<script>
export default {
data() {
return {
message: 'child message',
}
},
}
</script>
<style>
</style>
$attrs/$listeners
在非直接父子组件之间的通信,如A组件下面有B组件,B组件下面有C组件,我们想将A组件的数据传递给C组件或者将C组件的数据传递给A组件,可以有以下几种方法:
- props:使用
props
绑定来进行一级一级的信息传递,如果D组件中状态改变需要传递数据给A,使用事件系统一级级往上传递(繁琐) - $parent/$children:多个
$parent
或者$children
一层一层向上或向下传递(节制地使用这两个实例属性) - eventBus:使用
eventBus
,这种情况下还是比较适合使用,但是碰到多人合作开发时,代码维护性较低, 可读性也低 - Vuex:使用
Vuex
来进行数据管理, 但是如果仅仅是传递数据, 而不做中间处理,使用Vuex
处理感觉有点大材小用了
在vue2.4
中,为了解决该需求,引入了$attrs
和$listeners
, 新增了inheritAttrs
选项。
祖先组件向后代组件传递
vue官方对inheritAttrs
地说明:
默认情况下父作用域的不被认作 props 的 attribute 绑定 (attribute bindings) 将会“回退”且作为普通的 HTML attribute 应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置
inheritAttrs
到false
,这些默认行为将会被去掉。而通过 (同样是 2.4 新增的) 实例 property$attrs
可以让这些 attribute 生效,且可以通过v-bind
显性的绑定到非根元素上。注意:这个选项不影响
class
和style
绑定。
通俗的讲就是:inheritAttrs
属性默认为true
,这时父作用域上的非props属性回退为普通的HTML attribute,如果设置为false
,则不会回退为普通的HTML attribute,这些attribute会被子组件的实例属性 $attrs
收集起来。这些属性不包括class
和style
。
$attrs
:包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class
和style
除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class
和style
除外),并且可以通过v-bind="$attrs"
传入内部组件
A组件
<template>
<div>
<h1>A</h1>
<com-b :a="1" :b="2" :c="3"></com-b>
</div>
</template>
<script>
import comB from './ComB'
export default {
data() {
return {
name: 'A',
}
},
components: {
comB
},
}
</script>
<style>
</style>
B组件
<template>
<div>
<h1>B</h1>
<com-c v-bind="$attrs"></com-c>
</div>
</template>
<script>
import comC from './ComC'
export default {
data() {
return {
name: 'B',
}
},
components: {
comC
},
}
</script>
<style>
</style>
C组件
<template>
<div>
<h1>C</h1>
</div>
</template>
<script>
export default {
inheritAttrs: false,
data() {
return {
name: 'C',
}
},
props: {
a: {
type: Number
}
}
mounted() {
console.log(this.$attrs) // {b: 2, c: 3}
}
}
</script>
<style>
</style>
后代组件向祖先组件传递
$listenders
:包含了父作用域中的 (不含.native
修饰器的)v-on
事件监听器。它可以通过v-on="$listeners"
传入内部组件
A组件
<template>
<div>
<my-parent @test1="showTest1" @test2="showTest2"></my-parent>
</div>
</template>
<script>
import MyParent from './views/Parent'
export default {
data() {
return {
name: 'app',
}
},
components: {
MyParent,
},
methods: {
showTest1() {
console.log('test1...')
},
showTest2(val) {
console.log('test2 ' + val)
}
}
}
</script>
<style>
</style>
B组件
<template>
<div>
<h1>Parent</h1>
<my-child v-on="$listeners"></my-child>
</div>
</template>
<script>
import myChild from './Child'
export default {
data() {
return {
name: 'parent',
}
},
components: {
myChild
}
}
</script>
<style>
</style>
C组件
<template>
<div>
<h2>Child</h2>
</div>
</template>
<script>
export default {
created() {
console.log(this.$listeners)
this.$emit("test1")
this.$emit("test2", 'from child component')
}
}
</script>
<style>
</style>
eventBus
eventBus
又称为事件总线,在vue中可以使用它来作为沟通桥梁的概念, 就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件, 所以组件都可以通知其他组件。
$emit
和$on
的事件必须使用一个空的 Vue 实例作为中央事件总线的实例上,才能够触发,否则会出现子组件$emit
后父组件没有监听到函数的变化
初始化
首先需要创建一个事件总线并将其导出, 以便其他模块可以使用或者监听它
// bus.js
import Vue from 'vue'
export default new Vue()
发送事件
假设我们有两个兄弟组件:Elder-brother
、Younger-brother
,我们需要在这两个兄弟组件之间传递数据
<template>
<div>
<elder-brother></elder-brother>
<younger-brother></younger-brother>
</div>
</template>
<script>
import ElderBrother from './views/Elder-brother'
import YoungerBrother from './views/Younger-brother'
export default {
components: {
ElderBrother,
YoungerBrother
}
}
</script>
<style>
</style>
Elder-brother
组件:
<template>
<div>
<h1>elder brother</h1>
<button @click="sendMessage">send</button>
</div>
</template>
<script>
import EventBus from '../util/bus'
export default {
data() {
return {
message: 'from Elder-brother component',
}
},
methods: {
sendMessage() {
// 利用 $emit 发布事件传递数据
EventBus.$emit('transMessage', this.message)
},
},
}
</script>
<style>
</style>
接收事件
Younger-brother
组件:
<template>
<h1>younger brother</h1>
</template>
<script>
import EventBus from '../util/bus'
export default {
mounted() {
// 利用 $on 接收事件,并执行回调函数
EventBus.$on('transMessage', (param) => {
console.log(param)
})
}
}
</script>
<style>
</style>
这样就完成了兄弟组件之间的数据传递。
移除事件监听者
import { eventBus } from 'bus.js'
EventBus.$off('transMessage')
如果想精确移除某个事件的监听器,还需要传递一个回调函数:
<template>
<div>
<h1>younger brother</h1>
<button @click="destroy">destroy</button>
</div>
</template>
<script>
import EventBus from '../util/bus'
export default {
mounted() {
EventBus.$on('transMessage', this.fn1)
EventBus.$on('transMessage', this.fn2)
},
methods: {
fn1(param) {
console.log(param + 'fn1')
},
fn2(param) {
console.log(param + 'fn2')
},
destroy() {
EventBus.$off('transMessage', this.fn1)
}
}
}
</script>
<style>
</style>
localStorage / sessionStorage
这种通信比较简单,缺点是数据和状态比较混乱,不太容易维护。 通过window.localStorage.getItem(key)
获取数据 通过window.localStorage.setItem(key,value)
存储数据
注意用
JSON.parse()
/JSON.stringify()
做数据格式转换localStorage
/sessionStorage
可以结合vuex
,实现数据的持久保存,同时使用vuex解决数据和状态混乱问题。
v-model
v-model只能实现父子组件之间的数据传递
父组件
<template>
<div>
<h1>Parent</h1>
<p>{{ message }}</p>
<my-child v-model="message"></my-child>
</div>
</template>
<script>
import myChild from './Child'
export default {
data() {
return {
message: 'parent',
}
},
components: {
myChild
}
}
</script>
<style>
</style>
子组件
<template>
<div>
<h2>Child</h2>
<input type="text" v-model="myMessage" @input="changeValue">
</div>
</template>
<script>
export default {
props: {
value: String
},
data() {
return {
myMessage: this.value,
}
},
methods: {
changeValue() {
this.$emit('input', this.myMessage)
}
}
}
</script>
<style>
</style>
- 在 Parent 组件中,我们给自定义的 Child 组件实现了 v-model 绑定了
message
属性。此时相当于给 Child 组件传递了 value 属性和绑定了 input 事件 - 顺理成章,在定义的 child 组件中,可以通过 props 获取 value 属性,根据 props 单向数据流的原则,又将 value 缓存在了 data 里面的
myMessage
上,再在 input 上通过v-model
绑定了myMessage
属性和一个change
事件。当 input 值变化时,就会触发 change 事件,处理 parent 组件通过v-model
给 child 组件绑定的input
事件,触发parent
组件中message
属性值的变化,完成child
子组件改变 parent 组件的属性值。
Vuex
还没了解Vuex,内容应该挺多的,等熟悉了再仔细总结一下。