Vue组件通信总结

作者: 禾小沐的技术与生活 | 来源:发表于2019-11-22 17:49 被阅读0次

能工摹形,巧匠窃意。必三省吾身,万不可怠惰因循。

foreword

  • 这篇容纳了我个人所知道的一些Vue 2.x组件通信的总结,之后3.x官网公布后会增加3.x的部分。(篇幅长,细节有那么一些些,熟知部分可以一眼略过).

start

一. props $emit $attrs $listeners $props

  • 之所以把$attrs/$listeners/$props 和props $emit 放在一起 是因为个人感觉,更加方便记忆。老项目使用$attrs $listeners $props这几个API需要看当时的vue版本是不是已经支持;
1.props 父组件向子组件传值

_ parent.vue

<template>
  <div>
    PARENT
    <children :stars="stars"></children>
  </div>
</template>

<script>
import children from './children/children';
export default {
  components:{children},
  data(){
    return {
      stars:[
        {name:"周杰伦",id:1},
        {name:"刘亦菲",id:2},
        {name:"胡歌",id:3},
        {name:"古天乐",id:4},
      ]
    }
  },
}
</script>

_ children.vue

<template>
   <div>
     CHILDREN
     <ul>
       <li v-for="star in stars" :key="star.id">{{star.name}}</li>
     </ul>
   </div>
</template>

<script>
export default {
  name:"children", 
  props:{
    stars:{
      type:Array,
      default(){
        return []
      },
      // required:true // 是否必须属性
      // type:Symbol, // 传入类型 type String Number Boolean Function Object Array Symbol 
      // type:CustormFn,// 可以是自定义构造函数,用instanceof 检测
      validator(V){  // 自定义验证函数
        return V.length > 2
      }
    }
  },
  created(){
    console.log(this.stars) //[{…}, {…}, {…}, {…}, __ob__: Observer]
  }
}
</script>
summarize: 父组件通过props传入到子组件. 子组件可以设定传入值的校验,等属性.组件中的数据方式共有 data,computed,props以及provide和inject(这个待商榷).
2. 子组件通过事件的形式向父组件传值

_ parent

<template>
  <div>
    <p>{{bestHandsome}}</p>
    <children @handleBs='handleBs'></children>
  </div>
</template>
<script>
import children from './children';
export default {
  name:'parent2',
  components:{children},
  data(){
    return {
      bestHandsome:'刘德华'
    }
  },
  methods:{
    handleBs(name){
      this.bestHandsome = name;
    }
  }
}
</script>

_ children

<template>
  <button @click="setBestHandsome('吴彦祖')">BUTTON</button>
</template>

<script>
export default {
  name:'children2',
  methods:{
    setBestHandsome(name){
      this.$emit('handleBs',name);
    }
  }
}
</script>
summarize:子组件通过events的形式改变父组件的值,实际上是调用传入参数父组件的方法,来改变父组件的值. 有部分程序员喜欢将 .sync 和v-model这两个语法糖也归为组件通信方式,这里不做归纳.详细请看官方文档.sync,v-model.
3. $attrs/$listeners/$props

官方解释

  • $props:当前组件接收到的 props 对象。Vue 实例代理了对其 props 对象属性的访问。类型(Object)
  • $attrs:包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。类型:{ [key: string]: string }(只读)
  • $listeners: 包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。类型: { [key: string]: Function | Array<Function> }(只读)
$props

_ code

// parent.vue
<template>
  <div>
    <children
      name='input'
      type='nmber'
      disabled
      autofocus
      placeholder='这是一个输入框'
    ></children>
  </div>
</template>
// children.vue
<template>
   <div>
     <input v-bind="$props">
   </div>
</template>

<script>
export default {
  name:"children", 
  props:['name','type','disabled','autofocus','placeholder'],
  mounted(){
    console.log(this.$props.name)// input
  }
}
</script>

_ view


view
html.png
  • 注意这里使用v-bind="$props"就会使得子组件中的input标签绑定上父组件中定义的props属性.
