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

前言

最近在网上发现面试官都很喜欢问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

js 复制代码
<Child v-model="name"></Child>

Child.vue

js 复制代码
<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

js 复制代码
<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

js 复制代码
<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

js 复制代码
<template>
  <div id="app">
    <Child :name.sync="name"></Child>
  </div>
</template>

Child.vue

js 复制代码
<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

js 复制代码
<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

js 复制代码
<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

js 复制代码
<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

js 复制代码
<Son ref="Son"></Son>

Son.vue

js 复制代码
<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

js 复制代码
<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

js 复制代码
<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

js 复制代码
<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

向子组件传入插槽

js 复制代码
<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

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

js 复制代码
<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属性。

js 复制代码
<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:字符串数组或者对象

普通用法:

js 复制代码
// 祖先组件
provide: {
  name: '哈哈哈'
}

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

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

进阶用法

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

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

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

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

高级用法

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

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

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

eventBus事件

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

eventBus.js

js 复制代码
import Vue from 'vue'
export const eventBus = new Vue()

在组件当中使用

js 复制代码
// 监听事件
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等,好了这个问题就讲这么多了,面试结束了吗?

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

结语

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

相关推荐
昨天今天明天好多天几秒前
【Node.js]
前端·node.js
亿牛云爬虫专家11 分钟前
Puppeteer教程:使用CSS选择器点击和爬取动态数据
javascript·css·爬虫·爬虫代理·puppeteer·代理ip
2401_8576100323 分钟前
深入探索React合成事件(SyntheticEvent):跨浏览器的事件处理利器
前端·javascript·react.js
_xaboy26 分钟前
开源项目低代码表单设计器FcDesigner扩展自定义的容器组件.例如col
vue.js·低代码·开源·动态表单·formcreate·低代码表单·可视化表单设计器
_xaboy26 分钟前
开源项目低代码表单设计器FcDesigner扩展自定义组件
vue.js·低代码·开源·动态表单·formcreate·可视化表单设计器
雾散声声慢35 分钟前
前端开发中怎么把链接转为二维码并展示?
前端
熊的猫35 分钟前
DOM 规范 — MutationObserver 接口
前端·javascript·chrome·webpack·前端框架·node.js·ecmascript
天农学子36 分钟前
Easyui ComboBox 数据加载完成之后过滤数据
前端·javascript·easyui
mez_Blog37 分钟前
Vue之插槽(slot)
前端·javascript·vue.js·前端框架·插槽
爱睡D小猪39 分钟前
vue文本高亮处理
前端·javascript·vue.js