vue组件间通信

96 阅读3分钟

1. 父子组件间通信

  • props / $emit

父组件通过props的方式向子组件传递数据,而通过$emit 子组件可以向父组件通信。

(1)父组件向子组件传值

<!-- 父组件 --> 
               <template> 
                    <div class="section"> 
                        <child :msg="articleList"></child> 
                    </div> 
               </template> 
               <script> 
                   import child from './child.vue' 
                  export default { 
                      name: 'HelloWorld', 
                      components: { comArticle }, 
                      data() { return { msg: '阿离王' 
                      } 
                     } 
                    } 
                </script>
<!-- 子组件 child.vue --> 
<template> 
    <div> {{ msg }} </div> 
</template> 
<script> 
    export default { 
        props: { msg: String } 
            } 
</script>

注意:

  • 第一,不应该在一个子组件内部改变 prop,这样会破坏单向的数据绑定,导致数据流难以理解。如果有这样的需要,可以通过 data 属性接收或使用 computed 属性进行转换

  • 第二,如果 props 传递的是引用类型(对象或者数组),在子组件中改变这个对象或数组,父组件的状态会也会做相应的更新,利用这一点就能够实现父子组件数据的“双向绑定”,虽然这样实现能够节省代码,但会牺牲数据流向的简洁性,令人难以理解,最好不要这样去做。

  • 想要实现父子组件的数据“双向绑定”,可以使用 v-model 或 .sync。

(2)子组件向父组件传值

<!-- 父组件 --> 
<template> 
    <div class="section"> 
        <child :msg="articleList" @changMsg="changMsg"></child> 
    </div> 
</template> 
<script> 
    import child from './child.vue' 
    export default { 
        name: 'HelloWorld', 
        components: { comArticle }, 
        data() { 
            return { 
                msg: '高渐离' 
                } 
         }, 
        methods:{ 
        changMsg(msg) { 
        this.msg = msg 
        } 
       } 
      } 
 </script>
<!-- 子组件 child.vue --> 
<template> 
    <div> {{ msg }} 
        <button @click="change">改变字符串</button> 
    </div> 
</template> 
<script> 
    export default { 
           props: { msg: String }, 
           methods: { 
               change(){ 
                   this.$emit('changMsg', '高渐离带你学习前端')//changMsg保持一致 
                   } 
                  } 
                 } 
</script>
  • provide/ inject

provide/ inject 是vue2.2.0新增的api, 简单来说就是父组件中通过provide来提供变量, 然后再子组件中通过inject来注入变量。

注意: 这里不论子组件嵌套有多深, 只要调用了inject 那么就可以注入provide中的数据,而不局限于只能从当前父组件的 props属性中回去数据

// A.vue 
//三个组件: A.vue、B.vue、C.vue 其中 C是B的子组件,B是A的子组件 
<template> 
    <div> 
        <comB></comB> 
    </div> 
</template> 
<script> 
    import comB from '../components/test/comB.vue' 
    export default { 
        name: "A", 
        provide: { for: "demo" }, 
        components:{ comB } 
        } 
</script>
// B.vue 
<template> 
    <div> 
        {{demo}} 
        <comC></comC> 
    </div> 
</template> 
<script> 
    import comC from '../components/test/comC.vue' 
    export default { 
        name: "B", 
        inject: ['for'], 
        data() { 
            return { 
                demo: this.for 
                } 
               }, 
        components: { comC } } 
</script>
// C.vue
<template>
    <div>
        {{demo}}
    </div>
</template>
<script>
    export default {
        name: "C",
        inject: ['for'],
        data() {
            return {
            demo: this.for
            }
        }
    }
</script>
  • attrs/attrs/listeners

多级组件嵌套需要传递数据时,通常使用的方法是通过vuex。但如果仅仅是传递数据,而不做中间处理,使用 vuex 处理,未免有点大材小用。为此Vue2.4 版本提供了另一种方法----attrs/attrs/listeners

  • attrs:包含了父作用域中不被prop所识别(且获取)的特性绑定(classstyle除外)。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定(classstyle除外),并且可以通过vbind="attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用
  • listeners:包含了父作用域中的(不含.native修饰器的)von事件监听器。它可以通过von="listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="listeners" 传入内部组件
// index.vue
<template>
    <div>
        <h2>浪里行舟</h2>
        <child-com1
        :foo="foo"
        :boo="boo"
        :coo="coo"
        :doo="doo"
        title="前端工匠"
        ></child-com1>
    </div>
</template>
<script>
    const childCom1 = () => import("./childCom1.vue");
    export default {
        components: { childCom1 },
        data() {
            return {
                foo: "Javascript",
                boo: "Html",
                coo: "CSS",
                doo: "Vue"
            };
        }
    };
</script>
// childCom1.vue 
<template class="border"> 
    <div> 
        <p>foo: {{ foo }}</p> 
        <p>childCom1的$attrs: {{ $attrs }}</p> 
        <child-com2 v-bind="$attrs"></child-com2> 
    </div> 
