# Vue中组件通信的九种方法

# 前言

Vue中的组件通信是一个比较复杂的一个知识点,涵盖各种api和语法糖,接下来我们分别来介绍下常见的九种组件通信方法

# props 和 $emit

这是我们日常开发最常见的一种父子组件通信的方法了,我们来看一个简单的例子,后文全部方法都围绕这个例子进行




 






// App.vue
<Parent />

import Parent from './components/Parent'
export default {
    components: {
        Parent,
    }
}



 













// Parent.vue
<Son @change="change" :number="number" />

import Son from './Son'
export default {
    data() {
        return {
            number: 100,
        }
    },
    methods: {
        change(newVale) {
            this.number = newVale
        }
    }
}



 















// Son.vue
<div>子组件
    {{ number }}
    <button @click="change">修改父组件的值</button>
</div>

export default {
    props: {
        number: {
            type: Number
        }
    },
    methods: {
        change() {
            this.$emit('change', 200)
        }
    }
}

我们可以发现,父组件通过props将数据传递给子组件,而子组件可以通过$emit的方式调用父组件的方法修改父组件的值,我们知道Vue和React都是单向数据流,也就是数据是从上往下传递的,当我们子组件修改父组件的数据的时候,自己拿到的数据也会随着修改

# $parent 和 $children

介绍了最常见的一种用法,我们接下来看下一种更暴力的用法 $parent$children




 




// Son.vue
<button @click="change">修改父组件的值</button>
methods: {
    change() {
        this.$parent.change(200)
    }
}

我们可以通过$parent拿到父组件的实例,然后调用父组件的方法改变父组件的数据,同样道理我们在父组件中也可以使用$children的方法修改子组件的值,这里就不演示啦

# $dispatch

这个也是在element ui 的源码中看到的一个方法




 





// main.js
Vue.prototype.$dispatch = function(eventName, newValue) {
    let parent = this.$parent
    while (parent) {
        parent.$emit(eventName, newValue)
        parent = parent.$parent // 继续递归接着往上找
    }
}

我们将$dispatch挂载到Vue的原型上方便调用,我们可以看到,$dispatch的原理是一直递归找父组件,然后执行父组件中对应的$emit方法,我们来看下怎么用




 














// Parent.vue
<div>父组件
    <Son @change="change" :number="number" />
</div>

export default {
    data() {
        return {
            number: 100,
        }
    },
    methods: {
        change(newVale) {
            this.number = newVale
        }
    }
}



 











// Son.vue
<div>子组件
    {{ number }}
    <br/>
    <Grandson />
</div>

export default {
    props: {
        number: {
            type: Number
        }
    },
}



 









// Grandson.vue
<div>孙组件
    <button @click="change">修改父组件的值</button>
</div>

export default {
    methods: {
        change(newVale) {
            this.$dispatch('change', 200)
        }
    }
}

我们可以发现在孙子组件中调用$dispatch方法,他会触发所以父组件中的$emit('xxx', xxx)方法,达到修改对应父组件数据的目的

# $broadcast

$broadcast的原理和$dispatch很像,$dispatch是一直往父元素上面找,而$broadcast刚刚相反,一直往子元素上面找对应的方法执行




 










// main.js
Vue.prototype.$broadcast = function(eventName, newValue) {
    let children = this.$children
    function broad(children) {
        children.forEach(child => {
            child.$emit(eventName, newValue)
            if (child.$children) {
                broad(child.$children)
            }
        })
    }
    broad(children)
}

同样是挂载原型的方法,有了这个方法我们就可以在父组件中主动调用全部后代组件中的$emit方法了

# .sync 和 v-modal

# .sync

接下来我们看到的这种用法其实是 props$emit的一些语法糖, 我们来看下使用props$emit时候的代码




// Parent.vue
<Son @update:number="newValue =>number = newValue" :number="number" />



 




