父子关系便是组件 A 在它的模板中使用了组件 B,那么组件 A 就是父组件,组件 B 就是子组件。vue
// 注册一个子组件 Vue.component('child', { data: function(){ return { text: '我是father的子组件!' } }, template: '<span>{{ text }}</span>' }) // 注册一个父组件 Vue.component('father', { template: '<div><child></child></div>' // 在模板中使用了child组件 })
两个组件互不引用,则为兄弟组件。git
Vue.component('brother1', { template: '<div>我是大哥</div>' }) Vue.component('brother2', { template: '<div>我是小弟</div>' })
使用组件的时候:github
<div id="app"> <brother1></brother1> <brother2></brother2> </div>
就是在父子关系中,中间跨了不少个层级vue-router
一个再复杂的组件,都是由三部分组成的:prop、event、slot,它们构成了 Vue.js 组件的 API。vuex
prop 定义了这个组件有哪些可配置的属性,组件的核心功能也都是它来肯定的。写通用组件时,props 最好用对象的写法,这样能够针对每一个属性设置类型、默认值或自定义校验属性的值,这点在组件开发中很重要,然而不少人却忽视,直接使用 props 的数组用法,这样的组件每每是不严谨的。数组
插槽 slot,它能够分发组件的内容。和 HTML 元素同样,咱们常常须要向一个组件传递内容,像这样:app
<alert-box> Something bad happened. </alert-box>
可能会渲染出这样的东西:iview
Error!Something bad happended.
幸亏,Vue 自定义的 <slot> 元素让这变得很是简单:ide
Vue.component('alert-box', { template: ` <div class="demo-alert-box"> <strong>Error!</strong> <slot></slot> </div> ` })
如你所见,咱们只要在须要的地方加入插槽就好了——就这么简单!函数
两种写法:
<template> <button @click="handleClick"> <slot></slot> </button> </template> <script> export default { methods: { handleClick (event) { this.$emit('on-click', event); } } } </script>
经过 $emit,就能够触发自定义的事件 on-click ,在父级经过 @on-click 来监听:
<i-button @on-click="handleClick"></i-button>
因此上面的示例也能够这样写:
<i-button @click.native="handleClick"></i-button>
若是不写 .native 修饰符,那上面的 @click 就是自定义事件 click,而非原生事件 click,但咱们在组件内只触发了 on-click 事件,而不是 click,因此直接写 @click 会监听不到。
Vue.js 内置的通讯手段通常有两种:
用 ref 来访问组件(部分代码省略):
// component-a export default { data () { return { title: 'Vue.js' } }, methods: { sayHello () { window.alert('Hello'); } } }
<template> <component-a ref="comA"></component-a> </template> <script> export default { mounted () { const comA = this.$refs.comA; console.log(comA.title); // Vue.js comA.sayHello(); // 弹窗 } } </script>
$parent 和 $children 相似,也是基于当前上下文访问父组件或所有子组件的。
这两种方法的弊端是,没法在跨级或兄弟间通讯,好比下面的结构:
// parent.vue <component-a></component-a> <component-b></component-b> <component-b></component-b>
咱们想在 component-a 中,访问到引用它的页面中(这里就是 parent.vue)的两个 component-b 组件,那这种状况下,是暂时没法实现的,后面会讲解到方法。
一种无依赖的组件通讯方法:Vue.js 内置的 provide / inject 接口
provide / inject 是 Vue.js 2.2.0 版本后新增的 API,在文档中这样介绍 :
这对选项须要一块儿使用,以容许一个祖先组件向其全部子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。若是你熟悉 React,这与 React 的上下文特性很类似。
provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。
假设有两个组件: A.vue 和 B.vue,B 是 A 的子组件:
// A.vue export default { provide: { name: 'Aresn' } } // B.vue export default { inject: ['name'], mounted () { console.log(this.name); // Aresn } }
须要注意的是:
provide 和 inject 绑定并非可响应的。这是刻意为之的。然而,若是你传入了一个可监听的对象,那么其对象的属性仍是可响应的。
只要一个组件使用了 provide 向下提供数据,那其下全部的子组件均可以经过 inject 来注入,无论中间隔了多少代,并且能够注入多个来自不一样父级提供的数据。须要注意的是,一旦注入了某个数据,那这个组件中就不能再声明 这个数据了,由于它已经被父级占有。
provide / inject API 主要解决了跨级组件间的通讯问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间创建了一种主动提供与依赖注入的关系。而后有两种场景它不能很好的解决:
这种父子(含跨级)传递数据的通讯方式,Vue.js 并无提供原生的 API 来支持,下面介绍一种在父子组件间通讯的方法 dispatch 和 broadcast。
若是父组件A下面有子组件B,组件B下面有组件C,这时若是组件A想传递数据给组件C怎么办呢? Vue 2.4开始提供了$attrs和$listeners来解决这个问题,可以让组件A之间传递消息给组件C。
Vue.component('C',{ template:` <div> <input type="text" v-model="$attrs.messagec" @input="passCData($attrs.messagec)"> </div> `, methods:{ passCData(val){ //触发父组件A中的事件 this.$emit('getCData',val) } } }) Vue.component('B',{ data(){ return { mymessage:this.message } }, template:` <div> <input type="text" v-model="mymessage" @input="passData(mymessage)"> <!-- C组件中能直接触发getCData的缘由在于 B组件调用C组件时 使用 v-on 绑定了$listeners 属性 --> <!-- 经过v-bind 绑定$attrs属性,C组件能够直接获取到A组件中传递下来的props(除了B组件中props声明的) --> <C v-bind="$attrs" v-on="$listeners"></C> </div> `, props:['message'],//获得父组件传递过来的数据 methods:{ passData(val){ //触发父组件中的事件 this.$emit('getChildData',val) } } }) Vue.component('A',{ template:` <div> <p>this is parent compoent!</p> <B :messagec="messagec" :message="message" v-on:getCData="getCData" v-on:getChildData="getChildData(message)"></B> </div> `, data(){ return { message:'hello', messagec:'hello c' //传递给c组件的数据 } }, methods:{ getChildData(val){ console.log('这是来自B组件的数据') }, //执行C子组件触发的事件 getCData(val){ console.log("这是来自C组件的数据:"+val) } } }) var app=new Vue({ el:'#app', template:` <div> <A></A> </div> ` })
要实现的 dispatch 和 broadcast 方法,将具备如下功能:
在子组件调用 dispatch 方法,向上级指定的组件实例(最近的)上触发自定义事件,并传递数据,且该上级组件已预先经过 $on 监听了这个事件;
相反,在父组件调用 broadcast 方法,向下级指定的组件实例(最近的)上触发自定义事件,并传递数据,且该下级组件已预先经过 $on 监听了这个事件。
// 部分代码省略 import Emitter from '../mixins/emitter.js' export default { mixins: [ Emitter ], methods: { handleDispatch () { this.dispatch(); // ① }, handleBroadcast () { this.broadcast(); // ② } } }
//emitter.js 的代码: function broadcast(componentName, eventName, params) { this.$children.forEach(child => { const name = child.$options.name; if (name === componentName) { child.$emit.apply(child, [eventName].concat(params)); } else { broadcast.apply(child, [componentName, eventName].concat([params])); } }); } export default { methods: { dispatch(componentName, eventName, 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.apply(parent, [eventName].concat(params)); } }, broadcast(componentName, eventName, params) { broadcast.call(this, componentName, eventName, params); } } };
由于是用做 mixins 导入,因此在 methods 里定义的 dispatch 和 broadcast 方法会被混合到组件里,天然就能够用 this.dispatch 和 this.broadcast 来使用。
这两个方法都接收了三个参数,第一个是组件的 name 值,用于向上或向下递归遍从来寻找对应的组件,第二个和第三个就是上文分析的自定义事件名称和要传递的数据。
能够看到,在 dispatch 里,经过 while 语句,不断向上遍历更新当前组件(即上下文为当前调用该方法的组件)的父组件实例(变量 parent 即为父组件实例),直到匹配到定义的 componentName 与某个上级组件的 name 选项一致时,结束循环,并在找到的组件实例上,调用 $emit 方法来触发自定义事件 eventName。broadcast 方法与之相似,只不过是向下遍历寻找。
来看一下具体的使用方法。有 A.vue 和 B.vue 两个组件,其中 B 是 A 的子组件,中间可能跨多级,在 A 中向 B 通讯:
<!-- A.vue --> <template> <button @click="handleClick">触发事件</button> </template> <script> import Emitter from '../mixins/emitter.js'; export default { name: 'componentA', mixins: [ Emitter ], methods: { handleClick () { this.broadcast('componentB', 'on-message', 'Hello Vue.js'); } } } </script>
// B.vue export default { name: 'componentB', created () { this.$on('on-message', this.showMessage); }, methods: { showMessage (text) { window.alert(text); } } }
同理,若是是 B 向 A 通讯,在 B 中调用 dispatch 方法,在 A 中使用 $on 监听事件便可。
以上就是自行实现的 dispatch 和 broadcast 方法。
它适用于如下场景:
5 个不一样的场景,对应 5 个不一样的函数,实现原理也大同小异。
// 由一个组件,向上找到最近的指定组件 function findComponentUpward (context, componentName) { let parent = context.$parent; let name = parent.$options.name; while (parent && (!name || [componentName].indexOf(name) < 0)) { parent = parent.$parent; if (parent) name = parent.$options.name; } return parent; } export { findComponentUpward };
好比下面的示例,有组件 A 和组件 B,A 是 B 的父组件,在 B 中获取和调用 A 中的数据和方法:
<!-- component-a.vue --> <template> <div> 组件 A <component-b></component-b> </div> </template> <script> import componentB from './component-b.vue'; export default { name: 'componentA', components: { componentB }, data () { return { name: 'Aresn' } }, methods: { sayHello () { console.log('Hello, Vue.js'); } } } </script>
<!-- component-b.vue --> <template> <div> 组件 B </div> </template> <script> import { findComponentUpward } from '../utils/assist.js'; export default { name: 'componentB', mounted () { const comA = findComponentUpward(this, 'componentA'); if (comA) { console.log(comA.name); // Aresn comA.sayHello(); // Hello, Vue.js } } } </script>
// 由一个组件,向上找到全部的指定组件 function findComponentsUpward (context, componentName) { let parents = []; const parent = context.$parent; if (parent) { if (parent.$options.name === componentName) parents.push(parent); return parents.concat(findComponentsUpward(parent, componentName)); } else { return []; } } export { findComponentsUpward };
// 由一个组件,向下找到最近的指定组件 function findComponentDownward (context, componentName) { const childrens = context.$children; let children = null; if (childrens.length) { for (const child of childrens) { const name = child.$options.name; if (name === componentName) { children = child; break; } else { children = findComponentDownward(child, componentName); if (children) break; } } } return children; } export { findComponentDownward };
// 由一个组件,向下找到全部指定的组件 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); }, []); } export { findComponentsDownward };
// 由一个组件,找到指定组件的兄弟组件 function findBrothersComponents (context, componentName, exceptMe = true) { let res = context.$parent.$children.filter(item => { return item.$options.name === componentName; }); let index = res.findIndex(item => item._uid === context._uid); if (exceptMe) res.splice(index, 1); return res; } export { findBrothersComponents };
相比其它 4 个函数,findBrothersComponents 多了一个参数 exceptMe,是否把自己除外,默认是 true。寻找兄弟组件的方法,是先获取 context.$parent.$children,也就是父组件的所有子组件,这里面当前包含了自己,全部也会有第三个参数 exceptMe。Vue.js 在渲染组件时,都会给每一个组件加一个内置的属性 _uid,这个 _uid 是不会重复的,借此咱们能够从一系列兄弟组件中把本身排除掉。
有时候两个组件之间须要进行通讯,可是它们彼此不是父子组件的关系。在一些简单场景,你可使用一个空的 Vue 实例做为一个事件总线中心(central event bus):
//中央事件总线 var bus=new Vue(); var app=new Vue({ el:'#app', template:` <div> <brother1></brother1> <brother2></brother2> </div> ` }) // 在组件 brother1 的 methods 方法中触发事件 bus.$emit('say-hello', 'world') // 在组件 brother2 的 created 钩子函数中监听事件 bus.$on('say-hello', function (arg) { console.log('hello ' + arg); // hello world })
若是业务逻辑复杂,不少组件之间须要同时处理一些公共的数据,这个时候才有上面这一些方法可能不利于项目的维护,vuex的作法就是将这一些公共的数据抽离出来,而后其余组件就能够对这个公共数据进行读写操做,这样达到了解耦的目的。 详情可参考:https://vuex.vuejs.org/zh-cn/
参考 vue组件之间8种组件通讯方式总结
参考 https://github.com/iview/ivie...
参考 https://github.com/iview/ivie...