能工摹形,巧匠窃意。必三省吾身,万不可怠惰因循。
foreword
- 这篇容纳了我个人所知道的一些Vue 2.x组件通信的总结,之后3.x官网公布后会增加3.x的部分。(篇幅长,细节有那么一些些,熟知部分可以一眼略过).
start
一. props $emit $attrs $listeners $props
- 之所以把$attrs/$listeners/$props 和props $emit 放在一起 是因为个人感觉,更加方便记忆。老项目使用$attrs $listeners $props这几个API需要看当时的vue版本是不是已经支持;
1.props 父组件向子组件传值
_ parent.vue
<template>
<div>
PARENT
<children :stars="stars"></children>
</div>
</template>
<script>
import children from './children/children';
export default {
components:{children},
data(){
return {
stars:[
{name:"周杰伦",id:1},
{name:"刘亦菲",id:2},
{name:"胡歌",id:3},
{name:"古天乐",id:4},
]
}
},
}
</script>
_ children.vue
<template>
<div>
CHILDREN
<ul>
<li v-for="star in stars" :key="star.id">{{star.name}}</li>
</ul>
</div>
</template>
<script>
export default {
name:"children",
props:{
stars:{
type:Array,
default(){
return []
},
// required:true // 是否必须属性
// type:Symbol, // 传入类型 type String Number Boolean Function Object Array Symbol
// type:CustormFn,// 可以是自定义构造函数,用instanceof 检测
validator(V){ // 自定义验证函数
return V.length > 2
}
}
},
created(){
console.log(this.stars) //[{…}, {…}, {…}, {…}, __ob__: Observer]
}
}
</script>
summarize: 父组件通过props传入到子组件. 子组件可以设定传入值的校验,等属性.组件中的数据方式共有 data,computed,props以及provide和inject(这个待商榷).
2. 子组件通过事件的形式向父组件传值
_ parent
<template>
<div>
<p>{{bestHandsome}}</p>
<children @handleBs='handleBs'></children>
</div>
</template>
<script>
import children from './children';
export default {
name:'parent2',
components:{children},
data(){
return {
bestHandsome:'刘德华'
}
},
methods:{
handleBs(name){
this.bestHandsome = name;
}
}
}
</script>
_ children
<template>
<button @click="setBestHandsome('吴彦祖')">BUTTON</button>
</template>
<script>
export default {
name:'children2',
methods:{
setBestHandsome(name){
this.$emit('handleBs',name);
}
}
}
</script>
summarize:子组件通过events的形式改变父组件的值,实际上是调用传入参数父组件的方法,来改变父组件的值. 有部分程序员喜欢将 .sync 和v-model这两个语法糖也归为组件通信方式,这里不做归纳.详细请看官方文档.sync,v-model.
3. $attrs/$listeners/$props
官方解释
- $props:当前组件接收到的 props 对象。Vue 实例代理了对其 props 对象属性的访问。类型(Object)
- $attrs:包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。类型:{ [key: string]: string }(只读)
- $listeners: 包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。类型: { [key: string]: Function | Array<Function> }(只读)
$props
_ code
// parent.vue
<template>
<div>
<children
name='input'
type='nmber'
disabled
autofocus
placeholder='这是一个输入框'
></children>
</div>
</template>
// children.vue
<template>
<div>
<input v-bind="$props">
</div>
</template>
<script>
export default {
name:"children",
props:['name','type','disabled','autofocus','placeholder'],
mounted(){
console.log(this.$props.name)// input
}
}
</script>
_ view
![](https://img.haomeiwen.com/i7280129/abdfbc905b17dcff.png)
![](https://img.haomeiwen.com/i7280129/04ef5e3d162db62b.png)
- 注意这里使用v-bind="$props"就会使得子组件中的input标签绑定上父组件中定义的props属性.
$attrs
_ code
// parent.vue
<template>
<div>
<children
name='input'
type='nmber'
disabled
autofocus
placeholder='这是一个输入框'
></children>
</div>
</template>
// children.vue
<template>
<div>
<input v-bind="$attrs">
</div>
</template>
<script>
export default {
inheritAttrs:false, // 将默认绑定根元素属性去掉
name:"children",
props:['handsome'],
mounted(){
console.log(this.$attrs.name)// input
console.log(this.$attrs.handsome)// undefined
console.log(this.$props.handsome)// 1
}
}
</script>
- inheritAttrs
默认情况下父作用域的不被认作 props 的特性绑定 (attribute bindings) 将会“回退”且作为普通的 HTML 特性应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置 inheritAttrs 到 false,这些默认行为将会被去掉。而通过 (同样是 2.4 新增的) 实例属性 $attrs 可以让这些特性生效,且可以通过 v-bind 显性的绑定到非根元素上。
![](https://img.haomeiwen.com/i7280129/73edfb0ce18ce7ae.png)
![](https://img.haomeiwen.com/i7280129/09e15c1c69940ab5.png)
$listeners
_ code
// parent.vue
<template>
<div>
<p>{{ handsome }}</p>
<children
@changeHandsome="changeHandsome"
@clearHandsome="clearHandsome"
@resetHandsome="resetHandsome"
></children>
</div>
</template>
<script>
import children from './children/children';
export default {
components:{children},
data(){
return {
handsome:'lin'
}
},
methods:{
changeHandsome(name){
this.handsome = name;
},
clearHandsome(){
this.handsome = '';
},
resetHandsome(){
this.handsome = 'lin';
},
}
}
</script>
// children.vue
<template>
<div>
<g-children v-on="$listeners"></g-children>
</div>
</template>
<script>
import gChildren from './grandchildren'
export default {
name:"children",
components:{gChildren},
mounted(){
console.log(this.$listeners)
}
}
</script>
// grandchildren.vue
<template>
<div>
<button @click="$emit('changeHandsome','zhou')">set Zhou</button>
<button @click="$emit('clearHandsome')">clear</button>
<button @click="$emit('resetHandsome')">reset</button>
</div>
</template>
以上这些实际上是父子组件直接直接或者间接通过vue提供的通信方式通信.
二. $refs $parent $children $root
- 官方解释
$refs:一个对象,持有注册过 [
ref
特性] 的所有 DOM 元素和组件实例。
$parent:父实例,如果当前实例有的话。(类型:Vue instance)
$children:当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的。如果你发现自己正在尝试使用 $children 来进行数据绑定,考虑使用一个数组配合 v-for 来生成子组件,并且使用 Array 作为真正的来源。(类型:Array)
$root:当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。
$refs
_ code
//children.vue
<script>
export default {
name: "children",
data() {
return {
name: "xiaoerlang",
age: 18
};
}
};
</script>
// parent.vue
<template>
<div>
<children ref="children"></children>
<button @click="setChildrenData">button</button>
</div>
</template>
<script>
import children from "./children/children";
export default {
components: { children },
methods: {
setChildrenData() {
console.log(this.$refs.children.name); //第一次点击按钮的时候打印 xiaolang
this.$refs.children.name = "xiaoming";
console.log(this.$refs.children.name); //第一次点击按钮的时候打印 xiaoming
}
}
};
</script>
$parent
// parent.vue
<template>
<div>
{{name}}
<children></children>
</div>
</template>
<script>
import children from "./children/children";
export default {
components: { children },
data(){
return {
name:'liu'
}
},
};
</script>
// children.vue
<template>
<div>
<button @click="setParentName('fei')">button</button>
</div>
</template>
<script>
export default {
name: "children",
methods:{
setParentName(name){
this.$parent.name = name;
}
}
};
</script>
$children
// parent.vue
<template>
<div>
<children></children>
<button @click="setChildrenName('yi')">button</button>
</div>
</template>
<script>
import children from "./children/children";
export default {
components: { children },
methods:{
setChildrenName(name){
this.$children[0].name = name;
}
}
};
</script>
// children.vue
<template>
<div>
{{name}}
</div>
</template>
<script>
export default {
name: "children",
data() {
return {
name: "xiaoerlang",
};
},
};
</script>
$root:这里与$parent类似,是当前组件树的根实例.
附加:使用$parent或者$root配合$on和$emit可以 进行兄弟组件之间通信
// parent.vue
<template>
<div>
<bother1></bother1>
<bother2></bother2>
</div>
</template>
<script>
import bother1 from './children/brother1';
import bother2 from './children/brother2';
export default {
components: { bother1,bother2 },
};
</script>
// bother2.vue
<template>
<div>{{name}}</div>
</template>
<script>
export default {
name:'brother2',
data(){
return{
name:'zhouxiaolun'
}
},
created(){
this.$parent.$on('setB2',this.setName)
},
methods:{
setName(name){
this.name = name;
}
}
}
</script>
// bother1.vue
<template>
<button @click="setB2Name('zhoujielun')">button</button>
</template>
<script>
export default {
name:'brother1',
methods:{
setB2Name(name){
this.$parent.$emit('setB2',name)
}
}
}
</script>
summarize
- 注意这里$children 格式为数组,如果没有就是空数组,但是这里的数组顺序与页面顺序是不对应的,这里涉及到了虚拟dom挂载.
- 上面的部分情况其实是拿到对应的组件的实例,相当于在对应vue组件中调用this.xx = 'xxxx';
- 实际开发中,非自定义组件,或者真实需要,不建议使用$parent和$children $root进行组件之间的通信.
三. provide/inject
- provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。provide/inject能够实现祖先和后代之间传值.
// 祖先组件
export default {
provide() {
const that = this;
return {
foo: "foo",
forefathersThis: that
};
},
name: "parent",
components: { children }
};
// 后代组件
export default {
name: "children",
inject: ["foo", "forefathersThis"],
created() {
console.log(this.foo);
console.log(this.forefathersThis); // 祖先组件的实例
}
};
- 提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。这里也可以传入this到后代组件中,但实际开发中不推荐使用,可以用于开发高阶组件或者组件库.
四.事件总线eventBus方式(自定义Bus类,或者使用Vue代替);
// Bus 类
class Bus {
constructor() {
this.CB = {};
}
// 监听
$on(name, fn) {
this.CB[name] = this.CB[name] || [];
this.CB[name].push(fn)
}
// 派发
$emit(name, args) {
this.CB[name] && this.CB[name].forEach(cb => cb(args))
}
}
export default Bus;
// main.js
import Bus from './eventBus';
Vue.prototype.$bus = new Bus();
// 组件1
methods: {
setBH2Name() {
this.$bus.$emit("setB2", "zhoujielun");
}
}
// 组件2
created() {
this.$bus.$on("setB2", this.setName);
},
methods: {
setName(name) {
this.name = name;
}
}
summarize:
- 如果不使用自定义方式,也可以Vue.prototype.$bus = new Vue(); vue内部已经做了具体处理.并且提供$once只监听一次这个事件,$off(name)移除name事件监听,$off() 移除所有事件监听.
- 这里主要说Vue通信方式,所以关于上部分需要在destroy生命周期需要注销监听等操作都未列出,实际开发实际需求.
五.Vuex
- Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,实际上是把一些需要多处用到的状态放在同一个对象中.
- 小demo
// store
state: {
infoName: "handsome"
},
mutations: {
setInfoName(state, payload) {
state.infoName = payload;
}
}
// 组件1
<script>
import { mapMutations } from "vuex";
import bother2 from "./children/brother2";
export default {
name: "parent",
components: { bother2 },
methods: {
...mapMutations(["setInfoName"]),
setInfo() {
const name = "ugly";
this.setInfoName(name);
}
}
};
</script>
// 组件2
<script>
import { mapState } from "vuex";
export default {
name: "brother2",
computed: {
...mapState({
infoName: s => s.infoName
})
}
};
</script>
summarize: Vuex相对来说比redux简单一些,详细可以参考中文官网Vuex中文官网
六. 自定义broadcast/dispatch
- vue 1.x 版本中有两个API $dipatch,$broadcast,$broadcast和$dispatch 这两个API在2.x版本中去除. 实际上我们经常写一些自定义组件库,或者高阶组件的时候可能会用到.
vue 1.x解释
$dispatch:向上级派发事件,祖辈组件中$on监听到
$broadcast:与$dispatch相反,向下级广播事件.
- 自定义代码实现功能.
/**
* @param {*} componentName // 组件名
* @param {*} eName // 自定义事件名称
* @param {*} params // 传递参数数据
*/
export function broadcast(componentName, eName, params) {
this.$children.forEach(child => {
const name = child.$options.name;
if (name === componentName) {
// 调用子组件emit
child.$emit.bind(child)(eName, params)
} else {
// 递归调用
broadcast.bind(child)(componentName, eName, params)
}
})
};
/**
* @param {*} componentName // 组件名
* @param {*} eName // 自定义事件名称
* @param {*} params // 传递参数数据
*/
export function dispatch(componentName, eName, 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.bind(parent)(eName, params)
}
解析
- this.$options.xx 可以取到vue组件中export default暴露的对象的对应xx属性值.我们一帮用来取一些静态属性.例如 组件的name值,判断是哪个组件.
- 我们找到对应的子组件或者父组件,然后用$emit调用,实际上就相当于我们在对应的组件A中用this.$emit(xxx)调用其在当前组件A中created生命周期中$on监听的事件.
- 实际的逻辑就是找到对应组件实例, 组件实例$emit 自己本身$on监听的事件.
- 引入 main.js
import { broadcast, dispatch } from './dispatch-broadcast';
Vue.prototype.$dispatch = dispatch;
Vue.prototype.$broadcast = broadcast;
- 实例引用.
- $dispatch 派发
// 后代
<template>
<div><button @click="setParentDay('Sat')">button</button></div>
</template>
<script>
export default {
name: "children",
methods: {
setParentDay(day) {
this.$dispatch("parent", "setDay", day);
}
}
};
</script>
// 祖先
<div>
<children></children>
<p>{{ day }}</p>
</div>
</template>
<script>
import children from "./children/children";
export default {
name: "parent",
components: { children },
data() {
return { day: "Fir" };
},
created() {
this.$on("setDay", this.setDay);
},
methods: {
setDay(day) {
this.day = day;
}
}
};
</script>
- $broadcast 广播
// 祖先
<template>
<div>
<children></children>
<button @click="setChildrenDay('Fir')">button</button>
</div>
</template>
<script>
import children from "./children/children";
export default {
name: "parent",
components: { children },
methods: {
setChildrenDay(day) {
this.$broadcast("children", "setDay", day);
}
},
};
</script>
// --------- 后代 --------------
<template>
<div>{{ day }}</div>
</template>
<script>
export default {
name: "children",
data() {
return {
day: "Sat"
};
},
created() {
this.$on("setDay", this.setDay);
},
methods: {
setDay(day) {
this.day = day;
}
}
};
</script>
七. 自定义findComponents多个方法
- 就像上面说的,其实我们寻找到了对应组件的实例,就可以用这个实例进行操作,就可以说进行了组件的通信.那么这里就存在几个问题. (注意这里的前提是组件中name的属性设置严格按照规范),这些方法一般在我们自定义组件库,或者定义一些高阶组件用来使用.
提出问题.
- 如何由一个组件向上找到第一个最近的指定组件?
- 如何由一个组件向上找到所有的指定组件?
- 如何由一个组件向下找到最近的指定组件?
- 如何由一个组件向下找到所有的指定组件?
- 如何由一个组件找到指定的兄弟组件?
分析:
- 利用$options.name $children $parent , 参数包含当前组件的this,要找到的组件名name. 通过$options.name确定寻找的组件.
1. 由一个组件向上找到第一个最近的指定组件.
/**
* @param {*} context 执行上下文,这里一般传 this
* @param {*} componentName 要找到的组件名 name
* @returns
*/
function findComponentUpwrad(context, componentName) {
let parent = context.$parent;
let { name } = parent.$options;
while (parent && (!name || [componentName].indexOf(name) < 0)) {
parent = parent.$parent;
if (parent) name = parent.$options.name;
}
return parent;
}
2. 由一个组件向上找到所有的指定组件
/**
* @param {*} context 执行上下文,这里一般传 this
* @param {*} componentName 要找到的组件名 name
*/
function findComponentsUpward(context, componentName) {
const parents = [];
const parent = context.$parent;
if (parent) {
if (parent.$options.name === componentName) parents.push(parent);
return parents.concat(findComponentUpwrad(parent, componentName));
}
return [];
}
3. 由一个组件向下找到最近的指定组件
/**
*@description 向下找到最近的指定组件
*
* @context {*} context 执行上下文,这里一般传 this
* @componentName {*} componentName 要找到的组件名 name
*/
function findComponentDownward(context, componentName) {
const childrens = context.$children;
let children = null;
if (childrens.length) {
for (const child of childrens) {
const { name } = child.$options;
if (name === componentName) {
children = child;
break;
} else {
children = findComponentDownward(child, componentName);
if (children) break;
}
}
}
return children;
}
4. 由一个组件向下找到所有的指定组件
/**
* @context {*} context 执行上下文,这里一般传 this
* @componentName {*} componentName 要找到的组件名 name
*/
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);
}, []);
}
5. 由一个组件找到指定的兄弟组件
/**
* @context {*} context 执行上下文,这里一般传 this
* @componentName {*} componentName 要找到的组件名 name
* @exceptMe {Boolean} 是否包含本身
* @description2 Vue.js 在渲染组件时,都会给每个组件加一个内置的属性 _uid,这个 * * *_uid 是不会重复的,
*/
function findBrothersComponents(context, componentName, exceptMe) {
const res = context.$parent.$children.filter(item => item.$options.name === componentName);
const index = res.findIndex(item => item._uid === context._uid);
if (exceptMe) res.splice(index, 1);
return res;
}
找到组件后就等于找到组件中的this,之后通信的方式就可以很随意了,当然这里使用方式一般是存在特殊情况下,正常我们组件之间的通信使用Vuex 或者 props $emit 就可以了.
代码参考 iview源码 具体位置在 iview assets.js,有兴趣的朋友可以查看源码.
总结
- Vue组件之间的通信,当然可能还有更多,这里容纳了大部分,当然可能还有其他一些.
好学而不勤问非真好学者. 如果有帮助请点上一个赞,如果由疑问,请评论留言.
网友评论