趁着Vue3还没发布,先把vue2的各个知识点、源码、轮子全部温习一遍。 友情提示:阅读本文大概需要** 40****分钟**
前言
本篇文章将复习 Vue.js 父子组件之间通信的九种方式,无可否认,现在无论大厂还是小厂都已经用上了 Vue.js 框架,生态圈丰富,社区活跃,掌握 Vue 已经成为刚需而不是优势。Vue3发布之前,抓紧一起复习一下 Vue 的组件传值吧。
Vue组件传值
方式①.Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。
// Vuex 组件传值
// Vuex.js
const store = new Vuex.Store({
strict:true, // 开启严格模式 确保state 中的数据只能 mutations 修改
state:{
count:0
},
mutations:{ // 更改数据的方法
add(state){
state.count++
},
// 提交载荷用法
// add(state,n){
// state.count += n
// },
sub(state){
state.count--
}
}
})
// 在组件的computed中使用
template
div class="hello"
button @click="add"+/button
h2{{count}}/h2
button @click="sub"-/button
/div
/template
script
export default {
name: 'HelloWorld',
computed:{
count(){
return this.$store.state.count;
}
},
methods:{
add(){
this.$store.commit('add');
},
//提交载荷用法
// add(){
// this.$store.commit('add',10);
// },
//对象风格的提交方式
// store.commit({
// type: 'add',
// n: 10
// })
sub(){
this.$store.commit('sub');
}
}
}
/script
优点:
缺点:
方式②.EventBus
通过共享一个vue实例,使用该实例的
$on
以及
$emit
实现数据传递。
// EventBus 使用方法
// bus.js
import Vue from 'vue'
export default new Vue({})
// 组件a
import bus from './bus.js'
export default {
created () {
bus.$on('event-name', (preload) = {
// ...
})
}
}
// 组件b
import bus from './bus.js'
export default {
created () {
bus.$emit('event-name', preload)
}
}
优点:
缺点:
由于是都使用一个Vue实例,所以容易出现重复触发的情景,例如: 1.多人开发时,A、B两个人定义了同一个事件名。 2.两个页面都定义了同一个事件名,并且没有用`$off`销毁(常出现在路3.由切换时)。在for出来的组件里注册。
方式③.props和 $emit/$on
最基本的父组件给子组件传递数据方式,将我们自定义的属性传给子组件,子组件通过$emit方法,触发父组件v-on的事件,从而实现子组件触发父组件方法。
$attrs
/
$listeners
可以将父组件的props和事件监听器继承给子元素,在子组件可以调用到父组件的事件和props。
// props组件传值最常用
// asyncData为异步获取的数据,想传递给子组件使用
// 父组件
template
div
父组件
child :child-data="asyncData"/child
/div
/template
script
import child from './child'
export default {
data: () = ({
asyncData: ''
}),
components: {
child
},
created () {
},
mounted () {
// setTimeout模拟异步数据
setTimeout(() = {
this.asyncData = 'async data'
console.log('parent finish')
}, 2000)
}
}
/script
// 子组件
template
div
子组件{{childData}}
/div
/template
script
export default {
props: ['childData'],
data: () = ({
}),
created () {
console.log(this.childData) // 空值
},
methods: {
}
}
/script
// prop 也经常和 $emit/$on同时使用
// 仅仅使用$emit/$on传值,可以首先新建一个bus.js
// 父组件与子组件都要import bus.js
import {bus} from '../../bus.js'
created(){
bus.$on('custTreeSay',(id)={
//监听传值--机构代码
this.instCode = id;
//关闭弹窗
this.popupVisibleTree = false;
//调用查询方法刷新通讯录列表
this.query();
});
bus.$on('custTreeSayName',(name)={
//监听传值--机构名称
this.instName = name;
});
}
// 在子组件中定义点击事件,调用父组件方法通过$emit将相应值传给父组件
span @click="propInstCode(model);propInstName(model)"
{{model.name}}
/span
script type="text/javascript"
import {bus} from '../../bus.js'
export default {
props: ['model'],//这里通过props接收父组件的传值
//method钩子定义传值方法,这边需要传不同的值
methods: {
//通过总线将值传给父组件
propInstCode:function (model) {
//$emit触发当前实例事件
bus.$emit('custTreeSay',this.model.id);
},
propInstName:function (model) {
bus.$emit('custTreeSayName',this.model.name);
}
},
}
优点:
缺点:
方式④.provide / inject
简单的来说就是在父组件中通过provider来提供变量,然后在子组件中通过inject来注入变量。需要注意的是这里不论子组件有多深,只要调用了inject那么就可以注入provider中的数据。而不是局限于只能从当前父组件的prop属性来获取数据。
// provide / inject 组件传值
// 父组件
template
div
comp-one ref = "comp"
span slot-scope = "props" ref="span"{{props.value}} {{props.aaa}} {{value}}/span
/comp-one
/div
/template
new Vue({
components: {
CompOne: ChildComponent
},
//父组件通过provide将自己的数据以对象形式传出去
provide () {
return {
yeye: this
value: this.value
}
},
el: '#root',
data () {
return {
value: '本组件的123'
}
},
mounted () {
console.log(this.$refs.comp.value)
console.log(this.$refs.span)
}
// 子组件1
div :style = "style"
slot :value = "value" :aaa = "aaa"/slot
sun-child-component/sun-child-component
/div
const ChildComponent = {
name: 'comp',
components: {
SunChildComponent
},
data () {
return {
value: 'component val',
aaa: 'component aaa'
}
}
}
// 子组件2
divchild component/div
const SunChildComponent = {
// 跨级使用了父组件的数据
inject: ['yeye'],
mounted () {
console.log(this.yeye)
}
}
优点:
缺点:
方式⑤.slot
2.1.0新增作用域插槽,你可以在组件的html模版里添加自定义内容,这个内容可以是任何代码模版,就像:
navigation-link url="/profile"
!-- 添加一个 Font Awesome 图标 --
span class="fa fa-user"/span
Your Profile
/navigation-link
父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。你也可以通过slot-scope属性来实现从子组件将一些信息传递给父组件,注意这个属性是vue2.1.0+新增的。
// slot 组件传值
// App.vue
template
l-input value='用户名' /
/template
// 父组件
templale
div class="control"
slot :is-group="isGroup"/slot
/div
/template
script
export default {
data () {
isGroup: true
}
}
/script
// 子组件
template
template v-if="!isGroup"
div class="control"
input type='text' :value='value' /
/div
/template
template
input type='text' :value='value' /
/template
/template
script
export default {
props: {
value: {
type: ''
}
},
computed: {
}
}
/script
优点:
复用性好,适合做组件开发。 缺点:
方式⑥.$parent / $children
通过
$parent
/
$children
可以拿到父子组件的实例,从而调用实例里的方法,实现父子组件通信。并不推荐这种做法。可以使用
this.$parent
查找当前组件的父组件,使用
this.$children
查找当前组件的直接子组件,可以遍历全部子组件, 需要注意
$children
并不保证顺序,也不是响应式的,也可以使用
this.$root
查找根组件,并可以配合
$children
遍历全部组件,也可以使用
this.$refs
查找命名子组件。
使用方面通过
this.$parent
或者
this.$children
拿到父或子组件实例。
// $parent / $children 组件传值
// 父组件 Game.vue
template
div class="game"
h2{{ msg }}/h2
LOL ref="lol"/LOL
DNF ref="dnf"/DNF
/div
/template
script
import LOL from '@/components/game/LOL'
import DNF from '@/components/game/DNF'
export default {
name: 'game',
components: {
LOL,
DNF
},
data () {
return {
msg: 'Game',
lolMsg:'Game-LOL',
dnfMsg:'Game-DNF',
}
},
methods: {
},
mounted(){ //注意 mounted
//读取子组件数据,注意$children子组件的排序是不安全的
console.log(this.$children[0].gameMsg);//LOL-Game
//读取命名子组件数据
console.log(this.$refs.dnf.gameMsg);//DNF-Game
//从根组件查找组件数据
console.log(this.$root.$children[0].msg); //APP
console.log(this.$root.$children[0].$children[0].msg); //Game
console.log(this.$root.$children[0].$children[0].$children[0].msg); //Game-LOL
console.log(this.$root.$children[0].$children[0].$children[1].msg); //Game-DNF
}
}
/script
style lang="css"
.game{
border: 1px solid #00FF00;
width: 200px;
}
/style
// 子组件 LOL.vue
template
div class="lol"
h2{{ msg }}/h2
/div
/template
script
export default {
name: 'LOL',
data () {
return {
msg: 'LOL',
gameMsg:'LOL-Game',
}
},
methods:{
},
created(){
//读取父组件数据
this.msg = this.$parent.lolMsg;
}
}
/script
// DNF.vue
template
div class="dnf"
h2{{ msg }}/h2
/div
/template
script
import Bus from '../../utils/bus.js'
export default {
name: 'DNF',
data () {
return {
msg: 'DNF',
gameMsg:'DNF-Game',
}
},
methods:{
},
created(){
//从根组件向下查找父组件数据
this.msg = this.$root.$children[0].$children[0].dnfMsg;
}
}
/script
优点:
缺点:
方式⑦. .sync 修饰符
它在 vue@1.x 的时候曾作为双向绑定功能存在,即子组件可以修改父组件中的值。因为它违反了单向数据流的设计理念,所以在 vue@2.0 的时候被干掉了。但是在 vue@2.3.0+ 以上版本又重新引入了这个 .sync 修饰符。但是这次它只是作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的 v-on 监听器。说白了就是让我们手动进行更新父组件中的值了,从而使数据改动来源更加的明显。下面引入自官方的一段话:
在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源。
既然作为一个语法糖,肯定是某种写法的简写形式,直接看demo:
text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
/text-document
于是我们可以用 .sync 语法糖简写成如下形式:
text-document v-bind:title.sync="doc.title"/text-document
假如我们想实现这样一个效果:改变子组件文本框中的值同时改变父组件中的值。怎么做?列位不妨先想想。先看段代码:
//
div class="input-group"
label姓名:/label
input v-model="text"
/div
let Login = Vue.extend({
props: ['name'],
data () {
return {
text: ''
}
},
watch: {
text (newVal) {
this.$emit('update:name', newVal)
}
}
})
new Vue({
el: '#app',
data: {
userName: ''
},
components: {
Login
}
})
注意代码里有这一句话:
this.$emit('update:name', newVal)
官方语法是:update:myPropName 其中 myPropName 表示要更新的 prop 值。当然如果你不用 .sync 语法糖使用上面的
.$emit
也能达到同样的效果。仅此而已。
方式⑧.$attrs 和 $listeners
官网对
$attrs
的解释如下:包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过
v-bind="$attrs"
传入内部组件——在创建高级别的组件时非常有用。
官网对
$listeners
的解释如下:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过
v-on="$listeners"
传入内部组件——在创建更高层次的组件时非常有用。
我觉得
$attrs
和
$listeners
属性像两个收纳箱,一个负责收纳属性,一个负责收纳事件,都是以对象的形式来保存数据。看下面的代码解释:
div id="app"
child
:foo="foo"
:bar="bar"
@one.native="triggerOne"
@two="triggerTwo"
/child
/div
从 Html 中可以看到,这里有俩属性和俩方法,区别是属性一个是 prop 声明,事件一个是 .native 修饰器。
let Child = Vue.extend({
template: 'h2{{ foo }}/h2',
props: ['foo'],
created () {
console.log(this.$attrs, this.$listeners)
// - {bar: "parent bar"}
// - {two: fn}
// 这里我们访问父组件中的 `triggerTwo` 方法
this.$listeners.two()
// - 'two'
}
})
new Vue({
el: '#app',
data: {
foo: 'parent foo',
bar: 'parent bar'
},
components: {
Child
},
methods: {
triggerOne () {
alert('one')
},
triggerTwo () {
alert('two')
}
}
})
我们可以通过
$attrs
和
$listeners
进行数据传递,在需要的地方进行调用和处理,还是很方便的。当然,我们还可以通过
v-on="$listeners"
一级级的往下传递,
方式⑨.broadcast / dispatch
broadcast 也叫事件广播,dispatch 是查找所有父级,直到找到要找到的父组件,并在身上触发指定的事件。它们俩是 vue@1.0 中的方法,在 vue@2.0 里面已经删掉了。最近在使用 Element 过程中发现组件通信大量使用 dispatch 和 broadcast 两个方法,之前在 vue2 组件通信 也提到过 vue2 中取消了
$dispatch
和
$broadcast
两个重要的事件,而 Element 重新实现了这两个函数。
// broadcast / dispatch 组件传值
// element-ui/lib/mixins/emitter 里的 emitter.js
"use strict";
exports.__esModule = true;
function _broadcast(componentName, eventName, params) {
this.$children.forEach(function (child) {
var name = child.$options.componentName;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
_broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
exports.default = {
methods: {
dispatch: function dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
broadcast: function broadcast(componentName, eventName, params) {
_broadcast.call(this, componentName, eventName, params);
}
}
};
解析:
dispatch 方法会寻找所有的父组件,直到找到名称为 componentName 的组件,调用其
$emit()
事件。broadcast 方法则是遍历当前组件的所有子组件,找到名称为 componentName 的子组件,然后调用其
$emit()
事件。
使用方式
// 在 App.vue 中监听 message 事件,收到事件后,
// 通过 broadcast 和接收到的参数,将事件定向传播给相关组件。
template
div id="app"
hello/hello
fuck/fuck
/div
/template
script
import Hello from 'components/Hello'
import Fuck from 'components/Fuck'
import Emitter from 'element-ui/lib/mixins/emitter'
export default {
name: 'app',
componentName: 'ROOT',
mixins: [Emitter],
components: {
Hello,
Fuck
},
created () {
this.$on('message', params = {
this.broadcast(params.componentName, params.eventName, params.text)
})
}
}
/script
// Fuck.vue
import Emitter from 'element-ui/lib/mixins/emitter'
import event from 'mixins/event'
export default {
componentName: 'Fuck',
mixins: [Emitter, event],
data () {
return {
name: 'Fuck',
textarea: '',
tableData: []
}
},
methods: {
submit () {
this.communicate('message', {
componentName: 'Hello',
text: this.textarea
})
this.textarea = ''
}
},
created () {
this.$on('message', text = {
this.tableData.push(this.getMessage(text))
})
}
}
// mixins/event.js
import Emitter from 'element-ui/lib/mixins/emitter'
export default {
mixins: [Emitter],
methods: {
communicate (event, params = {}) {
this.dispatch('ROOT', event, Object.assign({
eventName: event
}, params))
}
}
}
其中 Fuck.vue 中监听了 message 事件,当收到消息时,向 tableData 中加入新的值。而 summit 方法则调用 event.js 中的 communicate 方法,通过 dispatch 方法将事件传播给 ROOT 组件。
总结
vue3 之前的组件通信方式总结:
参考
1.小鱼-Vue.js 父子组件之间通信的十种方式 - 链接 http://www.yyyweb.com/5189.html
最后
今天的 Vue2总结(三)组件通信 就分享到这里,我的公众号没有留言功能哈,有问题大家心里默念,我能感受到,谢谢 ~
原文始发于微信公众号(程序员思语):