组件(Component)是 Vue 最核心的功能,也是整个框架设计最出彩的地方,而组件实例的作用域是相互独立的,也就是说不同组件间的数据是无法直接互相引用的。那么,组件之间是如何进行通信传递数据的呢?这就需要我们先搞清楚组件之间的关系:
<!--parent.vue-->
<template>
<div>
<!--props的值可以直接传递也可以动态绑定-->
<child message1='我是父组件直接传递的数据' :message2='message'></child>
</div>
</template>
<script>
import child from './child'
export default {
name: 'parent',
components: {
child
},
data() {
return {
message: ['我是父组件动态绑定传递的数据1', '我是父组件动态绑定传递的数据2', '我是父组件动态绑定传递的数据3']
}
}
}
</script>
<!--child.vue-->
<template>
<div>
<p>{{message1}}</p>
<ul>
<li v-for='(item,index) in message2' :key=index>{{item}}</li>
</ul>
</div>
</template>
<script>
export default {
name: 'child',
//props: ['message1', 'message2'], //props的第一种:字符串数组
props: {//props的第二种:对象
message1: {
type: String,
default: ''
},
message2: {
type: Array,
default: () => []
}
},
data() {
return {
}
}
}
</script>
<!--parent.vue-->
<template>
<div>
<p>{{parentData}}</p>
<!--在子组件自定义标签上用v-on(此处用的语法糖@)监听子组件触发的自定义事件clickEvent-->
<child @clickEvent='parentClickEvent'></child>
</div>
</template>
<script>
import child from './child'
export default {
name: 'parent',
components: {
child
},
data() {
return {
parentData: '父组件本来的数据'
}
},
methods: {
parentClickEvent(val) {
this.parentData = val; //val是子组件传递过来的数据
}
}
}
</script>
<!--child.vue-->
<template>
<button @click='childClickEvent'>点击修改父组件数据</button>
</template>
<script>
export default {
name: 'child',
data() {
return {
}
},
methods: {
childClickEvent() {
//$emit方法第一个参数是自定义事件(clickEvent),后面的参数都是要传递的数据,可以不填或填写多个
this.$emit('clickEvent', '子组件向父组件传递的数据');
}
}
}
</script>
v-model
指令,实现父子组件之间的通信,该方式与上面介绍的方式类似,是一个语法糖。父组件通过 v-model
向子组件传递数据时,会自动传递一个 value
的 prop
属性,而子组件通过 this.$emit('input',val)
自动修改 v-model
绑定的值。<!--parent.vue-->
<template>
<div>
<h2>我是父组件</h2>
<p>我是父组件的数据:{{pData}}</p>
<child v-model='pData'></child>
</div>
</template>
<script>
import child from './child'
export default {
name: 'parent',
components: {
child
},
data() {
return {
pData: '我是父组件的数据'
}
}
}
</script>
<!--child.vue-->
<template>
<div>
<h2>我是子组件</h2>
<p>我是子组件的数据: {{cData}}</p>
<p>我是父组件传递过来的数据: {{msg}}</p>
<button @click='handleClick'>点击传递子组件数据给父组件</button>
</div>
</template>
<script>
export default {
name: 'child',
props: ['value'],//v-model 会自动传递一个字段为 value 的 props 属性
data() {
return {
msg: this.value,
cData: '我是子组件的数据'
}
},
methods: {
handleClick() {
this.$emit('input', this.cData);//通过emit特定的input事件可以改变父组件上v-model绑定的值
}
}
}
</script>
prop
的双向绑定,而真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父子组件中都没有明显的改动来源,所以官方推荐以 update:my-prop-name
的模式触发事件实现 “上行绑定” 最终实现 “双向绑定”。而 .sync
是一个编译时的语法糖,它会被自动扩展为一个自动更新父组件属性的 v-on
监听器。如:<child :abc.sync=”msg”></child>
就会被扩展为:<child :abc=”data” @update:abc=”val => data= val”>
(@
是 v-on
的简写)。当子组件需要更新 abc
的值的时候,他需要显示的触发一个更新事件:this.$emit( “update:abc”, newValue )
。当使用一个对象一次性设置多个属性的时候,这个 .sync
修饰符也可以和 v-bind
一起使用。如:<child v-bind.sync = “{ a: data1, b: data2}”></child>
(不能写成 :.sync=...
,否则会报错),这样会为 a
和 b
同时添加用于更新的 v-on
监听器。<!--parent.vue-->
<template>
<div>
<h2>我是父组件</h2>
<p>我是父组件的数据(单属性):{{pData}}</p>
<p>我是父组件的数据(多属性):{{myProps.a1}},{{myProps.a2}}</p>
<!--1.单个属性传递-->
<child :abc.sync='pData' :ifShow='true'></child>
<!-- <child :abc='pData' @update:abc='val=>pData= val'></child> 上面会自动扩展为该形式-->
<!--2.多个属性传递-->
<child v-bind.sync='myProps' :ifShow='false'></child> <!-- 不能写成字面量形式如 v-bind.sync='{ a1: '我是父组件的pData1', a2: '我是父组件的pData2'}'-->
</div>
</template>
<script>
import child from './child'
export default {
name: 'parent',
components: {
child
},
data() {
return {
pData: 'Hi!我是父组件!',
myProps: { a1: '我是父组件的pData1', a2: '我是父组件的pData2' }
}
}
}
</script>
<!--child.vue-->
<template>
<div>
<h2>我是子组件</h2>
<p v-if='ifShow'>我是子组件接收到的父组件单个属性:{{abc}}</p>
<p v-else>我是子组件接收到的父组件的多个属性:{{a1}},{{a2}}</p>
<button @click='handleClick'>点击传递子组件数据给父组件</button>
</div>
</template>
<script>
export default {
name: 'child',
props: ['ifShow', 'abc', 'a1', 'a2'],
data() {
return {
cData: 'Hi!我是子组件!'
}
},
methods: {
handleClick() {
this.$emit('update:abc', this.cData);
this.$emit('update:a1', this.cData);
this.$emit('update:a2', this.cData);
}
}
}
</script>
.sync
。注意:将 v-bind.sync 用在一个字面量的对象上,例如v-bind.sync=”{ title: doc.title}”,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。
vue.js 官网
v-model
和 .sync
都可以实现 props
的双向绑定,但是 v-model
有局限性,只能传递 value
属性,而 .sync
可以传递其他的属性值。$parent
可以直接访问父组件的实例(对象),而父组件通过 $children
可以访问所有的子组件实例(数组),并且可以递归向上或向下无限访问,直到根实例或最内层组件。虽然 Vue 允许这样操作,但在实际处理中,不建议这么做,因为这样会使父子组件紧耦合,而且会使父组件的状态因为可能被任意组件修改而难以理解。<!--parent.vue-->
<template>
<div>
<child></child>
<button @click='parentClickEvent'>点击修改子组件数据</button>
</div>
</template>
<script>
import child from './child'
export default {
name: 'parent',
components: {
child
},
data() {
return {
parentData: '父组件的数据'
}
},
methods: {
parentClickEvent() {
this.$children[0].childData = '这是父组件修改的子组件数据';
}
}
}
</script>
<!--child.vue-->
<template>
<div>
<p>子组件数据:{{childData}}</p>
<p>子组件获取的父组件数据:{{parentData}}</p>
</div>
</template>
<script>
export default {
name: 'child',
data() {
return {
childData: '子组件的数据'
}
},
computed: {
parentData() {
return this.$parent.parentData;
}
}
}
</script>
2.2 $dispatch / $broadcast
$parent
和遍历 $children
,使用 $on
和 $emit
进行事件的监听和调用。通过 $dispatch
和 $broadcast
定向的向某个父或者子组件远程调用事件,这样就避免了通过传 props
或者使用 refs
调用组件实例方法的操作。//main.js
import Vue from 'vue'
import parent from './parent'
//在Vue的原型上添加$dispatch方法,通过this.$dispatch调用
Vue.prototype.$dispatch = function (eventName, params) {
let parent = this.$parent;
while (parent) {
parent.$emit(eventName, params);
parent = parent.$parent;
}
};
//在Vue的原型上添加$broadcast方法,通过this.$broadcast调用
Vue.prototype.$broadcast = function (eventName, params) {
//获取当前组件下所有的子孙组件,递归调用
const boradcast = children => {
children.forEach(child => {
child.$emit(eventName, params);
if (child.$children) {
boradcast(child.$children);
}
});
}
boradcast(this.$children);
};
new Vue({
render: h => h(parent)
}).$mount('#app')
<!--parent.vue-->
<template>
<div>
<h2>我是parent.vue</h2>
<p>parent组件:{{pData}}</p>
<button @click='clickEvent'>所有子孙组件(-100)</button>
<child></child>
</div>
</template>
<script>
import child from './child'
export default {
name: 'parent',
components: {
child
},
data() {
return {
pData: 100
}
},
methods: {
test(val) {
this.pData = val;
},
clickEvent() {
this.$broadcast('broadcastEvent', -100);//向所有子孙广播
}
},
mounted() {
this.$on('dispatchEvent', this.test);//用$on监听
}
}
</script>
<!--child.vue-->
<template>
<div>
<h2>我是child.vue</h2>
<p>child组件:{{cData}}</p>
<grandson></grandson>
</div>
</template>
<script>
import grandson from './grandson'
export default {
name: 'child',
components: {
grandson
},
data() {
return {
cData: 200
}
},
methods: {
test(val) {
this.cData = val;
}
},
mounted() {
this.$on('dispatchEvent', this.test);//用$on监听dispatchEvent
this.$on('broadcastEvent', this.test);//用$on监听broadcastEvent
},
}
</script>
<!--grandson.vue-->
<template>
<div>
<h2>我是grandson.vue</h2>
<p>grandson组件:{{gData}}</p>
<button @click='clickEvent'>所有祖先组件( 100)</button>
</div>
</template>
<script>
export default {
name: 'grandson',
data() {
return {
gData: 100
}
},
methods: {
test(val) {
this.gData = val;
},
clickEvent() {
this.$dispatch('dispatchEvent', 100);//向所有的祖先派发
}
},
mounted() {
this.$on('broadcastEvent', this.test);//用$on监听broadcastEvent
}
}
</script>
props
和$emit
向下和向上进行数据传递,如果中间有更多的层级,这种方式就更加复杂难以维护。为此,Vue2.4 版本提供了 $attrs
和 $listeners
来解决这种跨级通信的需求:$attrs
:包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind='$attrs' 传入内部组件。通常配合 inheritAttrs
选项一起使用。$listeners
:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on='$listeners' 传入内部组件。<!--parent.vue-->
<template>
<div>
<child :a1='a1' :a2='a2' :a3='a3' :a4='a4' @click.native='clickEventNative' @click='clickEvent'
@pEvent1='parentEvent1' @pEvent2='parentEvent2'>
</child>
</div>
</template>
<script>
import child from './child'
export default {
name: 'parent',
components: {
child
},
data() {
return {
a1: '我是parent属性1的数据',
a2: '我是parent属性2的数据',
a3: '我是parent属性3的数据',
a4: '我是parent属性4的数据',
}
},
methods: {
clickEventNative() {
console.log('我是parent的native事件');
},
clickEvent() {
console.log('我是parent事件0');
},
parentEvent1() {
console.log('我是parent事件1');
},
parentEvent2() {
console.log('我是parent事件2');
}
}
}
</script>
<!--child.vue-->
<template>
<div>
<p>child中接收到的a1: {{a1}}</p>
<p>child中接收到的$attrs: {{$attrs}}</p>
<grandson v-bind='$attrs' v-on='$listeners'></grandson>
</div>
</template>
<script>
import grandson from './grandson'
export default {
name: 'child',
components: {
grandson
},
props: ['a1'],
mounted() {
console.log(this.$listeners);//{click: ƒ, pEvent1: ƒ, pEvent2: ƒ}
this.$emit('pEvent1');//parent方法调用方式1
},
}
</script>
<!--grandson.vue-->
<template>
<div>
<p>grandson中接收到的a2: {{a2}}</p>
<p>grandson中接收到的a3: {{a3}}</p>
<p>grandson中接收到的$attrs: {{$attrs}}</p>
</div>
</template>
<script>
import grandSon from './grandson'
export default {
name: 'child',
inheritAttrs: false, // 可以关闭自动挂载到组件根元素上的没有在props声明的属性
props: ['a2', 'a3'],
mounted() {
this.$listeners.pEvent2();//parent方法调用方式2
},
}
</script>
例子中,给 grandson.vue
加上 inheritAttrs:false
属性前后如图所示:
provider
来提供属性,然后在子组件中通过 inject
来注入变量。不论子组件有多深,只要调用了 inject
那么就可以注入在 provider
中提供的数据,只要在父组件的生命周期内,子组件都可以调用。<!--parent.vue-->
<template>
<div>
<child></child>
</div>
</template>
<script>
import child from './child'
export default {
name: 'parent',
components: {
child
},
provide: {//provide选项可以是一个对象或返回一个对象的函数
parentData: '我是父组件的数据'
},
}
</script>
<!--child.vue-->
<template>
<div>{{parentData}}</div>
</template>
<script>
export default {
name: 'child',
inject: ['parentData']//injec选项可以是一个字符串数组或一个对象
}
</script>
parent.vue
中的parentData,child.vue
中的 parentData 是不会改变的。要实现数据响应式,有两种方法:provide 祖先组件的实例,在后代组件中注入依赖。这样就可以在后代组件中直接修改祖先组件实例的属性。
使用 Vue. observable
优化响应式 provide(2.6新增API,推荐)
<!--parent.vue-->
<template>
<div>
<p>{{pData}}</p>
<child></child>
<button @click=changeData>改变parentData</button>
</div>
</template>
<script>
import Vue from 'vue'
import child from './child'
export default {
name: 'parent',
components: {
child
},
data() {
return {
pData: '我是父组件数据'
}
},
//初始用法
// provide() {
// return {
// parentData: this.pData,//该方式绑定的数据不是响应式的,即祖先组件中parentData变化,后代组件中不会跟着变
// }
// },
// methods: {
// changeData() {
// this.pData = '我是改变以后的父组件数据';
// }
// }
//方法一
// provide() {
// return {
// parentData: this,//provide祖先组件的实例
// }
// },
// methods: {
// changeData() {
// this.pData = '我是改变以后的父组件数据1';
// }
// }
//方法二
provide() {
this.parentData = Vue.observable({
pData: this.pData
});
return {
parentData: this.parentData
}
},
methods: {
changeData() {
this.parentData.pData = '我是改变以后的父组件数据2';
}
}
}
</script>
<!--child.vue-->
<template>
<div>{{parentData.pData}}</div>
</template>
<script>
export default {
name: 'child',
inject: {
parentData: {
default: () => { }
}
}
}
</script>
provide
和 inject
主要为高阶插件/组件库提供用例,不推荐直接用于应用程序代码,可视情况采用。ref
如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例,可以通过实例直接调用组件的方法或访问数据。<!--parent.vue-->
<template>
<div>
<p>我是父组件获取的子组件的数据:{{pData}}</p>
<child ref='compChild'></child>
</div>
</template>
<script>
import child from './child'
export default {
name: 'parent',
components: {
child
},
data() {
return {
pData: ''
}
},
methods: {
parentEvent() {
let compChild = this.$refs.compChild;//通过this.$refs获取子组件实例
this.pData = compChild.childData;//获取子组件数据
compChild.childEvent();//调用子组件方法
}
},
mounted() {
this.parentEvent();
},
}
</script>
<!--child.vue-->
<template>
<div></div>
</template>
<script>
export default {
name: 'child',
data() {
return {
childData: '我是child的数据'
}
},
methods: {
childEvent() {
console.log('我是child的方法');
}
}
}
</script>
//eventBus.js
import Vue from 'vue'
export const bus = new Vue();
<!--compA.vue-->
<template>
<div>
<comp-b></comp-b>
<comp-c></comp-c>
</div>
</template>
<script>
import compB from './compB'
import compC from './compC'
export default {
name: 'compA',
components: {
compB, compC
}
}
</script>
<!--compB.vue-->
<template>
<div>
<p>compB:{{dataB}}</p>
<button @click='handleEventB'>点击emit组件compB的数据</button>
</div>
</template>
<script>
import { bus } from './eventBus'
export default {
name: 'compB',
data() {
return {
dataB: '我是组件compB中的数据'
}
},
methods: {
handleEventB() {
bus.$emit('on-msg', this.dataB);//发送事件
}
}
}
</script>
<!--compC.vue-->
<template>
<div>
<p>{{dataC}}</p>
</div>
</template>
<script>
import { bus } from './eventBus'
export default {
name: 'compC',
data() {
return {
dataC: '我是组件compC中的数据'
}
},
methods: {
handleEventC() {
//接收事件
bus.$on('on-msg', val => {
this.dataC = val;
})
}
},
mounted() {
this.handleEventC();
},
beforeDestroy() {
bus.$off('on-msg', {})//移除事件监听
},
}
</script>
Vuex
是一个专为 Vue 服务,用于管理页面数据状态、提供统一数据操作的生态系统。它集中于 MVC 模式中的 Model 层,规定所有的数据操作必须通过 action - mutation - state change
的流程来进行,再结合 Vue 的数据视图双向绑定特性来实现页面的展示更新。dispatch
方法触发对应 action
进行回应。action
的方法。Vue Components
接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他 action
以及提交 mutation
的操作。该模块提供了 Promise 的封装,以支持 action 的链式触发。mutation
进行提交,是唯一能执行mutation 的方法。state
的唯一推荐方法,其他修改方式在严格模式下将会报错。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些 hook
暴露出来,以进行 state
的监控等。Vue components
中 data 对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用 Vue 的细粒度数据响应机制来进行高效的状态更新。state
对象读取方法。图中没有单独列出该模块,应该被包含在了render
中,Vue Components
通过该方法读取全局state对象。下面看个实例:
//main.js 入口文件
import Vue from 'vue'
import compA from './compA'
import store from './store'; //使用store
Vue.config.productionTip = false;
new Vue({
store, //关联store
render: h => h(compA)
}).$mount('#app')
//store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
dataB: '',
dataC: ''
}
const mutations = {
setDataB(state, data) {
// 将compA组件的数据存放于state
state.dataB = data
},
setDataC(state, data) {
// 将compB组件的数据存放于state
state.dataC = data
}
}
export default new Vuex.Store({
state,
mutations
})
<!--compA.vue-->
<template>
<div>
<comp-b></comp-b>
<comp-C></comp-c>
</div>
</template>
<script>
import compB from './compB'
import compC from './compC'
export default {
name: 'compA',
components: {
compB, compC
}
}
</script>
<!--compB.vue-->
<template>
<div>
<h2>我是compB组件</h2>
<p>compB组件获取到的数据:{{showDataB}}</p>
<button @click='handleEventB'>点击将compB的数据传给compC</button>
</div>
</template>
<script>
export default {
name: 'compB',
data() {
return {
dataB: '我是compB的数据'
}
},
computed: {
showDataB() {
return this.$store.state.dataB//获取数据dataB
}
},
methods: {
handleEventB() {
this.$store.commit('setDataC', this.dataB);//修改数据dataC
}
}
}
</script>
<!--compC.vue-->
<template>
<div>
<h2>我是compC组件</h2>
<p>compB组件获取到的数据:{{showDataC}}</p>
<button @click='handleEventC'>点击将compC的数据传给compB</button>
</div>
</template>
<script>
export default {
name: 'compC',
data() {
return {
dataC: '我是compC的数据'
}
},
computed: {
showDataC() {
return this.$store.state.dataC//获取数据dataC
}
},
methods: {
handleEventC() {
this.$store.commit('setDataB', this.dataC);//修改数据dataB
}
}
}
</script>
localStorage
来实现,当 Vuex 中数据变化时,将数据存储到 localStorage
中,刷新之后,如果 localStorage
中有数据,取出来替换 store
中的 state
。localStorage
与 sessionStorage
在使用方法上是相同的,区别在于 sessionStorage
在关闭页面后即被清空,而 localStorage
则会一直保存。存储的内容是以 Json 的形式存储的,JSON.parse()
用于将一个 JSON 字符串转换为对象,JSON.stringify()
可以将对象转换为字符串。sessionStorage.setItem('key', JSON.stringify(value));
localStorage.setItem('key', JSON.stringify(value));
let data1 = JSON.parse(sessionStorage.getItem('key'));
let data2 = JSON.parse(localStorage.getItem('key'));
sessionStorage.clear()
localStorage.clear()
localStorage.removeItem(key);
sessionStorage.removeItem(key);
localStorage.key(index);
sessionStorage.key(index);
props / $emit
, $parent / $children
, $attrs / $listeners
, provide / inject
, ref
, EventBus
, Vuex
, localStorage/sessionStorage
EventBus
, Vuex
, localStorage/sessionStorage
$attrs / $listeners
, provide / inject
, EventBus
, Vuex
, localStorage/sessionStorage
本文所示的例子都已上传至 github
(https://github.com/lara0101/lara_vue_components_comm.git),都采用快速原型开发,如果需要可参考以下步骤:
1.使用如下命令安装 vue-cli3
npm install @vue/cli -g
或者
yarn global add @vue/cli
2.使用如下命令安装一个额外的全局插件,这样就可以使用 vue serve 和 vue build 命令独立运行单个 * .vue 文件:
npm install -g @vue/cli-service-global
或者
yarn global add @vue/cli-service-global
3.新建 *.vue 文件
4.在 *.vue 文件所在目录下运行如下命令:
# App.vue
vue serve
# 指定入口文件
vue serve component.vue
联系客服