likes
comments
collection
share

“细狗”玩转vue组件之间通信的各种姿势!

作者站长头像
站长
· 阅读数 6

前言

最近在网上发现面试官都很喜欢问vue组件间的通信方式有哪些,问问应届生也就算了,1年工作经验也问,勉勉强强那也还说得过去吧!到了3年经验的你还问啊?算了,问就问吧,可能3年也有些水分。到了5年经验,人家都要求15k+的人了,你还问啊?好好好!这么玩是吧,那就一次给你说完吧!

父子组件双向通信之props/emit

下面我将通过给自定义组件使用v-model.sync修饰符,展示父子组件间如何通过的双向数据通信,并且还能顺便复习一下v-model.sync是如何实现双向数据绑定。

1.v-model(vue2.2.0+)

v-model默认用法

在子组件上绑定v-model,子组件内通过propsvalue属性接收值,并通过触发$emit('input', newValue)更新父组件中的值,实现数据双向绑定。

Parent.vue

<Child v-model="name"></Child>

Child.vue

<template>
  <div class="child">
    <input :value="value" @input="$emit('input', $event.target.value)">
  </div>
</template>

<script>
export default {
  props: {
    value: String
  }
}
</script>

自定义接收v-model的属性名及触发事件名

当子组件已经使用了propsvalue属性作为其他用途,这时你再使用v-model时,就可以自定义接收v-model值的属性名,我们可以通过和props同级的model属性配置相关信息。下面的例子展示了其用法:

Parent.vue

<template>
  <div id="app">
    <Child v-model="name" :value="age"></Child>
  </div>
</template>

<script>
import Child from './components/Child.vue'
export default {
  components: {
    Child
  },
  data() {
    return {
      name: '蔡徐坤',
      age: 18
    }
  }
}
</script>

Child.vue

<template>
  <div class="child">
    年龄:{{ value }}
    姓名:<input :value="vModelValue" @input="$emit('valueChange', $event.target.value)">
  </div>
</template>

<script>
export default {
  model: {
    // 自定义props中接收v-model的属性名
    prop: 'vModelValue',
    // 自定义触发更新父组件v-model值的事件名
    event: 'valueChange'
  },
  props: {
    value: Number,
    // v-model传递的值和model配置的名称一致
    vModelValue: String
  }
}
</script>

2.修饰符.sync(vue2.3.0+)

父组件向子组件传递props属性时,可在属性名后增加.sync修饰符,如:name.sync="name",子组件可通过触发$emit(upadte:name, newValue)事件更新父组件中的值。

看到这是不是觉得.sync特别像vue3当中的v-model:name="name"用法。没错,就是一样的用法!

Parent.vue

<template>
  <div id="app">
    <Child :name.sync="name"></Child>
  </div>
</template>

Child.vue

<template>
  <div class="child">
    姓名:<input :value="name" @input="$emit('update:name', $event.target.value)">
  </div>
</template>

<script>
export default {
  props: {
    name: String
  }
}
</script>

$attrs/$listeners(2.4.0+)跨组件双向通信

这两个api可以实现透传,在创建高级别的组件时非常有用。比如我们现在再来一个孙子组件,而我们的儿子组件特别懒,它让父亲有什么事情直接和孙子说。这时我们就可使用透传,实现跨组件通信

Parent.vue

<template>
  <div class="parent">
    <Son :money="money" @cb="cb"></Son>
  </div>
</template>

<script>
import Son from './components/Son.vue'
export default {
  components: {
    Son
  },
  data() {
    return {
      money: '100万'
    }
  },
  methods: {
    // 孙子告诉爷爷收到了多少钱,防止被中间商赚差价
    cb(money) {
      console.log('孙子收到了:', money)
    }
  },
}
</script>

Son.vue

<template>
  <div class="son">
    <Grandson v-bind="$attrs" v-on="$listeners"></Grandson>
  </div>
</template>

<script>
import Grandson from './Grandson.vue'
export default {
  components: {
    Grandson
  }
}
</script>

Grandson.vue

<template>
  <div class='grandson'>
    太开心了 ,爷爷直接给了我{{money}}!
    <button @click="$emit('cb', money)">点击,通知爷爷收到多少钱!</button>
  </div>
</template>

<script>
export default {
  props: {
    money: String
  }
}
</script>

这样我们就轻松实现了一个跨组件的双向通信

$refs

$refs是一个对象,持有注册过 ref attribute 的所有 DOM 元素和组件实例。

我们可以给子组件绑定一个ref名称,就能够通过this.$refs[ref名称],获取到对应的组件实例。从而能够操作子组件中的属性或方法。

Parent.vue

<Son ref="Son"></Son>

Son.vue

<Grandson ref="Grandson"></Grandson>

以上爷爷给儿子挂了ref,儿子又给孙子挂了ref,这时爷爷也可以通过儿子的ref间接获取到孙子的实例(this.$refs.Son.$refs.Grandson)。

所以我们也可看出$refs也能实现跨组件的通信

$chidren/$parent/$root

$chidren:当前实例的直接子组件。

$parent:父实例,如果当前实例有的话。

$root:当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。

我们还是拿上面的Parent.vueSon.vueGrandson.vue三个组件来讲。

Parent.vue

<template>
  <div class="parent">
    <Son ref="Son"></Son>
  </div>
