Vue组件间通讯方式(历史最全)

745 阅读1分钟

bb.jpg 组件之间存在父子关系、兄弟关系、隔代关系,针对不同的场景,选择更适合自己的通讯方式。

方法一、props/$emit

父子组件的数据通讯

  • props主要是子组件接收父组件传递的数据
  • $emit是子组件触发父组件传递的方法,通过参数传递给父组件数据
父组件Parent.vue
<template>
    <div class="parent">
        <Child :obj="obj" @my-event="myEventHandle"></Child>
    </div>
</template>
<script>
import Child from "./Child.vue"
export default {
    data () {
        return {
            obj: {
                a: 1,
                b: 2
            }
        }
    },
    components: {
        Child
    },
    methods: {
        myEventHandle (data) {
            console.log(data)
        }
    }
}
</script>

子组件Child.vue
<template>
    <div class="child">
        <button @click="$emit('my-event', '子组件传给父组件的数据')">click me</button>
    </div>
</template>
<script>
export default {
    props: {
        obj: {
            type: Object,
            default: () => ({}) // {}
            // default: function () {
            //     return {}
            // } // {}
            // default: () => {} // undefined
        }
    }
}
</script>

PS:Props接收对象时,默认值这么写default: () => {},在不传obj的情况下obj为undefined,使用上面2种方式均可

.sync修饰符

模拟组件之间双向数据流的简写形式,推荐update:myPropName的模式触发事件。

父组件
<template>
    <div class="about">
        <div>.sync 修饰符</div>
        <!-- <SyncDemo :title="title" @update:title="title = $event"></SyncDemo> -->
        <SyncDemo :title.sync="title"></SyncDemo>
    </div>
</template>
<script>
import SyncDemo from "./SyncDemo.vue"
export default {
    data () {
        return {
            title: "sync demo"
        }
    },
    components: {
        SyncDemo
    }
}
</script>

子组件
<template>
    <div>
        <div>{{title}}</div>
        <button @click="$emit('update:title', 'change title')">change title</button>
    </div>
</template>
<script>
export default {
    props: ["title"]
}
</script>

注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用,可以同时蚌寺那个多个属性

v-model

组件上的 v-model 默认会利用名为 valueprop 和名为 input 的事件

父组件
<template>
    <div class="about">
        <div>v-model component 自定义输入组件 {{ cval }}</div>
        <!-- <VmodelComponent :value="cval" @input="cval = $event"></VmodelComponent> -->
        <VmodelComponent v-model="cval"></VmodelComponent>
    </div>
</template>
<script>
import VmodelComponent from "./VmodelComponent.vue"
export default {
    data () {
        return {
            val: ""
        }
    },
    components: {
        VmodelComponent
    }
}
</script>
子组件
<template>
    <div class="about">
        <input :value="value" @input="$emit('input', $event.target.value)"/>
    </div>
</template>
<script>
export default {
    props: ["value"]
}
</script>

方法二、eventBus

通过一个空的Vue实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级。

全局注册

main.js
// 定义事件车
const bus = new Vue()
Vue.prototype.$bus = bus

局部注册

eventBus.js
import Vue from 'vue'
export default const bus = new Vue()

实现

触发事件eventHandle并附带参数
this.$bus.$emit("eventHandle", "兄弟组件传递数据")
监听当前实例上的自定义事件eventHandle,通过回调接收附带参数
this.$bus.$on("eventHandle", function (data) {
    console.log(data)
})
beforeDestory中移除自定义事件监听器
this.$bus.$off("eventHandle")

方法三、Vuex

参考Vuex官网

购物车实例

方法四、provide/inject

这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。

祖先组件注入依赖provideVal,当点击按钮修改data中数据val时,子孙组件并没有实时更新。

祖先组件 Provide.vue
<template>
    <div>
        {{val}}
        <button @click="changeHandle">change val</button>
        <Inject />
    </div>