$attrs

_ code

// parent.vue
<template>
  <div>
    <children
      name='input'
      type='nmber'
      disabled
      autofocus
      placeholder='这是一个输入框'
    ></children>
  </div>
</template>
// children.vue
<template>
   <div>
     <input v-bind="$attrs">
   </div>
</template>

<script>
export default {
  inheritAttrs:false, // 将默认绑定根元素属性去掉
  name:"children", 
  props:['handsome'],
  mounted(){
    console.log(this.$attrs.name)// input
    console.log(this.$attrs.handsome)// undefined
    console.log(this.$props.handsome)// 1
  }
}
</script>
  • inheritAttrs

默认情况下父作用域的不被认作 props 的特性绑定 (attribute bindings) 将会“回退”且作为普通的 HTML 特性应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置 inheritAttrs 到 false,这些默认行为将会被去掉。而通过 (同样是 2.4 新增的) 实例属性 $attrs 可以让这些特性生效,且可以通过 v-bind 显性的绑定到非根元素上。

不设置inheritAttrs:false效果
设置inheritAttrs效果
$listeners

_ code

// parent.vue
<template>
  <div>
    <p>{{ handsome }}</p>
    <children
      @changeHandsome="changeHandsome"
      @clearHandsome="clearHandsome"
      @resetHandsome="resetHandsome"
    ></children>
  </div>
</template>

<script>
import children from './children/children';
export default {
  components:{children},
  data(){
    return {
      handsome:'lin'
    }
  },
  methods:{
    changeHandsome(name){
      this.handsome = name;
    },
    clearHandsome(){
      this.handsome = '';
    },
    resetHandsome(){
      this.handsome = 'lin';
    },
  }
}
</script>
// children.vue
<template>
   <div>
     <g-children v-on="$listeners"></g-children>
   </div>
</template>
<script>
import gChildren from './grandchildren'
export default {
  name:"children", 
  components:{gChildren},
  mounted(){
    console.log(this.$listeners)
  }
}
</script>
// grandchildren.vue
<template>
  <div>
    <button @click="$emit('changeHandsome','zhou')">set Zhou</button>
    <button @click="$emit('clearHandsome')">clear</button>
    <button @click="$emit('resetHandsome')">reset</button>
  </div>
</template>
以上这些实际上是父子组件直接直接或者间接通过vue提供的通信方式通信.

二. $refs $parent $children $root

  • 官方解释

$refs:一个对象,持有注册过 [ref 特性] 的所有 DOM 元素和组件实例。
$parent:父实例,如果当前实例有的话。(类型:Vue instance)
$children:当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的。如果你发现自己正在尝试使用 $children 来进行数据绑定,考虑使用一个数组配合 v-for 来生成子组件,并且使用 Array 作为真正的来源。(类型:Array)
$root:当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。

$refs

_ code

//children.vue
<script>
export default {
  name: "children",
  data() {
    return {
      name: "xiaoerlang",
      age: 18
    };
  }
};
</script>
// parent.vue
<template>
  <div>
    <children ref="children"></children>
    <button @click="setChildrenData">button</button>
  </div>
</template>
<script>
import children from "./children/children";
export default {
  components: { children },
  methods: {
    setChildrenData() {
      console.log(this.$refs.children.name); //第一次点击按钮的时候打印 xiaolang
      this.$refs.children.name = "xiaoming";
      console.log(this.$refs.children.name); //第一次点击按钮的时候打印 xiaoming
    }
  }
};
</script>
$parent
// parent.vue
<template>
  <div>
    {{name}}
    <children></children>
  </div>
</template>
<script>
import children from "./children/children";
export default {
  components: { children },
  data(){
    return {
      name:'liu'
    }
  },
};
</script>
// children.vue
<template>
  <div>
    <button @click="setParentName('fei')">button</button>
  </div>
