Vue2 组件传值的方式(共12种)
- props
- $emit / v-on
- sync
- v-model
- ref
- $children / $parent
- $attrs / $listeners
- provide / inject
- EventBus
- Vuex
- $root
- slot
虽然 Vue2 组件通信方式虽然有很多,但是不同方式有不同的应用场景。
父子组件通信
- props
- $emit / v-on
- $attrs / $listeners
- ref
- .sync
- v-model
- $children / $parent
- slot
兄弟组件通信
跨层级组件通信
- provide / inject
- EventBus
- Vuex
- $attrs / $listeners
- $root
具体使用
1. props
用于父组件向子组件传送数据。
子组件接收到数据之后,不能直接修改父组件的值,会报错。
当需要修改props的时候应该在父组件修改。父组件修改后子组件会同步渲染。
如果子组件一定要修改props的话推荐使用computed,计算一个新属性给子组件使用,而不是直接修改。
// 父组件
<template>
<child :msg="msg"></child>
</template>
// 子组件
<template>
<div>{{msg}}</div>
</template>
<script>export default {
// 写法一 用数组接收
props:['msg'],
// 写法二 用对象接收
props: {
msg: String
},
// 写法三 用对象接收,可以限定接收的数据类型、设置默认值、验证等
props:{
msg:{
type:String,
default:'这是默认数据',
// 必传
required: true
}
},
props: {
user:{
type: Object,
// 对象或数组默认值必须从一个工厂函数获取
default: () => return {},
// 自定义验证
validator: () => {
return true/false
}
}
}
}</script>
2.$emit / v-on
我们知道Vue是单向数据流,父组件传递给子组件的值是不能直接修改的。
所以子组件想要修改父组件数据的时候需要先暴露事件并传递值给父组件,父组件监听该事件来完成值的修改。
// 子组件
<template>
<div>
<div>{{ msg }}</div>
<button @click="handleClick">change</button>
</div>
</template>
<script>export default {
props: ["msg"],
methods: {
handleClick() {
const newMsg = "我被修改了";
this.$emit("changeMsg", newMsg);
},
},
};</script>
// 父组件
<template>
<child :msg="msg" v-on:changeMsg="handleChangeMsg" ></child>
</template>
<script>export default {
data() {
return {msg: "我会传递给子组件"}
},
methods:{
handleChangeMsg(newMsg){
this.msg = newMsg
}
}
}</script>
3. .sync
需要修改父组件传递过来的属性的值,每次都需要暴露一个方法过去,然后在父组件监听然后再修改,是不是感觉特别麻烦。
.sync 就是来简化这一过程的。
// 子组件
<template>
<div>
<div>{{ msg }}</div>
<button @click="handleClick">change</button>
</div>
</template>
<script>export default {
props: ["msg"],
methods: {
handleClick() {
const newMsg = "我被修改了";
this.$emit("update:msg", newMsg);
},
},
};</script>
// 父组件
<template>
<child :msg.sync="msg"></child>
</template>
<script>export default {
data() {
return {msg: "我会传递给子组件"}
},
}</script>
父组件不需要再监听事件然后修改值了。
父组件只需要在传递属性的时候加上.sync 修饰符,然后子组件再修改完值的时候暴露update: 属性名的事件就可以了。
4. v-model
.sync 我们知道,是用来简化数据修改的,那还有没有更简单的方法呢?有,那就是v-model
v-model 默认传递名为value的属性,并自动监听input事件。
我们来看个例子:
// 子组件
<template>
<div>
<input type="text" :value="value" @input="handleInput" />
</div>
</template>
<script>export default {
props: {
value: String,
},
methods: {
handleInput(e) {
this.$emit("input", e.target.value);
},
},
};</script>
// 父组件
<template>
<div>{{msg}}</div>
<child v-model="msg"></child>
</template>
<script>
在上面的例子中,我们input框默认值是我会传递值给子组件,当我们修改input框值的时候,父组件的值也会改变。这其实就是双向绑定。
但有时候我们不可能就直接传递属性为value,然后监听input事件,这显然不满足我们的要求。
需要修改属性和事件怎么办呢?这就需要用到我们的model参数了。该属性定义在子组件。包含两个属性,prop用来定义属性名,event 用来定义事件名。
比如,我们想传递过来的属性名为customValue,监听的事件为customInput。
// 子组件
<template>
<div>
<button :url="url" @click="handleChange1">子组件的值:{{url}}</button>
</div>
</template>
<script>export default {
model: {
prop: 'url',
event: 'change1'
},
props: {
//value: String,
// 使用url 代替 value 作为model 的prop
url: {
type: String,
default: ''
}
},
methods: {
handleChange1(){
const newUrl = '我被子修改了呀'
this.$emit('change1',newUrl)
}
},
};</script>
// 父组件
<template>
<div>{{msg}}</div>
<child v-model:url="msg"></child>
</template>
<script>
其他的事件和属性类似,但需要注意一个组件只能使用一个v-model。
5. ref
如果需要直接操作子组件我们就可以使用ref了。
ref 如果在普通的元素上,引用指向的就是该DOM元素;
如果在子组件上,引用的指向就是子组件实例。
// 子组件
<template>
<div>
<div>{{ msg }}</div>
</div>
</template>
<script>export default {
props: ["msg"],
data() {
return {
name: "child6",
};
},
methods: {
say() {
console.log("say child6");
},
},
computed: {
title() {
return this.name + "title";
},
},
};</script>
// 父组件
<template>
<child :msg="msg" ref="childRef"></child>
</template>
<script>
需要注意,如果ref定义在循环中,this.$refs.xxx 会是一个数组。
6. $children / $parent
类似ref,我们可以通过 $children / $parent 直接获取到子组件或父组件。
$children : 获取到一个包含所有子组件(不包含孙子组件)的VueComponent 对象数组,可以直接拿到子组件中所有数据和反方法。
$parent:获取到一个父节点的VueComponent对象,同样包含父节点中所有数据和方法。
// 父组件
export default{
mounted(){
this.$children[0].someMethod() // 调用第一个子组件的someMethod
this.$children[0].name // 获取第一个子组件中的name属性
}
}
// 子组件
export default{
mounted(){
this.$parent.someMethod() // 调用父组件的someMethod
this.$parent.name // 获取父组件中的name属性
}
}
7. $root
和$children / root 可以获取到根实例里。
也就是我们main.js 创建的Vue实例。
new Vue({
router,
store,
render: (h) => h(App),
data: {
name: "根组件",
},
}).$mount("#app");
8. provide / inject
provide / inject 需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。如果你熟悉React,这与React 的context特性很相似。
provide选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的property。
inject 选项应该是:
- 一个字符串数组,或
- 一个对象,对象的key是本地绑定的域名,value是:
- 在可用的注入内容中搜索用的key(字符串或Symbol),或
- 一个对象,该对象的:
- from property 是在可用的注入内容中搜索用的key(字符串或Symbol)
- default property 是降级情况下使用的value。
提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的property还是可响应的。
// 父级组件提供 'foo'
var Provider = {
provide: {
foo: 'bar'
},
// ...
}
// 后代组件注入 'foo'
var Child = {
inject: ['foo'],
created () {
console.log(this.foo) // => "bar"
}
// ...
}
在2.5.0+ 的注入中可以通过设置默认值使其变成可选项:
const Child = {
inject: {
foo: {
from: 'bar',
default: 'foo'
}
}
}
如果它需要从一个不同名字的property注入,则使用from来表示其源property:
const Child = {
inject: {
foo: {
from: 'bar',
default: 'foo'
}
}
}
与prop的默认值类似,你需要对非原始值使用一个工厂方法:
const Child = {
inject: {
foo: {
from: 'bar',
default: () => [1, 2, 3]
}
}
}
9.$attrs / $listeners
多层嵌套组件传递数据时,如果只是传递数据,而不做中间处理的话,就可以使用$attrs / $listeners,比如,父组件向孙子组件传递数据时。
$attrs 包含了父作用域中不作为prop被识别(且获取)的attribute绑定(class和style除外)。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定(class和style除外)。
$ listeners 包含了父作用域的(不含.native修饰器的)v-on事件监听器。
//子组件
props: ['msg'],
created() {
// 获取没在props中定义的但是传递过来的属性,除去 class、style
console.log(this.$attrs); // {id: 'childId', name: 'randy'}
// 获取除.native修饰的原生事件
console.log(this.$listeners); // {customChange(){}}
// 如果单纯只传递属性给子组件,不需要处理,我们可以直接传递$attrs、$listeners
},
// 父组件
<template>
<child :msg="msg" @click.native="handleClick" @customChange="handleChange" class="child" id="childId"></child>
</template>
<script>
export default {
data() {
return {msg: "我会传递给子组件", name: 'randy'}
},
}
10. slot
slot 可以用在父组件直接向子组件传递内容,我们在子组件标签内的内容都可以传递过去。没命名的slot 默认名称是default。
// 父组件
<h3>默认插槽 default</h3>
<Slot1>
<div>slot1</div>
<div>哈哈</div>
</Slot1>
<h3>老具名插槽</h3>
<Slot1>
<div>slot传递过来的 2.6已被废弃但是还能使用 vue3彻底移除</div>
<div slot="header">header</div>
<div slot="footer">footer</div>
<div slot="content">content</div>
</Slot1>
<h3>新具名插槽</h3>
<Slot1>
<template v-slot:header>header</template>
<template v-slot:footer>footer</template>
<template v-slot:content>content</template>
</Slot1>
<h3>新具名插槽缩写 #</h3>
<Slot1>
<div>v-slot传递过来的</div>
<template #header>header2</template>
<template #footer>footer2</template>
<template #content>content2</template>
</Slot1>
// 子组件 Slot1
<!-- 默认插槽 -->
<slot>我是后备内容,没有传递的时候展示</slot>
<slot name="header"></slot>
<slot name="content"></slot>
<slot name="footer"></slot>
不单父组件传递内容给子组件,子组件还可以通过作用域插槽传递数据给父组件。
// 父组件
<h3>老作用域插槽</h3>
<!-- scope 被 2.5.0 新增的 slot-scope 取代 -->
<!-- 除了 scope 只可以用于 <template> 元素,其它和 slot-scope 都相同。 -->
<Slot2>
<template slot="main" slot-scope="slotProps">
<div>user name: {{ slotProps.user.name }}</div>
<div>user age: {{ slotProps.user.age }}</div>
<div></div>
</template>
<template slot-scope="slotProps">
<div>user name: {{ slotProps.user.name }}</div>
<div>user age: {{ slotProps.user.age }}</div>
</template>
<template slot="footer" slot-scope="{ user: { name, age } }">
<div>user name: {{ name }}</div>
<div>user age: {{ age }}</div>
</template>
</Slot2>
<h3>新作用域插槽</h3>
<Slot2>
<template v-slot:main="slotProps">
<div>user name: {{ slotProps.user.name }}</div>
<div>user age: {{ slotProps.user.age }}</div>
<div></div>
</template>
<template v-slot:default="slotProps">
<div>user name: {{ slotProps.user.name }}</div>
<div>user age: {{ slotProps.user.age }}</div>
</template>
<template v-slot:footer="{ user: { name, age } }">
<div>user name: {{ name }}</div>
<div>user age: {{ age }}</div>
</template>
</Slot2>
// 子组件 Slot2
<template>
<slot v-bind:user="user1"> </slot>
<slot name="main" v-bind:user="user2"> </slot>
<slot name="footer" :user="user3"> </slot>
</template>
顺便说一说this.scopedSlots
this.$slot
this.slots.foo中被找到)。default property 包括了所有没有被包含在具名插槽中的节点,或 v-slot:default 的内容。
每个属性包含一个VNode的数组。
请注意插槽不是响应性的。如果你需要一个组件可以在被传入的数据发生变化时重新渲染,我们建议改变策略,依赖诸如props或data等响应性实例选项。
this.$scopedSlots
this.slots的返回值。
console.log(
this.$scopedSlots.default({
user: this.user1,
})
);
就类似于前面的,不过有$scopedSlots, $slots 就不会有值了。
11. EventBus
EventBus主要利用了Vue实例 once、emit这四个方法来进行数据的传递。
首先我们定义一个EventBus
// Bus.js
import Vue from "vue"
export default new Vue()
import Bus from "./Bus.js"
// 暴露事件,传递数据
Bus.$emit("sendMsg", "这是要向外部发送的数据")
// 监听事件,进行处理
Bus.$on("sendMsg", data => {
console.log("这是接收到的数据:", data)
})
// 监听事件,进行处理,但是只监听一次,后面会自动取消监听
Bus.$once("sendMsg", data => {
console.log("这是接收到的数据:", data)
})
// 取消监听
Bus.$off("sendMsg")
12. Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
Vue3
Vue3组件通讯有9种:
- props
- emit
- ref / expose
- attrs
- v-model
- provide / inject
- mitt
- Vuex
- Pinia
父子组件通信
- props
- emit
- attrs
- ref / expose
- v-model
兄弟组件通信
跨层级组件通信
- provide/inject
- attrs
- mitt
- Vuex
- Pinia
具体使用
1. props
props用于父组件向子组件传值,使用方式和vue2没什么差别。
2. emit
emit和vue2的$emit使用方式是一样的,但是需要注意:
vue3中,emit是在setup函数的第二个参数上。
vue3中,子组件暴露的方法需要在emits定义。
// 父组件
<template>
<child :msg="msg" @changeMsg="handleChangeMsg"></child>
</template>
<script>import { defineComponent, ref } from "vue";
export default defineComponent({
setup(props, { emit }) {
const msg = ref("message");
const handleChangeMsg = (newValue) => {
msg.value = newValue;
};
return { msg, handleChangeMsg };
},
});</script>
// 子组件
<template>
<div class="">
<div>{{ msg }}</div>
<button @click="handelClick">changeMsg</button>
</div>
</template>
<script>import { defineComponent } from "vue";
export default defineComponent({
props: {
msg: String,
},
emits: { changeMsg: null },
setup(props, { emit }) {
const handelClick = () => {
emit("changeMsg", "我被改变了");
};
return { handelClick };
},
});</script>
3. ref / expose
vue3中定义ref需要使用ref()定义。
使用的时候需要注意:
- 当组件没定义expose暴露内容的时候,通过ref获取到的就是组件自身的内容,也就是setup函数return的内容。
- 当定义了expose暴露内容的时候,通过ref获取到的就是组件expose暴露内容,并且setup函数return的内容会失效,也就是会被覆盖。
// 父组件
<template>
<child :msg="msg" ref="ref1"></child>
</template>
<script>import { defineComponent, ref, onMounted } from "vue";
export default defineComponent({
setup(props, { emit }) {
const msg = ref("message");
const ref1 = ref(null)
onMounted(() => {
console.log(ref1.value)
console.log(ref1.value.name)
console.log(ref1.value.user)
ref1.value.say()
})
return { msg, ref1 };
},
});</script>
// 子组件
<template>
<div class="">{{ msg }}</div>
</template>
<script>import { defineComponent, ref, reactive } from "vue";
export default defineComponent({
props: ["msg"],
emits: {},
setup(props, { expose }) {
const say = () => {
console.log("RefChild say");
};
const name = ref("RefChild");
const user = reactive({ name: "randy", age: 27 });
// 如果定义了会覆盖return中的内容
// expose({
// user,
// });
return {
name,
user,
say,
};
},
});</script>
4. attrs
在vue2中,我们知道this.$attrs包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。
在vue3中有了更改,attrs被转移到setup的第二个参数context上,context.attrs。并且class 和 style也都不再忽略了。也就是说class 和 style也会在attrs里面。
使用方式和vue2还是一样的,多层嵌套组件传递数据时,如果只是传递数据,而不做中间处理的话就可以用attrs,比如父组件向孙子组件传递数据时。
//子组件
props: ['msg'],
setup(props, {attrs}) {
// 获取没在props中定义的但是传递过来的属性,包括 class、style
console.log(attrs); // {id: 'childId', name: 'randy', customChange(){}, class: 'child'}
// 如果单纯只传递属性给子组件,不需要处理,我们可以直接传递attrs
},
// 父组件
<template>
<child :msg="msg" @customChange="handleChange" class="child" id="childId"></child>
</template>
<script>
5. v-model
vue3的v-model我们只需要注意两点:
在vue2中,一个组件只能绑定一个v-model,但是vue3中可以绑定多个。
在vue2中如果不指明model属性,那么它的值默认是value,事件默认是input事件。但是在vue3中不需要model属性了,并且它的值默认是modelValue,事件默认是update:modelValue事件。
// 父组件
<Child1 v-model="name1" />
<!--比使用默认名,自定义名字为name-->
<!-- <Child1 v-model:name="name1" /> -->
// 子组件
<template>
<div class="child1">
<button @click="changeName">改变值</button>
</div>
</template>
<script>import { defineComponent } from "vue";
export default defineComponent({
props: {
modelValue: String,
// name: String,
},
setup(props,) {
const changeName = () => {
context.emit("update:modelValue", "demi");
// context.emit("update:name", "demi");
};
return {
changeName,
};
},
});</script>
如果不想用默认名,我们可以在v-model:后面加上自定义的名字。
6. provide / inject
provide / inject需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
原理和vue2一样,可能就是方法有所改变。
// 父组件
<script setup>import { provide } from "vue"
provide("name", "沐华")</script>
// 子组件
<script setup>import { inject } from "vue"
const name = inject("name")
console.log(name) // 沐华</script>
上面的写法是setup的语法糖写法,更简洁.
7. mitt
我们知道vue3去除了$on $once $off方法,所以没办法再使用 EventBus 跨组件通信了。但是现在有了一个替代的方案 mitt.js,原理还是 EventBus。
先安装 npm i mitt -S
然后像以前封装 bus 一样,封装一下
import mitt from 'mitt'
const mitt = mitt()
export default
然后两个组件之间通信的使用
// 组件 A
<script setup>import mitt from './mitt'
const handleClick = () => {
mitt.emit('handleChange')
}</script>
// 组件 B
<script setup>import mitt from './mitt'
import { onUnmounted } from 'vue'
const someMethed = () => { ... }
mitt.on('handleChange',someMethed)
onUnmounted(()=>{
mitt.off('handleChange',someMethed)
})</script>
8. Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
9. Pinia
如果你之前使用过 vuex 进行状态管理的话,那么 pinia 就是一个类似的插件。它是最新一代的轻量级状态管理插件。按照尤雨溪的说法,vuex 将不再接受新的功能,建议将 Pinia 用于新的项目。