Vue的组件是其很是重要的系统,组件之间的通讯也是开发中不可避免的需求vue
通常来讲Vue组件是如下几种关系ajax
A组件和B组件、B组件和C组件、B组件和D组件是父子关系,C组件和D组件是兄弟关系,A组件和C/D组件是隔代关系。vuex
本文阐述了几种经常使用的通讯方式和使用场景api
父组件经过 props
传递数据给子组件,子组件经过 emit
发送事件传递数据给父组件数组
这种父子通讯方式也就是典型的单向数据流,父组件经过 props
传递数据,子组件不能直接修改 props
, 而是必须经过发送事件的方式告知父组件修改数据。app
// component-a <template> <div id="app"> <com-b title="cc" @changeTitle="handChange"></com-b> <!-- 传入title 接受自定义事件changeTitle --> </div> </template> <script> import comB from './components/comB.vue' export default { name: 'app', components: { comB }, methods:{ // 接受子组件传递的事件 handChange(val){ alert(val) } } } </script> // component-b <template> <div> {{title}} <button @click="handChange">Change</button> </div> </template> <script> export default { name: "com-b", props: ["title"],// props 父组件传来的值 methods:{ // 触发自定义事件,想父组件传递发送修改后的数据 handChange(){ this.$emit('changeTitle','newTitle') } } }; </script>
优势:易于使用,结构清晰ide
缺点:只能用于父子组件通讯函数
这两种都是直接获得组件实例,使⽤后能够直接调⽤组件的⽅法或访问数据工具
// component-a <template> <div id="app"> <!-- 为子组件注册引用信息 --> <com-b ref="comB"></com-b> </div> </template> <script> import comB from "./components/comB.vue"; export default { name: "app", components: { comB }, mounted() { // 经过$refs获取到对应子组件的引用信息 console.log(this.$refs.comB); } }; </script>
$parent
和$children
都是基于当前上下文访问父组件和子组件ui
// component-a <template> <div id="app"> <com-b></com-b> </div> </template> <script> import comB from "./components/comB.vue"; export default { name: "app", components: { comB }, mounted() { // 经过$children获取所有子组件 console.log(this.$children); } }; </script> // component-b <template> <div> </div> </template> <script> export default { name: "com-b", mounted(){ // 经过$parent获取父组件 console.log(this.$parent); } }; </script>
ref
和$parent/$children
的优缺点和props&&emit
相同,弊端都是没法在跨级和兄弟间通讯
ref
和$parent/$children
在跨级通讯中有必定的弊端。Vue.js 2.2.0
版本后新增 provide / inject
API
vue文档
这对选项须要⼀起使⽤,以容许⼀个祖先组件向其全部⼦孙后代注⼊⼀个依赖,不论组件层次有多深,并在起上下游关系成⽴的时间⾥始终⽣效
provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性。
inject 选项应该是:
一个对象,对象的 key 是本地的绑定名,value 是:
一个对象,该对象的:
provide 和 inject 绑定并非可响应的。这是刻意为之的。然而,若是你传入了一个可监听的对象,那么其对象的属性仍是可响应的。
// 父级组件提供 'foo' var Provider = { provide: { foo: 'bar' }, // ... } // 子组件注入 'foo' var Child = { inject: ['foo'], created () { console.log(this.foo) // => "bar" } // ... }
在作 Vue ⼤型项⽬时,可使⽤ Vuex 作状态管理。使用 provide / inject
,能够模拟 达到 Vuex
的效果 。
使⽤ Vuex,最主要的⽬的是跨组件通讯、全局数据维护。⽐如⽤户的登陆信息维护、通知信息维护等全局的状态和数据
一般vue应用都有一个根根组件app.vue
,能够⽤来存储全部须要的全局数据和状态,methods
等。项目中全部的组件其父组件都是app
,经过provide
将app
实例暴露对外提供
<template> <div> <router-view></router-view> </div> </template> <script> export default { provide () { return { app: this } } } </script>
接下来任何组件只要经过 inject
注⼊ app.vue
的 app
的话,均可以直接经过this.app.xxx
来访问 app.vue
的 data、computed、methods
等内容
例如经过这个特性保存登陆信息
export default { provide() { return { app: this }; }, data() { return { userInfo: null }; }, methods: { getUserInfo() { // 这⾥经过 ajax 获取⽤户信息后,赋值给this.userInfo; $.ajax("/user", data => { this.userInfo = data; }); } }, mounted() { this.getUserInfo(); } };
以后在任何⻚⾯或组件,只要经过 inject
注⼊ app
后,就能够直接访问 userInfo
的数据了
<template> <div> {{ app.userInfo }} </div> </template> <script> export default { inject: ['app'] } </script>
优势:
缺点:
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。当须要开发开发大型单页应用(SPA),就应该考虑使用Vuex了,它能把组件的共享状态抽取出来,当作一个全局单例模式进行管理。这样无论你在何处改变状态,都会通知使用该状态的组件作出相应修改。Vuex
官方文档已经给出了详细的使用方式
优势:
缺点:
若是不是大型项目,状态管理不复杂,数据量不是很大,没有必要使用Vuex
可使用一个空的vue
实例做为事件总线中间件Bus
处理组件间的通讯
首先在全局定义bus
let bus = new Vue(); var eventBus = { install(Vue, options) { Vue.prototype.$bus = bus; } }; Vue.use(eventBus);
而后就能够在组件中使用$on
,$emit
,off
来监听,分发和销毁组件
分发组件
// component-c <template> <div> <button @click="handClick">handClick</button> </div> </template> <script> export default { name: "com-c", methods: { handClick: function() { this.$bus.$emit("bus", "val"); } } }; </script>
监听组件
// component-d <template> <div></div> </template> <script> export default { name: "com-d", // ... created() { this.$bus.$on("bus", val => { //获取传递的参数并进行操做 console.log(val); }); }, // 最好在组件销毁前 // 清除事件监听 beforeDestroy() { this.$bus.$off("evetn"); } }; </script>
最好在组件销毁以前清除监听事件
优势:
缺点:
$dispatch
和 $broadcast
是Vue1.x中提供的API,前者⽤于向上级派发事件,只要是它的⽗级(⼀级或多级以上),均可以在组件内经过 $on 监听到,后者相反,是由上级向下级⼴播事件
// 子组件 vm.$dispatch(eventName,params) // 父组件 vm.$on(eventName , (params) => { console.log(params); });
$broadcast
相似,只不过⽅向相反。这两种⽅法⼀旦发出事件后,任何组件都是能够接收到的,就近原则,⽽且会在第⼀次接收到后停⽌冒泡,除⾮返回 true
。
这2个方法在已经被弃用,Vue
官方给出的解释是:
由于基于组件树结构的事件流方式实在是让人难以理解,而且在组件结构扩展的过程当中会变得愈来愈脆弱。这种事件方式确实不太好,咱们也不但愿在之后让开发者们太痛苦。而且$dispatch 和 $broadcast 也没有解决兄弟组件间的通讯问题。
虽然在开发中,没有Vuex这样的专业状态管理工具方便好用,可是在独立组件库和一些特殊场景中,也是很是好用的一种传递方式。
自行模拟dispatch/broadcast
没法达到与原方法如出一辙的效果,可是基本功能都是能够实现的,解决组件之间的通讯问题
方法有功能有向上/下找到对应的组件,触发指定事件并传递数据,其下/上级组件已经经过$on
监听了该事件。
首先须要正确地向上或向下找到对应的组件实例,并在它上⾯触发⽅法。
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); } } };
这两个⽅法都接收了三个参数,第⼀个是组件的 name 值,⽤于向上或向下递归遍从来寻找对应的组件,第⼆个和第三个就是上⽂分析的⾃定义事件名称和要传递的数据。
在 dispatch
⾥,经过 while
语句,不断向上遍历更新当前组件(即上下⽂为当前调⽤该⽅法的组件)的⽗组件实例(变量 parent
即为⽗组件实例),直到匹配到定义的 componentName
与某个上级组件的 name
选项⼀致时,结束循环,并在找到的组件实例上,调⽤ $emit
⽅法来触发⾃定义事件 eventName
。 broadcast
⽅法与之相似,只不过是向下遍历寻找
优势:
缺点:
上述介绍的各类通讯方法都有各自的局限性,咱们能够实现一个 findComponents
系列的方法,能够实现
5个方法都是经过递归和遍历,经过组件name
选项匹配到指定组件返回
function findComponentUpward(context, componentName) { let parent = context.$parent; // 获取父级组件 let name = parent.$options.name; // 获取父级组件名称 // 若是父级存在 且 父级组件 没有name 或 name与要寻找的组件名不一致,重置parent和name,再逐级寻找 while (parent && (!name || [componentName].indexOf(name) < 0)) { parent = parent.$parent; if (parent) name = parent.$options.name; } // 逐级查找父级组件名和传参名是否一致,返回找到的parent return parent; }
findComponentUpward
接收两个参数,第⼀个是当前上下⽂,即你要基于哪一个组件来向上寻找,⼀般都是基于当前的组件,也就是传⼊ this;第⼆个参数是要找的组件的 name 。dispatch
是经过触发和监听事件来完成事件交互,findComponentUpward
会直接拿到组件的实例
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 []; } }
findComponentsUpward
会返回的是⼀个数组,包含了全部找到的组件实例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; }
context.$children
获得的是当前组件的所有⼦组件,因此须要遍历⼀遍,找到有没有匹配到的组件 name
,若是没找到,继续递归找每一个 $children
的 $children
,直到找到最近的⼀个为⽌
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); }, []); }
使⽤ reduce
作累加器,并⽤递归将找到的组件合并为⼀个数组并返回
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; }
findBrothersComponents
多了⼀个参数 exceptMe
,是否把自己除外,默认是 true
。寻找兄弟组件的⽅法,是先获取 context.$parent.$children
,也就是⽗组件的所有⼦组件,这⾥⾯当前包含了自己,全部也会有第三个参数exceptMe
。Vue.js
在渲染组件时,都会给每一个组件加⼀个内置的属性 _uid
,这个 _uid
是不会重复的,借此咱们能够从⼀系列兄弟组件中把⾃⼰排除掉。