</template>
<script>
export default {
  name: "children",
  methods:{
    setParentName(name){
      this.$parent.name = name;
    }
  }
};
</script>
$children
// parent.vue
<template>
  <div>
    <children></children>
    <button @click="setChildrenName('yi')">button</button>
  </div>
</template>
<script>
import children from "./children/children";
export default {
  components: { children },
  methods:{
    setChildrenName(name){
      this.$children[0].name = name;
    }
  }
};
</script>
// children.vue
<template>
  <div>
    {{name}}
  </div>
</template>
<script>
export default {
  name: "children",
  data() {
    return {
      name: "xiaoerlang",
    };
  },
};
</script>
$root:这里与$parent类似,是当前组件树的根实例.

附加:使用$parent或者$root配合$on和$emit可以 进行兄弟组件之间通信

// parent.vue
<template>
  <div>
    <bother1></bother1>
    <bother2></bother2>
  </div>
</template>
<script>
import bother1 from './children/brother1';
import bother2 from './children/brother2';
export default {
  components: { bother1,bother2 },
};
</script>
// bother2.vue
<template>
  <div>{{name}}</div>
</template>
<script>
export default {
  name:'brother2',
  data(){
    return{
      name:'zhouxiaolun'
    }
  },
  created(){
    this.$parent.$on('setB2',this.setName)
  },
  methods:{
    setName(name){
      this.name = name;
    }
  }
}
</script>

// bother1.vue
<template>
  <button @click="setB2Name('zhoujielun')">button</button>
</template>

<script>
export default {
  name:'brother1',
  methods:{
    setB2Name(name){
      this.$parent.$emit('setB2',name)
    }
  }
}
</script>
summarize
  • 注意这里$children 格式为数组,如果没有就是空数组,但是这里的数组顺序与页面顺序是不对应的,这里涉及到了虚拟dom挂载.
  • 上面的部分情况其实是拿到对应的组件的实例,相当于在对应vue组件中调用this.xx = 'xxxx';
  • 实际开发中,非自定义组件,或者真实需要,不建议使用$parent和$children $root进行组件之间的通信.

三. provide/inject

  • provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。provide/inject能够实现祖先和后代之间传值.
// 祖先组件
export default {
  provide() {
    const that = this;
    return {
      foo: "foo",
      forefathersThis: that
    };
  },
  name: "parent",
  components: { children }
};
// 后代组件
export default {
  name: "children",
  inject: ["foo", "forefathersThis"],
  created() {
    console.log(this.foo);
    console.log(this.forefathersThis); // 祖先组件的实例
  }
};
  • 提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。这里也可以传入this到后代组件中,但实际开发中不推荐使用,可以用于开发高阶组件或者组件库.

四.事件总线eventBus方式(自定义Bus类,或者使用Vue代替);

// Bus 类
class Bus {
  constructor() {
    this.CB = {};
  }
  // 监听
  $on(name, fn) {
    this.CB[name] = this.CB[name] || [];
    this.CB[name].push(fn)
  }
  // 派发
  $emit(name, args) {
    this.CB[name] && this.CB[name].forEach(cb => cb(args))
  }
}
export default Bus;
// main.js
import Bus from './eventBus';
Vue.prototype.$bus = new Bus();
// 组件1
 methods: {
    setBH2Name() {
      this.$bus.$emit("setB2", "zhoujielun");
    }
  }
// 组件2 
  created() {
    this.$bus.$on("setB2", this.setName);
  },
  methods: {
    setName(name) {
      this.name = name;
    }
  }
summarize:
  • 如果不使用自定义方式,也可以Vue.prototype.$bus = new Vue(); vue内部已经做了具体处理.并且提供$once只监听一次这个事件,$off(name)移除name事件监听,$off() 移除所有事件监听.
  • 这里主要说Vue通信方式,所以关于上部分需要在destroy生命周期需要注销监听等操作都未列出,实际开发实际需求.

五.Vuex

  • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,实际上是把一些需要多处用到的状态放在同一个对象中.
  • 小demo