</template> 
<script> 
    const childCom2 = () => import("./childCom2.vue"); 
    export default { 
        components: { childCom2 }, 
        inheritAttrs: false, // 可以关闭自动挂载到组件根元素上的没有在props声明的属性 
        props: { foo: String // foo作为props属性绑定 }, 
        created() { 
            console.log(this.$attrs); // 
            { 
                "boo": "Html", 
                "coo": "CSS", 
                "doo": "Vue", 
                "title": "前端工匠" 
            } 
        } 
    }; 
</script>
// childCom2.vue 
<template> 
    <div class="border"> 
        <p>boo: {{ boo }}</p> 
        <p>childCom2: {{ $attrs }}</p> 
        <child-com3 v-bind="$attrs"></child-com3> 
    </div> 
</template> 
<script> 
    const childCom3 = () => import("./childCom3.vue"); 
    export default { 
        components: { childCom3 }, 
        inheritAttrs: false, 
        props: { boo: String }, 
        created() { 
            console.log(this.$attrs); // { "coo": "CSS", "doo": "Vue", "title": "前端工匠" }
        } 
    }; 
</script>
// childCom3.vue 
<template> 
    <div class="border"> 
    <p>childCom3: {{ $attrs }}</p> 
</div> 
</template> 
<script> 
    export default { 
        props: { 
            coo: String, 
            title: String 
        } 
    }; 
</script>

2. 事件总线(EventBus)

eventBus呢,其实原理就是 事件订阅发布,eventBus 又称为事件总线,在vue中可以使用它来作为沟通桥梁的概念, 就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件, 所以组件都可以通知其他组件。

var Event=new Vue(); 
Event.$emit(事件名,数据); 
Event.$on(事件名,data => {});

这里我们可以直接使用 vue 自带的事件监听,也就是 emitemit emiton,我们来简单封装下:

  1. 首先需要创建一个事件总线并将其导出, 以便其他模块可以使用或者监听它

新建一个 event-bus.js 文件

// event-bus.js 
import Vue from 'vue' 
export const EventBus = new Vue()
2.发生事件

假设你有两个组件: additionNum 和 showNum, 这两个组件可以是兄弟组件也可以是父子组件;这里我们以兄弟组件为例:

<template> 
    <div> 
        <show-num-com></show-num-com> 
        <addition-num-com></addition-num-com> 
    </div> 
</template> 
<script> 
    import showNumCom from './showNum.vue' 
    import additionNumCom from './additionNum.vue' 
    export default { 
        components: { 
            showNumCom, additionNumCom 
        } 
    } 
</script>
// addtionNum.vue 中发送事件 
<template> 
    <div> 
        <button @click="additionHandle">+加法器</button> 
    </div> 
</template> 
<script> 
import { EventBus } from './event-bus.js' 
console.log(EventBus) 
export default { 
    data() { 
        return { 
        num: 1 
        } 
    }, 
    methods: { 
        additionHandle() { 
            EventBus.$emit('addition', { 
                num: this.num++ 
            }) 
        } 
    } 
} 
</script>
3.接收事件
// showNum.vue 中接收事件 
<template> 
    <div>计算和: {{count}}</div> 
</template> 
<script> 
import { EventBus } from './event-bus.js' 
export default { 
    data() { 
        return { 
            count: 0 
        } 
    }, 
    mounted() { 
        EventBus.$on('addition', param => { 
        this.count = this.count + param.num; 
            }) 
        } 
} 
</script>

这样就实现了在组件addtionNum.vue中点击相加按钮, 在showNum.vue中利用传递来的 num 展示求和的结果.

3. Vuex

  • 简要介绍各模块在流程中的功能:
    • Vue Components:Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。
    • dispatch:操作行为触发方法,是唯一能执行action的方法。
    • actions:操作行为处理模块,由组件中的$store.dispatch('action 名称', data1)来触发。然后由commit()来触发mutation的调用 , 间接更新 state。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发
    • commit:状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。
    • mutations:状态改变操作方法,由actions中的commit('mutation 名称')来触发。是Vuex修改state的唯一推荐方法。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook暴露出来,以进行state的监控等。
    • state:页面状态管理容器对象。集中存储Vue components中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。
    • getters:state对象读取方法。图中没有单独列出该模块,应该被包含在了render中,Vue Components通过该方法读取全局state对象。
  • Vuex与localStorage
    • vuex 是 vue 的状态管理器,存储的数据是响应式的。但是并不会保存起来,刷新之后就回到了初始状态,具体做法应该在vuex里数据改变的时候把数据拷贝一份保存到localStorage里面,刷新之后,如果localStorage里有保存的数据,取出来再替换store里的state。
    • 注意的是:由于vuex里,我们保存的状态,都是数组,而localStorage只支持字符串,所以需要用JSON转换:

JSON.stringify(state.subscribeList); // array -> string JSON.parse(window.localStorage.getItem("subscribeList")); // string -> array

4. 作用域插槽

就是把子组件的数据通过插槽的方式传给父组件使用,然后再插回来

// Child.vue 
<template> 
    <div> 
        <slot :user="user"></slot> 
    </div> 
</template> 
export default{ 
    data(){ 
        return { 
            user:{ name:"xxx" } 
        } 
    } 
} 

// Parent.vue 
<template> 
    <div> 
        <child v-slot="slotProps"> {{ slotProps.user.name }} </child> 
    </div> 
</template>

5. refs

ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例,可以通过实例直接调用组件的方法或访问数据, 我们看一个ref 来访问组件的例子:

// 子组件 A.vue 
export default { 
    data () { 
        return { 
            name: 'Vue.js' 
        } 
    }, 
    methods: { 
        sayHello () { 
            console.log('hello') 
        } 
    } 
}

// 父组件 app.vue 
<template> 
    <component-a ref="comA"></component-a> 
</template> 
<script> 
export default { 
    mounted () { 
        const comA = this.$refs.comA; 
        console.log(comA.name); // Vue.js 
        comA.sayHello(); // hello 
    } 
} 
</script>

ref 这种方式,就是获取子组件的实例,然后可以直接子组件的方法和访问操作data的数据,就是父组件控制子组件的一种方式,子组件想向父组件传参或操作,只能通过其他的方式了

6. 路由组件传值

路由的query传参,路由的params参数,通过hash传参,通过props传参

  • 路由的query参数传参(类似于axios通过query参数发送请求)

<!-- 跳转并携带query参数,to的字符串写法 --> 
<router-link :to="/home/school/?id=1&name=王城二小"></router-link> 
<!-- 跳转并携带query参数,to的对象写法 --> 
<router-link :to="{ 
    path:'/home/school', 
    query:{ 
        id:1, 
        name:'王城二小' 
    } 
}" ></router-link>

