Vue组件通信

Vue的组件是其非常重要的系统,组件之间的通信也是开发中不可避免的需求

一般来说Vue组件是以下几种关系

《Vue组件通信》

A组件和B组件、B组件和C组件、B组件和D组件是父子关系,C组件和D组件是兄弟关系,A组件和C/D组件是隔代关系。

本文阐述了几种常用的通信方式和使用场景

props&&emit

父组件通过 props 传递数据给子组件,子组件通过 emit 发送事件传递数据给父组件

这种父子通信方式也就是典型的单向数据流,父组件通过 props 传递数据,子组件不能直接修改 props , 而是必须通过发送事件的方式告知父组件修改数据。

// 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>

优点:易于使用,结构清晰

缺点:只能用于父子组件通信

ref&&$parent / $children

这两种都是直接得到组件实例,使⽤后可以直接调⽤组件的⽅法或访问数据

  1. ref:给元素或组件注册引⽤信息
  2. $parent/$children:访问父/子组件

ref

// 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

$parent$children都是基于当前上下文访问父组件和子组件

// 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相同,弊端都是无法在跨级和兄弟间通信

provide/inject

ref$parent/$children在跨级通信中有一定的弊端。
Vue.js 2.2.0 版本后新增 provide / inject API
vue文档
这对选项需要⼀起使⽤,以允许⼀个祖先组件向其所有⼦孙后代注⼊⼀个依赖,不论组件层次有多深,并在起上下游关系成⽴的时间⾥始终⽣效

provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性。

inject 选项应该是:

  1. 一个字符串数组
  2. 一个对象,对象的 key 是本地的绑定名,value 是:

    • 在可用的注入内容中搜索用的 key (字符串或 Symbol),或
    • 一个对象,该对象的:

      • from 属性是在可用的注入内容中搜索用的 key (字符串或 Symbol)
      • default 属性是降级情况下使用的 value

provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

// 父级组件提供 'foo'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// 子组件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}

模拟Vuex

在做 Vue ⼤型项⽬时,可以使⽤ Vuex 做状态管理。使用 provide / inject,可以模拟 达到 Vuex 的效果 。
使⽤ Vuex,最主要的⽬的是跨组件通信、全局数据维护。⽐如⽤户的登录信息维护、通知信息维护等全局的状态和数据

通常vue应用都有一个根根组件app.vue,可以⽤来存储所有需要的全局数据和状态,methods 等。项目中所有的组件其父组件都是app,通过provideapp实例暴露对外提供

<template>
  <div>
    <router-view></router-view>
  </div>
</template>
<script>
  export default {
    provide () {
      return {
       app: this
      }
    }
  }
</script>

接下来任何组件只要通过 inject 注⼊ app.vueapp 的话,都可以直接通过this.app.xxx 来访问 app.vuedata、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

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。当需要开发开发大型单页应用(SPA),就应该考虑使用Vuex了,它能把组件的共享状态抽取出来,当做一个全局单例模式进行管理。这样不管你在何处改变状态,都会通知使用该状态的组件做出相应修改。
Vuex官方文档已经给出了详细的使用方式

优点:

  • 官方集成管理库,可以处理各种场景的通信和状态管理

缺点:

  • 需要额外引入管理库

Bus

如果不是大型项目,状态管理不复杂,数据量不是很大,没有必要使用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

$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

自行模拟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 ⽅法来触发⾃定义事件 eventNamebroadcast ⽅法与之类似,只不过是向下遍历寻找

优点:

  • 使用简单
  • 可以实现跨级通信

缺点:

  • 原生支持已经废除,需要自行实现

findComponents系列

上述介绍的各种通信方法都有各自的局限性,我们可以实现一个 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,也就是⽗组件的全部⼦组件,这⾥⾯当前包含了本身,所有也会有第三个参数exceptMeVue.js 在渲染组件时,都会给每个组件加⼀个内置的属性 _uid,这个 _uid 是不会重复的,借此我们可以从⼀系列兄弟组件中把⾃⼰排除掉。

    原文作者:刘小球
    原文地址: https://segmentfault.com/a/1190000018896108
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