// store
  state: {
    infoName: "handsome"
  },
  mutations: {
    setInfoName(state, payload) {
      state.infoName = payload;
    }
  }
// 组件1
<script>
import { mapMutations } from "vuex";
import bother2 from "./children/brother2";
export default {
  name: "parent",
  components: { bother2 },
  methods: {
    ...mapMutations(["setInfoName"]),
    setInfo() {
      const name = "ugly";
      this.setInfoName(name);
    }
  }
};
</script>
// 组件2
<script>
import { mapState } from "vuex";
export default {
  name: "brother2",
  computed: {
    ...mapState({
      infoName: s => s.infoName
    })
  }
};
</script>

summarize: Vuex相对来说比redux简单一些,详细可以参考中文官网Vuex中文官网

六. 自定义broadcast/dispatch

  • vue 1.x 版本中有两个API $dipatch,$broadcast,$broadcast和$dispatch 这两个API在2.x版本中去除. 实际上我们经常写一些自定义组件库,或者高阶组件的时候可能会用到.
    vue 1.x解释

$dispatch:向上级派发事件,祖辈组件中$on监听到
$broadcast:与$dispatch相反,向下级广播事件.

  • 自定义代码实现功能.
/**
 * @param {*} componentName  // 组件名
 * @param {*} eName  // 自定义事件名称
 * @param {*} params // 传递参数数据
 */
export function broadcast(componentName, eName, params) {
  this.$children.forEach(child => {
    const name = child.$options.name;
    if (name === componentName) {
      // 调用子组件emit
      child.$emit.bind(child)(eName, params)
    } else {
      // 递归调用
      broadcast.bind(child)(componentName, eName, params)
    }
  })
};
/**
 * @param {*} componentName  // 组件名
 * @param {*} eName  // 自定义事件名称
 * @param {*} params // 传递参数数据
 */
export function dispatch(componentName, eName, 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.bind(parent)(eName, params)
}
解析
  1. this.$options.xx 可以取到vue组件中export default暴露的对象的对应xx属性值.我们一帮用来取一些静态属性.例如 组件的name值,判断是哪个组件.
  2. 我们找到对应的子组件或者父组件,然后用$emit调用,实际上就相当于我们在对应的组件A中用this.$emit(xxx)调用其在当前组件A中created生命周期中$on监听的事件.
  3. 实际的逻辑就是找到对应组件实例, 组件实例$emit 自己本身$on监听的事件.
  • 引入 main.js
import { broadcast, dispatch } from './dispatch-broadcast';
Vue.prototype.$dispatch = dispatch;
Vue.prototype.$broadcast = broadcast;
  • 实例引用.
  1. $dispatch 派发
//  后代
<template>
  <div><button @click="setParentDay('Sat')">button</button></div>
</template>
<script>
export default {
  name: "children",
  methods: {
    setParentDay(day) {
      this.$dispatch("parent", "setDay", day);
    }
  }
};
</script>
//  祖先
<div>
    <children></children>
    <p>{{ day }}</p>
  </div>
</template>
<script>
import children from "./children/children";
export default {
  name: "parent",
  components: { children },
  data() {
    return { day: "Fir" };
  },
  created() {
    this.$on("setDay", this.setDay);
  },
  methods: {
    setDay(day) {
      this.day = day;
    }
  }
};
</script>
  1. $broadcast 广播
// 祖先
<template>
  <div>
    <children></children>
    <button @click="setChildrenDay('Fir')">button</button>
  </div>
</template>
<script>
import children from "./children/children";
export default {
  name: "parent",
  components: { children },
  methods: {
    setChildrenDay(day) {
      this.$broadcast("children", "setDay", day);
    }
  },
};
</script>
// --------- 后代 --------------
<template>
  <div>{{ day }}</div>
</template>
<script>
export default {
  name: "children",
  data() {
    return {
      day: "Sat"
    };
  },
  created() {
    this.$on("setDay", this.setDay);
  },
  methods: {
    setDay(day) {
      this.day = day;
    }
  }
};
</script>

