image.png

ref/$parent/$children

基于当前上下文访问父组件或全部子组件:

  • ref:给元素或组件注册引用信息;
  • $parent / $children:访问父 / 子实例。

这两种方法的弊端是,无法在跨级兄弟间通信,比如下面的结构:

  1. // parent.vue
  2. <component-a></component-a>
  3. <component-b></component-b>
  4. <component-b></component-b>
  1. <template>
  2. # 使用 ref 引用
  3. <component-a ref="comA"></component-a>
  4. </template>
  5. <script>
  6. export default {
  7. mounted () {
  8. const comA = this.$refs.comA;
  9. console.log(comA.title); // Vue.js
  10. comA.sayHello(); // 弹窗
  11. }
  12. }
  13. </script>

provide / inject

允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。

  1. // parent.vue
  2. export default {
  3. provide: {
  4. name: 'Aresn'
  5. }
  6. }
  7. // child.vue
  8. export default {
  9. inject: ['name'],
  10. mounted () {
  11. console.log(this.name); // Aresn
  12. }
  13. }

所以,上面 A.vue 的 name 如果改变了,B.vue 的 this.name 是不会改变的,仍然是 Aresn。

===》 解决方式:
之前实习遇到的一个需求场景,下拉选择框 选择了项目就更新下面的tab;但是tab是很深很深。不仅仅是一层;
当时就用到了 provide 和 inject;
但是 provide提供的属性改变了 inject不会改变,即不会触发自身的修改
vue 路由参数变化,页面不刷新,provide /inject 完美解决方案

  1. // 祖先
  2. <template>
  3. <div class="os_container">
  4. <navbar></navbar>
  5. # isRouterAlive改变了之后 就会让下面的页面重新渲染
  6. # 所以定义一个方法reload 修改isRouterAlive(不是真修改,只是通过这个变量触发组件重新渲染
  7. ====》 先让他false 然后在挂载之前又更新回来 this.$nextTick
  8. 问题:怎么把这个方法 传入给 更深的子组件?使得子组件可以调用这个reload;
  9. 当时调用这个方法的子组件是 放在nav里的一个 下拉框; 所以正好是 隔代关系;
  10. <router-view v-if="isRouterAlive"></router-view>
  11. </div>
  12. </template>
  13. <script>
  14. import navbar from '@/components/navbar'
  15. export default {
  16. name:'mainframe',
  17. provide(){
  18. return {
  19. reload:this.reload
  20. }
  21. },
  22. data () {
  23. return {
  24. isRouterAlive:true,
  25. }
  26. },
  27. components:{
  28. navbar,
  29. },
  30. methods:{
  31. reload(){
  32. this.isRouterAlive = false
  33. //在修改数据之后使用 $nextTick,则可以在回调中获取更新后的 DOM
  34. this.$nextTick(()=>{
  35. this.isRouterAlive = true
  36. })
  37. }
  38. }
  39. }
  40. </script>
  41. // nav-input.vue
  42. <script>
  43. export default {
  44. name: 'newproduct',
  45. inject:['reload'],
  46. method:{
  47. selectChange(){
  48. this.reload()
  49. }
  50. }
  51. </script>

原来这就是传说中的 依赖注入,控制反转

进阶,在根入口文件app.vue上挂载基本不会变的数据,比如用户的信息;给全局访问;

  1. <template>
  2. <div>
  3. <router-view></router-view>
  4. </div>
  5. </template>
  6. <script>
  7. export default {
  8. provide () {
  9. return {
  10. app: this
  11. }
  12. }
  13. data () {
  14. return {
  15. userInfo: null
  16. }
  17. },
  18. }
  19. </script>
  20. 全局数据多了可以拆分为 多个js 然后 mixin进来

$on 与 $emit

  1. <template>
  2. <div>
  3. <button @click="handleEmitEvent">触发自定义事件</button>
  4. </div>
  5. </template>
  6. <script>
  7. export default {
  8. methods: {
  9. handleEmitEvent () {
  10. // 在当前组件上触发自定义事件 test,并传值
  11. this.$emit('test', 'Hello Vue.js')
  12. }
  13. },
  14. mounted () {
  15. // 监听自定义事件 test
  16. this.$on('test', (text) => {
  17. window.alert(text);
  18. });
  19. }
  20. }
  21. </script>

自行实现dispatch / broadcast

功能:
事件触发与监听
===》 找到组件,让组件调用相关方法,并传入参数

要实现的 dispatch 和 broadcast 方法,将具有以下功能:

  • 在子组件调用 dispatch 方法,向上级指定的组件实例(最近的)上触发自定义事件,并传递数据,且该上级组件已预先通过 $on 监听了这个事件;
  • 相反,在父组件调用 broadcast 方法,向下级指定的组件实例(最近的)上触发自定义事件,并传递数据,且该下级组件已预先通过 $on 监听了这个事件。

如何实现?
实现这对方法的关键点在于准确地找到组件实例。那在寻找组件实例上,我们的“惯用伎俩”就是通过遍历来匹配组件的 name 选项,在独立组件(库)里,每个组件的 name 值应当是唯一的,name 主要用于递归组件

  1. function broadcast(componentName, eventName, params) {
  2. this.$children.forEach(child => {
  3. const name = child.$options.name;
  4. if (name === componentName) {
  5. child.$emit.apply(child, [eventName].concat(params));
  6. } else {
  7. broadcast.apply(child, [componentName, eventName].concat([params]));
  8. }
  9. });
  10. }
  11. export default {
  12. methods: {
  13. // 组件name,自定义事件,要传递的数据
  14. dispatch(componentName, eventName, params) {
  15. let parent = this.$parent || this.$root;
  16. let name = parent.$options.name;
  17. // 向上找
  18. while (parent && (!name || name !== componentName)) {
  19. parent = parent.$parent;
  20. if (parent) {
  21. name = parent.$options.name;
  22. }
  23. }
  24. if (parent) {
  25. parent.$emit.apply(parent, [eventName].concat(params));
  26. }
  27. },
  28. broadcast(componentName, eventName, params) {
  29. broadcast.call(this, componentName, eventName, params);
  30. }
  31. }
  32. };

使用

  1. <!-- A.vue -->
  2. <template>
  3. <button @click="handleClick">触发事件</button>
  4. </template>
  5. <script>
  6. import Emitter from '../mixins/emitter.js';
  7. export default {
  8. name: 'componentA',
  9. mixins: [ Emitter ],
  10. methods: {
  11. handleClick () {
  12. this.broadcast('componentB', 'on-message', 'Hello Vue.js');
  13. }
  14. }
  15. }
  16. </script>
  17. // B.vue
  18. export default {
  19. name: 'componentB',
  20. created () {
  21. this.$on('on-message', this.showMessage);
  22. },
  23. methods: {
  24. showMessage (text) {
  25. window.alert(text);
  26. }
  27. }
  28. }

实现的缺陷:

  • 需要额外传入组件的 name 作为第一个参数;
  • 无冒泡机制;
  • 第三个参数传递的数据,只能是一个(较多时可以传入一个对象),而 Vue.js 1.x 可以传入多个参数,当然,你对 emitter.js 稍作修改,也能支持传入多个参数,只是一般场景传入一个对象足以。

实现findComponents

可以说是组件通信的终极方案。findComponents 系列方法最终都是返回组件的实例,进而可以读取或调用该组件的数据和方法。

它适用于以下场景:

  • 由一个组件,向上找到最近的指定组件;
  • 由一个组件,向上找到所有的指定组件;
  • 由一个组件,向下找到最近的指定组件;
  • 由一个组件,向下找到所有指定的组件;
  • 由一个组件,找到指定组件的兄弟组件。

5 个不同的场景,对应 5 个不同的函数,实现原理也大同小异。
5 个函数的原理,都是通过递归、遍历,找到指定组件的 name 选项匹配的组件实例并返回。