</template>
<script>
...
export default {
  ...
  mounted() {
    console.log(this.$children) // [Son实例]
    console.log(this.$refs.Son === this.$children[0]) // true
    console.log(this.$parent) // Parent实例
    console.log(this.$root) //  Parent实例
    console.log(this === this.$parent)  // false
    console.log(this === this.$root)  // false
    console.log(this.$el === this.$parent.$el)  // true
    console.log(this.$el === this.$root.$el)  // true
    console.log(this.name) // 爷爷
    console.log(this.$parent.name) // undefined
    console.log(this.$root.name) // undefined
  }
}
</script>

以上我们能够通过this.$children获取到子组件的实例,也就是和$refs获取到的实例是一样的,这样我们就能够建立与祖孙组件之间的通信。这里有一个注意点是,当组件自身没有父实例时,$parent$root返回的是自身实例,但又不完全相等。

Son.vue

<template>
  <div class="son">
    <Grandson ref="Grandson"></Grandson>
  </div>
</template>
<script>
export default {
  ...
  mounted() {
    console.log(this.$children) // [Grandson实例]
    console.log(this.$parent) // Parent实例
    console.log(this.$root) // Parent实例
  }
}
</script>

Grandson.vue

<script>
export default {
  mounted() {
    console.log(this.$children) // []
    console.log(this.$parent) // Son实例
    console.log(this.$root) // Parent实例
  }
}
</script>

以上我们看出结合$chidren/$parent/$root三者也可以进行跨组件通信,原理和$refs基本一致。

$slot插槽

没想到吧!插槽也能实现跨组件通信,我们下面就来实现一个

Parent.vue

向子组件传入插槽

<template>
  <div class="parent">
    <Son ref="Son">
      <template #default="{ data }">
        <button @click="printName(data.name)">输出孙子名字</button>
        <div>
          {{ data.name }}
        </div>
      </template>
    </Son>
  </div>
</template>

<script>
import Son from "./components/Son.vue";
export default {
  components: {
    Son,
  },
  methods: {
    printName(name) {
      console.log(name);  // 孙子
    }
  }
};
</script>

Son.vue

把父组件的插槽继续传入下级子组件

<template>
  <div class="son">
    <Grandson ref="Grandson">
      <template #default="props">
        <slot v-bind="props"></slot>
      </template>
    </Grandson>
  </div>
</template>

<script>
import Grandson from './Grandson.vue'
export default {
  components: {
    Grandson
  }
}
</script>

Grandson.vue

把组件自身实例name属性通过插槽的data属性传递出去,上级组件可通过插槽名称<template default="{data}">佬来解构data属性。

<template>
  <div class='grandson'>
    <slot :data="{name}"></slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      name: '孙子'
    }
  }
}
</script>

以上我们向子组件注入了一个插槽,并且在这个插槽上绑定了一个父组件的事件来获取插槽内传递出来的数据,在子组件中又将这个插槽顺势传递到了孙子组件,这样进行爷孙俩的跨组件通信了!

provide/inject

这两个api需要搭配使用,使用provide可以向下级的所有组件提供依赖。在下级组件中,可通过inject注入由所有上层组件通过provide提供的依赖。但是需要注意,它们之间的绑定并不是响应式的,但是我们可以通过传入一个响应式对象,使它们变成可响应的。

provide:一个对象或者返回一个对象的函数

inject:字符串数组或者对象

普通用法:

// 祖先组件
provide: {
  name: '哈哈哈'
}

// 下级组件
inject: ['name']

provide使用对象的方式提供不能获取到this实例,提供响应式对象。

进阶用法

// 祖先组件
provide() {
  return {
    name: this.name,
  };
},
data() {
  return {
    name: "爷爷",
  };
},

// 下级组件
inject: {
  // 别名
  foo: {
    // 接收provide提供的name属性值
    from: 'name',
    // 默认值
    default: '儿子'
  }
}

这样子访问到this实例了,但是还有一个问题是name并不是响应式的,当name值改变时,子孙组件并不会触发更新。这时我们可以通过在把name包裹在一个对象里,再把这个对象传递下去就可实现响应式了。

// 祖先组件
provide() {
  return {
    name: this.provideData
  };
},
data() {
  return {
    provideData: {
      name: '爷爷'
    }
  };
},

高级用法

除了传递以上响应式对象外,还可以通过提供一个get方法来获取祖先组件对应的属性值,不用包装对象也能做到响应式更新。因为通过provide通过的方法会默认绑定当前组件实例的this,就算没有绑定,我们也能通过方法.bind(this)来手动绑定。

// 祖先组件
provide() {
  return {
    getName: this.getName
  };
},
data() {
  return {
    name: '爷爷',
  };
},
methods() {
  getName() {
    return this.name
  }
}

// 下级组件
inject: {
  getName: {
    default: () => (() => {})
  }
}

eventBus事件

eventBus就是我们所称的事件总线,使用方式是新建一个Vue实例并导出,可在任何组件当中导入,实现跨组件通信。

eventBus.js

import Vue from 'vue'
export const eventBus = new Vue()

在组件当中使用

// 监听事件
eventBus.$on('change', (a, b) => {
  console.log(a, b)
})

// 触发事件
eventBus.$emit('change', 1, 2)

// 移除事件
eventBus.$off('change', this.onChange)

// 移除所有事件
eventBus.$off()

注意:记得事件监听使用后或者组件销毁前手动移除事件,否则事件监听将一直存在。

大杂烩通信

除了以上vue内置的组件间通信方式外,还可以通过全局状态管理插件,如vuex/pinia实现跨组件通信,当然还有本地缓存方式,如:localstorage/sessionStorage等,好了这个问题就讲这么多了,面试结束了吗?

面试官:一个问题讲这么久?说完了吗?回去等通知吧!

结语

大家还有哪些手段,尽管在评论区使出来吧!