七. 自定义findComponents多个方法

  • 就像上面说的,其实我们寻找到了对应组件的实例,就可以用这个实例进行操作,就可以说进行了组件的通信.那么这里就存在几个问题. (注意这里的前提是组件中name的属性设置严格按照规范),这些方法一般在我们自定义组件库,或者定义一些高阶组件用来使用.
提出问题.
  1. 如何由一个组件向上找到第一个最近的指定组件?
  2. 如何由一个组件向上找到所有的指定组件?
  3. 如何由一个组件向下找到最近的指定组件?
  4. 如何由一个组件向下找到所有的指定组件?
  5. 如何由一个组件找到指定的兄弟组件?
分析:
  • 利用$options.name $children $parent , 参数包含当前组件的this,要找到的组件名name. 通过$options.name确定寻找的组件.
1. 由一个组件向上找到第一个最近的指定组件.
/**
 * @param {*} context  执行上下文,这里一般传 this
 * @param {*} componentName 要找到的组件名 name
 * @returns
 */
function findComponentUpwrad(context, componentName) {
  let parent = context.$parent;
  let { name } = parent.$options;

  while (parent && (!name || [componentName].indexOf(name) < 0)) {
    parent = parent.$parent;
    if (parent) name = parent.$options.name;
  }
  return parent;
}
2. 由一个组件向上找到所有的指定组件
/**
 * @param {*} context  执行上下文,这里一般传 this
 * @param {*} componentName 要找到的组件名 name
 */
function findComponentsUpward(context, componentName) {
  const parents = [];
  const parent = context.$parent;
  if (parent) {
    if (parent.$options.name === componentName) parents.push(parent);
    return parents.concat(findComponentUpwrad(parent, componentName));
  }
  return [];
}
3. 由一个组件向下找到最近的指定组件
/**
 *@description 向下找到最近的指定组件
 *
 * @context {*} context 执行上下文,这里一般传 this
 * @componentName {*} componentName 要找到的组件名 name
 */
function findComponentDownward(context, componentName) {
  const childrens = context.$children;
  let children = null;
  if (childrens.length) {
    for (const child of childrens) {
      const { name } = child.$options;
      if (name === componentName) {
        children = child;
        break;
      } else {
        children = findComponentDownward(child, componentName);
        if (children) break;
      }
    }
  }
  return children;
}
4. 由一个组件向下找到所有的指定组件
/**
 * @context {*} context 执行上下文,这里一般传 this
 * @componentName {*} componentName 要找到的组件名 name
 */
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);
  }, []);
}
5. 由一个组件找到指定的兄弟组件
/**
 * @context {*} context 执行上下文,这里一般传 this
 * @componentName {*} componentName 要找到的组件名 name
 * @exceptMe  {Boolean}  是否包含本身
 * @description2 Vue.js 在渲染组件时,都会给每个组件加一个内置的属性 _uid,这个 * * *_uid 是不会重复的,
 */
function findBrothersComponents(context, componentName, exceptMe) {
  const res = context.$parent.$children.filter(item => item.$options.name === componentName);
  const index = res.findIndex(item => item._uid === context._uid);
  if (exceptMe) res.splice(index, 1);
  return res;
}
找到组件后就等于找到组件中的this,之后通信的方式就可以很随意了,当然这里使用方式一般是存在特殊情况下,正常我们组件之间的通信使用Vuex 或者 props $emit 就可以了.
代码参考 iview源码 具体位置在 iview assets.js,有兴趣的朋友可以查看源码.

总结

  • Vue组件之间的通信,当然可能还有更多,这里容纳了大部分,当然可能还有其他一些.

好学而不勤问非真好学者. 如果有帮助请点上一个赞,如果由疑问,请评论留言.

相关文章

网友评论

    本文标题:Vue组件通信总结

    本文链接:https://www.haomeiwen.com/subject/ywmzictx.html