// Son.vue
<button @click="change">更新父组件的方法</button>
methods: {
    change() {
        this.$emit('update:number', 200)
    }
}

我们只需要将$emit(fn)中的fn改为update:xxx就可以了,其中xxx为父组件传递给子组件的数据名

为什么要这样写呢?因为上述代码Vue给我们提供了一个语法糖




 

// Parent.vue
<Son @update:number="newValue =>number = newValue" :number="number" />
// 等价于
<Son :number.sync="number" />

不过需要注意的是,在子组件中调用$emit(fn, val)的时候fn的名字一定是这样的格式的update:xxx

# v-modal

既然 props$emit 可以这么玩了,那我们再试一下另外的玩法




// Parent.vue
 <Son @input="newValue =>number = newValue" :value="number" />



 






// Son.vue
props: {
    value: {
        type: Number
    }
},
change() {
    this.$emit('input', 200)
}

我们只需要稍微的修改一下props$emit的用法也可以实现相应的逻辑,但是这样写的话我们不由想到这种写法是的语法糖是 v-modal,那我们的父组件可以这样使用



<Son v-model="number" />

但是啊v-modal的局限性是只能传递value, 而且只可以传递一次

# $attrs 和 $listeners

# $attrs 和 v-bind

假设我们有一个需求,要将父组件的数据传递给孙组件,但是我们的子组件又没有使用到这些数据,这个时候我们可以使用$attrs




// Parent.vue
 <Son :number="number" :count="count" />



// Son.vue
<Grandson v-bind="$attrs" />



// Grandson.vue
{{ $attrs }}

在子组件中可以通过v-bind="$attrs"的方式将父组件传递下来的数据原封不动的传递给孙组件,而孙组件可以通过$attrs来接收,不过这个用法有个局限性就是子组件不可以接收父组件的数据,也就是不可以有props钩子

# $listeners 和 v-on

既然可以这样传数据了,那方法是不是也可以这么玩,答案是肯定的




// Parent.vue
<Son :number="number" @change="change" @say="say" :count="count" />



// Son.vue
<Grandson v-on="$listeners" />



 

// Grandson.vue
mounted() {
    console.log(this.$listeners)
}

通过 $listenersv-on的配合可以将全部事件传递给孙组件,但是这里需要注意的一点是,子组件也是可以通过$listeners来接收父组件传递的全部事件的

# eventbus

我们先来说下eventbus的原理,假设现在2个组件A和B需要通信,那么我可以在组件C上面用$on发布一个事件,那么A和B就可以通过C.$emit()来互相通信了吧,就是弄一个全局的发布订阅




 

// main.js
Vue.prototype.$bug = function() {
    return new Vue()
}

我们知道Vue本身就是具有发布订阅能力的,这里我们直接new Vue()就好啦

那么怎么使用呢?




// 组件A - 在组件A中发布事件
this.$bus.$on('change', ()=>{})



// 组件B - 在组件B中可以订阅这个事件实现通信
this.$bus.$emit('change')

# ref

其实一直在纠结ref算不算组件通信的一种,但是我们可以通过ref拿到某一个组件的实例,也就是拿到某一个组件的方法和数据




 


// Parent.vue
<Son ref="son"/>
mounted() {
    console.log(this.$refs.son)
}

可以通过$refs拿到某个组件的实例

# provide 和 inject

接下来我们来说一下很多人都不知道的一种组件传值方法provideinject 我们只需要在父组件使用provide定义需要向下传递的数据,不管层级多深后代组件都可以通过inject接收




 



// Parent.vue
export default {
    provide() {
        return {vm: this}
    },
}

父组件向下传递数据vm




 




// Son.vue
export default {
    inject: ['vm'],
    mounted() {
        console.log(this.vm)
    }
}

子组件可以通过inject接收

但是这个方法在日常开发中不建议使用,因为太乱太难管理了

# 总结

Vue的组件通信太复杂了,因为语法糖和api太多了,所以我们还是选择vuex