</template>
<script>
import Inject from './Inject.vue'
export default {
    data () {
        return {
            val: "父组件的数据val"
        }
    },
    provide () {
        return {
            provideVal: this.val
        }
    },
    methods: {
        changeHandle () {
            this.val = "更改父组件的数据val"
        }
    },
    components: {
        Inject
    }
}
</script>

子孙组件 Inject.vue
<template>
    <div>
        <div>接收注入依赖: {{provideVal}}</div>
    </div>
</template>
<script>
export default {
    inject: ["provideVal"]
}
</script>

官网提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象(通过函数注入),那么其对象的 property 还是可响应的。

祖先组件 Provide.vue
<template>
    <div>
        {{val}}
        <button @click="changeHandle">change val</button>
        <Inject />
    </div>
</template>
<script>
import Inject from './Inject.vue'
export default {
    data () {
        return {
            val: "父组件的数据val"
        }
    },
    provide () {
        return {
            provideVal: () => this.val
        }
    },
    methods: {
        changeHandle () {
            this.val = "更改父组件的数据val"
        }
    },
    components: {
        Inject
    }
}
</script>

子孙组件 Inject.vue
<template>
    <div>
        <div>接收注入依赖: {{provideVal()}}</div>
        <div>计算属性,不需要执行方法了 {{provideValNew}}</div>
    </div>
</template>
<script>
export default {
    inject: {
        provideVal: {
            default: () => ({})
        }
    },
    computed: {
        provideValNew () {
            return this.provideVal()
        }
    }
}
</script>

inject

inject是对象时,value有两个属性

  • from 注入依赖中搜索来源的key
  • default 降级后的默认值

方法五、$children/$parent

$parent/$children访问父/子组件的实例

子组件可以存在多个,this.$children为数组类型

孙子组件可以通过this.$parent.$emit('my-event')触发父组件传递给子组件的方法

当组件层级更深时,推荐使用provide/inject

父组件 Parent.vue
<template>
    <div class="parent">
        <Child @my-event="myEventHandle"></Child>
    </div>
</template>
<script>
import Child from "./Child.vue"
export default {
    data () {
        return {
            val: "我是父组件数据!"
        }
    },
    mounted () {
        console.log(this.$children[0].val)
        console.log(this.$children[0].$children[0].val)
    },
    methods: {
        myEventHandle () {
            console.log("myEventHandle")
        }
    },
    components: {
        Child
    }
}
</script>

子组件 Child.vue
<template>
    <div class="child">
        <Grandson />
    </div>
</template>
<script>
import Grandson from "./Grandson.vue"
export default {
    data () {
        return {
            val: "我是子组件数据"
        }
    },
    mounted () {
        console.log(this.$parent.val)
    },
    components: {
        Grandson
    }
}
</script>

孙子组件 Grandson.vue
<template>
    <div class="grandson">
        <button @click="$parent.$emit('my-event')">click me</button>
    </div>
</template>
<script>
export default {
    data () {
        return {
            val: "我是孙子组件数据"
        }
    }
}
</script>

$dispatch

实现向上通知某个子组件触发其父组件的方法

Vue.prototype.$dispatch = function (eventName, componentName, ...args) {
    let parent = this.$parent
    while (parent) {
        // 只有是特定组件,才会触发事件。而不会一直往上,一直触发
        const isSpecialComponent = parent.$options.name === componentName
        if (isSpecialComponent) {
            // 触发了,就终止循环
            parent.$emit(eventName, ...args)
            return
        }
        parent = parent.$parent
    }
}

$broadcast

向下通知某个子组件触发其父组件的方法

Vue.prototype.$broadcast = function (eventName, componentName, ...args) {
    // 这里children是所有子组件,是子组件不是后代组件哈
    const children = this.$children
    broadcast(children)
    // 这里注意,抽离新的方法递归,而不是递归$broadcast
    function broadcast (children) {
        for (let i = 0; i < children.length; i++) {
            const child = children[i]
            const isSpecialComponent = child.$options.name === componentName
            if (isSpecialComponent) {
                // 触发了,就终止循环
                child.$emit(eventName, ...args)
                return
            }
            // 没触发的话,就看下有没有子组件,接着递归
            child.$children.length && child.$broadcast(eventName, componentName, ...args)
        }
    }
}