route.query.idroute.query.id route.query.title

  • 路由的params参数传参(类似于axios通过params参数发送请求),这里不再传数组,传单个数据

在router的配置文件中,声明接收params参数

export default new VueRouter({ 
    routes: [ 
    { 
        name:"aboutus", 
        path: '/aboutus', 
        component: Aboutus 
     }, 
        { 
        name: "home", 
        path: '/home', 
        component: Home,
            children:[ 
                { 
                    name: "school",
                    path: '/school/:id/:name',//使用占位符声明接收params参数 
                    component: School 
                }, 
                { 
                    name: "student", 
                    path: '/student', 
                    component: Student 
                }, 
            ] 
        } 
    ] 
})

传递参数

<!-- 跳转并携带params参数,to的字符串写法 --> 
<router-link :to="/home/school/id/name">跳转</router-link> 
<!-- 跳转并携带params参数,to的对象写法 --> 
<router-link :to="{ 
    name:'school',//必须使用name 
    params:{   
        id:1,           
        name:'王城二小' 
    } 
}" >跳转</router-link>

特别注意:路由携带params参数时,若使用to的对象写法,则不能使用path配置项,必须使用name配置! 接收参数:

$route.params.id $route.params.name
  • 通过props传参
    • 布尔模式
//组件 
const User = { 
    props: ['id'], // 组件中通过 props 获取 id 
    template: '<div>User {{ id }}</div>' 
} 
// 路由配置中,增加 props 字段,并将值 设置为 true,把路由收到params所有的参数通过props传递给组件 
const routes = [{ path: '/user/:id', component: User, props: true }]
    • 对象模式
//路由配置,对象中的key-value都会通过props传递给组件 
const routes = [ 
    { 
        path: '/hello', 
        component: Hello, 
        props: { 
            name: 'World' 
        } 
    } 
] 
//组件中获取数据 
const Hello = { 
    props: { 
        name: { 
            type: String,
            default: 'Vue' 
        } 
    }, 
    template: '<div> Hello {{ name }}</div>' 
}
    • 函数模式

// 创建一个返回 props 的函数 
const dynamicPropsFn = (route) => { 
    return { name: route.query.say + "!" } 
} 

const routes = [ 
    { 
    path: '/hello',
    component: Hello,
    props: dynamicPropsFn // 
    props(route)
        { 
            return{ 
                id:route.query.id, 
                title:route.query.title 
            } 
        } // 
    } 
] 
const Hello = { 
    props: { 
        name: { 
            type: String, 
            default: 'Vue' 
        } 
    }, 
    template: '<div> Hello {{ name }}</div>' 
}
  • 通过hash传参

    通过此方式,url 路径中带有 hash,例如:/details/001#car。

    • 路由配置

使用 hash 时,以下三种方式都是可行的(同 query):

this.$router.push('/details/001#car') 
this.$router.push({ path: '/details/001', hash: '#car'})
this.$router.push({ name: 'details', params: { id: '001' }, hash: 'car'})
    • 组件获取数据

组件通过 $route.hash.slice(1) 获取:

const Details = { 
    template: '<div>Details {{ $route.hash.slice(1) }} </div>', 
    }

7. 总结

常见使用场景可以分为三类:

  • 父子组件通信: props/emitemit、parent/children、 provide/inject 、 ref/children、 provide/inject 、 ref/refs 、attrs/attrs/listeners、slot
  • 兄弟组件通信: eventBus 、 vuex
  • 跨级通信: eventBus、 Vuex、  provide / inject 、 attrs/attrs / listeners