组件的分类
常规页面组件, 由 vue-router 产生的每个页面, 它本质上也是一个组件(.vue), 主要承载当前页面的 html 结构, 会包含数据获取, 数据整理, 数据可视化等常规业务.
功能性抽象组件, 不包含业务, 独立, 具体功能的基础组件, 比如日期选择器, 弹窗警告等. 这类组件作为项目的基础控件, 会被大量使用, 因此组件的 API 进行过高强度的抽象, 可以通过不同配置实现不同的功能.
业务组件, 它不像第二类独立组件只包含某个功能, 而是在业务中被多个页面复用的, 它与独立组件的区别是, 业务组件只在当前项目中会用到, 不具有通用性, 而且会包含一些业务, 比如数据请求; 而独立组件不含业务, 在任何项目中都可以使用, 功能单一, 比如一个具有数据校验功能的输入框.
组件的关系
父子组件
父子关系即是组件 A 在它的模板中使用了组件 B, 那么组件 A 就是父组件, 组件 B 就是子组件.
- // 注册一个子组件
- Vue.component('child', {
- data: function(){
- return {
- text: '我是 father 的子组件!'
- }
- },
- template: '<span>{{ text }}</span>'
- })
- // 注册一个父组件
- Vue.component('father', {
- template: '<div><child></child></div>' // 在模板中使用了 child 组件
- })
兄弟组件
两个组件互不引用, 则为兄弟组件.
- Vue.component('brother1', {
- template: '<div > 我是大哥</div>'
- })
- Vue.component('brother2', {
- template: '<div > 我是小弟</div>'
- })
使用组件的时候:
- <div id="app">
- <brother1></brother1>
- <brother2></brother2>
- </div>
跨级组件
就是在父子关系中, 中间跨了很多个层级
组件的构成
一个再复杂的组件, 都是由三部分组成的: prop,event,slot, 它们构成了 vue.js 组件的 API.
属性 prop
prop 定义了这个组件有哪些可配置的属性, 组件的核心功能也都是它来确定的. 写通用组件时, props 最好用对象的写法, 这样可以针对每个属性设置类型, 默认值或自定义校验属性的值, 这点在组件开发中很重要, 然而很多人却忽视, 直接使用 props 的数组用法, 这样的组件往往是不严谨的.
插槽 slot
插槽 slot, 它可以分发组件的内容. 和 HTML 元素一样, 我们经常需要向一个组件传递内容, 像这样:
- <alert-box>
- Something bad happened.
- </alert-box>
可能会渲染出这样的东西:
Error!Something bad happended.
幸好, Vue 自定义的 元素让这变得非常简单:
- Vue.component('alert-box', {
- template: `
- <div class="demo-alert-box">
- <strong>Error!</strong>
- <slot></slot>
- </div>
- `
- })
如你所见, 我们只要在需要的地方加入插槽就行了 -- 就这么简单!
自定义事件 event
两种写法:
在组件内部自定义事件 event
- <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>
用事件修饰符 .native 直接在父级声明 所以上面的示例也可以这样写:
<i-button @click.native="handleClick"></i-button>
如果不写 .native 修饰符, 那上面的 @click 就是自定义事件 click, 而非原生事件 click, 但我们在组件内只触发了 on-click 事件, 而不是 click, 所以直接写 @click 会监听不到.
组件的通信
ref 和children
Vue.JS 内置的通信手段一般有两种:
ref: 给元素或组件注册引用信息;
children: 访问父 / 子实例.
用 ref 来访问组件(部分代码省略):
- // component-a
- export default {
- data () {
- return {
- title: 'Vue.js'
- }
- },
- methods: {
- sayHello () {
- Windows.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>
children 类似, 也是基于当前上下文访问父组件或全部子组件的. 这两种方法的弊端是, 无法在跨级或兄弟间通信, 比如下面的结构:
// parent.vue <component-a></component-a> <component-b></component-b> <component-b></component-b>
我们想在 component-a 中, 访问到引用它的页面中 (这里就是 parent.vue) 的两个 component-b 组件, 那这种情况下, 是暂时无法实现的, 后面会讲解到方法.
provide / inject
一种无依赖的组件通信方法: 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.
listeners
如果父组件 A 下面有子组件 B, 组件 B 下面有组件 C, 这时如果组件 A 想传递数据给组件 C 怎么办呢? Vue 2.4 开始提供了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 和 broadcast 方法, 将具有以下功能: 在子组件调用 dispatch 方法, 向上级指定的组件实例 (最近的) 上触发自定义事件, 并传递数据, 且该上级组件已预先通过 on 监听了这个事件.
// 部分代码省略 import Emitter from '../mixins/emitter.js' export default { mixins: [ Emitter ], methods: { handleDispatch () { this.dispatch(); // 1 }, handleBroadcast () { this.broadcast(); // 2 } } }
//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) { Windows.alert(text); } } }
同理, 如果是 B 向 A 通信, 在 B 中调用 dispatch 方法, 在 A 中使用 $on 监听事件即可. 以上就是自行实现的 dispatch 和 broadcast 方法.
找到任意组件实例 --findComponents 系列方法
它适用于以下场景:
由一个组件, 向上找到最近的指定组件;
由一个组件, 向上找到所有的指定组件;
由一个组件, 向下找到最近的指定组件;
由一个组件, 向下找到所有指定的组件;
由一个组件, 找到指定组件的兄弟组件. 5 个不同的场景, 对应 5 个不同的函数, 实现原理也大同小异.
向上找到最近的指定组件 --findComponentUpward
// 由一个组件, 向上找到最近的指定组件 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>
向上找到所有的指定组件 --findComponentsUpward
// 由一个组件, 向上找到所有的指定组件 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 };
向下找到最近的指定组件 --findComponentDownward
// 由一个组件, 向下找到最近的指定组件 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 };
向下找到所有指定的组件 --findComponentsDownward
// 由一个组件, 向下找到所有指定的组件 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 };
找到指定组件的兄弟组件 --findBrothersComponents
// 由一个组件, 找到指定组件的兄弟组件 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.children, 也就是父组件的全部子组件, 这里面当前包含了本身, 所有也会有第三个参数 exceptMe.Vue.JS 在渲染组件时, 都会给每个组件加一个内置的属性 _uid, 这个 _uid 是不会重复的, 借此我们可以从一系列兄弟组件中把自己排除掉.
Event Bus
有时候两个组件之间需要进行通信, 但是它们彼此不是父子组件的关系. 在一些简单场景, 你可以使用一个空的 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 处理组件之间的数据交互
如果业务逻辑复杂, 很多组件之间需要同时处理一些公共的数据, 这个时候才有上面这一些方法可能不利于项目的维护, vuex 的做法就是将这一些公共的数据抽离出来, 然后其他组件就可以对这个公共数据进行读写操作, 这样达到了解耦的目的. 详情可参考: https://vuex.vuejs.org/zh-cn/
参考 vue 组件之间 8 种组件通信方式总结 参考 参考
来源: https://juejin.im/post/5c6ebe356fb9a049ba422ded