参考来源:颜酱 

方法六、ref

如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。

父组件通过ref触发子组件的输入框聚焦:

父组件 ParentRefDemo.vue
<template>
    <div>
        <ChildRefDemo ref="childRefs" />
    </div>
</template>
<script>
import ChildRefDemo from "./ChildRefDemo.vue"
export default {
    mounted () {
        // this.$refs.childRefs获取子组件实例
        this.$refs.childRefs.focusHandle()
    },
    components: {
        ChildRefDemo
    }
}
</script>

子组件 ChildRefDemo.vue
<template>
    <div>
        <input ref="inputRefs" />
    </div>
</template>
<script>
export default {
    methods: {
        focusHandle () {
            this.$refs.inputRefs.focus()
        }
    }
}
</script>

$refs只会在组件渲染完之后生效,并且他不是响应式的,应该避免在模板和计算属性中使用

方法七、$attrs/$listeners

$attrs

父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外),默认绑定在子组件的根元素上。那怎么绑定到子组件非根元素上呢?

inheritAttrs: false阻止默认行为,但不影响 class 和 style 绑定;同时需要v-bind="$attrs" 传入内部组件

父组件
<template>
    <div>
        <Child type="checkbox" />
    </div>
</template>
<script>
import Child from "./Child.vue"
export default {
    components: {
        Child
    }
}
</script>
子组件
<template>
    <div>
        <input v-bind="$attrs" />
    </div>
</template>
<script>
export default {
    inheritAttrs: false
}
</script>

$listeners

组件的根元素上监听一个原生事件。可以使用 v-on 的修饰符.native,此时组件内根元素应该是input,一旦根元素有所更改,父级的.native 监听器将静默失败。那怎么才能将原生事件绑定在子元素内呢?

包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

父组件
<template>
    <div class="about">
        <VmodelEvent @focus="myFocus"></VmodelEvent>
    </div>
</template>
<script>
import VmodelEvent from "./VmodelEvent.vue"
export default {
    methods: {
        myFocus () {
            console.log("focus")
        }
    },
    components: {
        VmodelEvent
    }
}
</script>

子组件
<template>
    <div>
        <input v-on="$listeners" />
        <Grandson v-on="$listeners" />
    </div>
</template>
<script>
import Grandson from "./Grandson.vue"
export default {
    components: {
        Grandson
    }
}
</script>

在创建更高层次的组件时非常有用,$attrs/$listeners层层传递

方法八、Vue.observable( object )

让object可响应,Vue内部就是通过这个方法处理data中的数据。

返回的对象可以直接用于渲染函数计算属性内,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器,用于简单的场景。

计算属性

利用计算属性进行响应式通讯

<template>
    <div>
        <div>{{msg}}</div>
        <button @click="change">change msg</button>
    </div>
</template>
<script>
import Vue from 'vue'
const state = Vue.observable({ msg: "Hello Observable" })
export default {
    computed: {
        msg () {
            return state.msg
        }
    },
    methods: {
        change () {
            state.msg = "change msg"
        }
    }
}
</script>

渲染函数

利用渲染函数进行响应式通讯

<script>
import Vue from 'vue'
const state = Vue.observable({ msg: "Hello Observable" })
export default {
    render (h) {
        return h('button', {
            on: { click: () => { state.msg = 'change msg' } }
        }, `count is: ${state.msg}`)
    }
}
</script>

方法九、$root

所有的子组件都可以通过this.$root来访问根实例的数据、计算属性和方法,还可以更改根实例的数据。对于demo或者小型的具有少量组件的应用还是比较方便的,对于中大型项目建议使用Vuex来管理应用的状态。这个实例可以作为一个全局 store 来访问或使用。

new Vue({
    data () {
        return {
            rootVal: "$root获取根实例的值、计算属性和方法"
        }
    },
    computed: {},
    methods: {},
    render: h => h(App)
}).$mount('#app')

方法十、其他方式

插槽传值、路由传参、本地存储