Vue组件通讯

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

通常来讲Vue组件是如下几种关系ajax

Vue组件

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

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

props&&emit

父组件经过 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

缺点:只能用于父子组件通讯函数

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都是基于当前上下文访问父组件和子组件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相同,弊端都是没法在跨级和兄弟间通讯

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 是不会重复的,借此咱们能够从⼀系列兄弟组件中把⾃⼰排除掉。

相关文章
相关标签/搜索