目录

1. 父子组件传值

1.1 父组件给子组件传值 —— v-bind

1.2 子组件接收父组件的传值 —— defineProps

1.3 设置子组件接受参数的默认值 —— withDefaults

1.4 子组件给父组件传参(派发事件) —— defineEmits

1.5 子组件暴露给父组件内部属性 —— defineExpose

2. 插槽

2.1 什么是插槽

2.2 匿名插槽、具名插槽

2.3 作用域插槽

2.4 动态插槽

3. 提供/注入

3.1 提供/注入是什么

3.2 父组件暴露(提供)数据

3.2.1 provide 基本用法

3.2.2 provide 源码解析

3.3 子组件接收(注入)数据

3.3.1 inject 基本用法

3.3.2 inject 源码解析

4. 兄弟组件传值

4.1 EventBus 原理

4.1.1 使用 ts 实现一个简单的 EventBus

4.1.2 使用 4.1.1 的 EventBus

4.2 Mitt

4.2.1 安装 mitt

4.2.2 新建 mitt-bus.ts

4.2.3 使用 mitt 发送事件

4.2.4 使用 mitt 接收事件

4.2.5 在全局实例上使用 mitt


1. 父子组件传值

1.1 父组件给子组件传值 —— v-bind

传递字符串类型,不需要加 v-bind(:)

传递非字符串类型,需要加 v-bind(:)

<template>
  <div class="layout">
    <Menu :data="data" title="我是标题"></Menu>
    <div class="layout-right">
      <Header></Header>
      <Content></Content>
    </div>
  </div>
</template>

<script setup lang="ts">
import Menu from "./Menu/index.vue";
import Header from "./Header/index.vue";
import Content from "./Content/index.vue";
import { reactive } from "vue";

const data = reactive<number[]>([1, 2, 3]);
</script>

1.2 子组件接收父组件的传值 —— defineProps

如果是在 setup 语法糖中使用 defineProps,则无需引入,直接使用即可

使用了 ts:defineProps<type>();

未使用 ts:defineProps({});

// 使用了 ts 时的写法
<script setup lang="ts">
defineProps<{
  title: string;
  data: number[];
}>();
</script>

// 未使用 ts 时的写法
<script setup>
defineProps({
  title: {
    default: "",
    type: string,
  },
  data: Array,
});
</script>

1.3 设置子组件接受参数的默认值 —— withDefaults

withDefaults 接受两个参数:

  • defineProps<Props>()
  • 一个对象(里面包含了所有默认值)

注意:

  • 仅当使用 ts 时,才可以使用 withDefaults
  • 设置默认值时,如果是引用类型,则要使用 () => {}、() => [] 的形式设置
type Props = {
  title?: string;
  data?: number[];
};

withDefaults(defineProps<Props>(), {
  title: "bilibili",
  data: () => [1, 2, 3],
});

1.4 子组件给父组件传参(派发事件) —— defineEmits

defineEmits 接受一个数组,用于存储 事件名 数组

defineEmits 返回一个函数,也就是传说中的 emit

子组件定义派发事件:

<template>
  <div class="menu">
    <button @click="clickTap">派发给父组件</button>
  </div>
</template>

<script setup lang="ts">
import { reactive } from "vue";
const list = reactive<number[]>([4, 5, 6]);

const emit = defineEmits(["on-click"]);
const clickTap = () => {
  emit("on-click", list);
};
</script>

父组件接受/监听子组件派发的事件:

<template>
  <div class="layout">
    <Menu @on-click="getList"></Menu>
  </div>
</template>

<script setup lang="ts">
import Menu from "./Menu/index.vue";
import { reactive } from "vue";

const getList = (list: number[]) => {
  console.log(list, "父组件接受子组件");
};
</script>

1.5 子组件暴露给父组件内部属性 —— defineExpose

在父组件中,获取子组件的 DOM

<Menu ref="menus"></Menu>
const menus = ref<HTMLElement | null>(null);
console.log(menus.value);

打印之后,发现没有任何属性

父组件若想读取子组件内部的属性,需要在子组件内 把需要的属性 通过 defineExpose 暴露出去

const list = reactive<number[]>([4, 5, 6]);

defineExpose({
  list,
});

这样做的目的:让父组件不能随意通过 DOM 修改子组件内的属性,父组件只能读取到子组件主动暴露出来的属性

2. 插槽

2.1 什么是插槽

子组件提供给父组件使用的占位符,用 <slot></slot> 表示

父组件可以使用任意代码填充 templete,填充的内容会替换子组件中的 <slot></slot>

插槽 | Vue3中文文档 - vuejsVue.js - The 渐进式 JavaScript 框架, Vue3中文文档 - Vue3最新动态https://www.javascriptc.com/vue3js/guide/component-slots.html#%E6%8F%92%E6%A7%BD%E5%86%85%E5%AE%B9

2.2 匿名插槽、具名插槽

具名插槽:有 name,父组件通过 #name 获取

匿名插槽:没有 name,父组件通过 #default 获取

<!-- 子组件 -->
<template>
  <div>
    <!-- 具名插槽 -->
    <slot name="header"></slot>
    <!-- 匿名插槽 -->
    <slot></slot>
    <slot name="footer"></slot>
  </div>
</template>

<!-- 父组件 -->
<Dialog>
  <!-- 简写 #header ;非简写 v-slot:header -->
  <template v-slot:header>
    <div>1</div>
  </template>
  <!-- 简写 #default ;非简写 v-slot -->
  <template #default>
    <div>2</div>
  </template>
  <template #footer>
    <div>3</div>
  </template>
</Dialog>

2.3 作用域插槽

子组件:给 slot 标签,动态绑定参数,派发给父组件使用

父组件:在 template 中使用解构赋值 #default="{ data, index }",获取子组件中的值

<!-- 子组件 -->
<template>
  <!-- 动态绑定参数 派发给父组件使用 -->
  <div v-for="(item, index) in 100" :key="index">
    <slot :data="item" :id="index"></slot>
  </div>
</template>

<!-- 父组件 -->
<Dialog>
  <!-- 通过解构的方式取值 -->
  <template #default="{ data, index }">
    <div>{{ data }} -- {{ index }}</div>
  </template>
</Dialog>

2.4 动态插槽

插槽可以是变量名

如果是 header,则填充到头部;如果是 footer,则填充到底部

  <Dialog>
    <template #[nameTest]>
      <div>手可摘星辰</div>
    </template>
  </Dialog>

const nameTest = ref('header')

3. 提供/注入

3.1 提供/注入是什么

提供 / 注入 | Vue3中文文档 - vuejsVue.js - The 渐进式 JavaScript 框架, Vue3中文文档 - Vue3最新动态https://www.javascriptc.com/vue3js/guide/component-provide-inject.html#%E5%A4%84%E7%90%86%E5%93%8D%E5%BA%94%E6%80%A7

 

provide:在祖先组件中,指定要暴露(提供)出去的属性

inject:在孙子组件中,指定要接收(注入)的属性

 

 

3.2 父组件暴露(提供)数据

3.2.1 provide 基本用法

provide 接收两个值

  • key
  • value:string、number、symbol

其中,value 默认是可以被子组件修改的,为了防止被修改,可以添加 readonly 属性

<template>
    <div class="App">
        <h2> App.vue </h2>
        <A></A>
    </div>
</template>
    
<script setup lang='ts'>
  import { provide, ref } from 'vue'
  import A from './components/A.vue'

  const colorInApp = ref<string>('red')
  // 如果不想父组件的值被子组件修改,可以添加 readonly 属性
  provide('color', readonly(colorInApp))

</script>

 

3.2.2 provide 源码解析

provide 只能在 setup 语法糖中使用,不能在 options 中使用

使用 原型链 的方式,实现 provide(举个栗子:var a = {name: 1}; var b = Object.create(a); 打印b 什么都没有,但是打印 b.name 可以打印出1)

export function provide<T>(key: InjectionKey<T> | string | number, value: T) {
  if (!currentInstance) {
    if (__DEV__) {
      // provide 只能在 setup 语法糖中使用,不能再 options 里使用
      warn(`provide() can only be used inside setup().`)
    }
  } else {
    // currentInstance 获取当前组件实例
    let provides = currentInstance.provides
    // 默认情况下,实例继承父类的 provides 对象
    // 如果当前组件有自己的 provides,那么他会使用 父provides 对象作为原型,创建自己的 provides
    // 在 inject 中只需要查询 原型链 即可

    // 读取父组件 provides
    const parentProvides =
      currentInstance.parent && currentInstance.parent.provides
    if (parentProvides === provides) {
      // var a = {name: 1}; var b = Object.create(a); 打印b 什么都没有,但是打印 b.name 可以打印出1
      provides = currentInstance.provides = Object.create(parentProvides)
    }
    // 在新的对象上增加了这次的 provides
    provides[key as string] = value
  }
}

3.3 子组件接收(注入)数据

补充:vue3 中可以使用 v-bind 绑定 setup 中的变量

3.3.1 inject 基本用法

inject 可以设置默认值(注意设置成响应式的)

inject 可以修改父组件中的 provide 值,注意响应式

<template>
    <div>
        <button @click="changeSth">改变 app.vue 中的 flag</button>
        <div class="my-box">{{ flag }}</div>
    </div>
</template>
    
<script setup lang='ts'>
import { inject, Ref, ref } from 'vue';

// 使用 Ref 进行类型推论
// 如果接收的值为空,则设置默认值 ref('yellow')
const colorInB = inject<Ref<string>>('color', ref('yellow'));

const changeSth = () => {
    // 如果无法修改,可以考虑改成 colorInB!.value = 'green';
    colorInB.value = 'green'
};
</script>

<style scoped lang="scss">
.my-box {
  // vue3 中可以使用 v-bind 绑定 setup 中的变量
  background: v-bind(colorInB)
}
</style>

3.3.2 inject 源码解析

查询父组件的 provides,如果读的到,则返回父组件的 provides

如果读不到(实例在根目录),则返回 appContext 的 provides

// inject 做了个函数重载
export function inject<T>(key: InjectionKey<T> | string): T | undefined
export function inject<T>(
  key: InjectionKey<T> | string,
  // 允许接收默认值
  defaultValue: T,
  treatDefaultAsFactory?: false
): T
export function inject<T>(
  key: InjectionKey<T> | string,
  defaultValue: T | (() => T),
  treatDefaultAsFactory: true
): T
export function inject(
  key: InjectionKey<any> | string,
  defaultValue?: unknown,
  treatDefaultAsFactory = false
) {
  // fallback to `currentRenderingInstance` so that this can be called in
  // a functional component

  // 读取当前组件的实例
  const instance = currentInstance || currentRenderingInstance
  if (instance) {
    // #2400
    // to support `app.use` plugins,
    // fallback to appContext's `provides` if the instance is at root

    // inject,查询父组件的 provides,如果读的到,则返回父组件的 provides
    // 如果读不到(实例在根目录),则返回 appContext 的 provides
    const provides =
      instance.parent == null
        ? instance.vnode.appContext && instance.vnode.appContext.provides
        : instance.parent.provides

    if (provides && (key as string | symbol) in provides) {
      // TS doesn't allow symbol as index type
      return provides[key as string]
    } else if (arguments.length > 1) {
      return treatDefaultAsFactory && isFunction(defaultValue)
        ? defaultValue.call(instance.proxy)
        : defaultValue
    } else if (__DEV__) {
      warn(`injection "${String(key)}" not found.`)
    }
  } else if (__DEV__) {
    warn(`inject() can only be used inside setup() or functional components.`)
  }
}

4. 兄弟组件传值

简而言之,兄弟组件通信 有两种方式:

  • 使用父组件充当桥梁,实现兄弟组件传参
  • 使用 EventBus(JavaScript 发布订阅)

4.1 EventBus 原理

4.1.1 使用 ts 实现一个简单的 EventBus

type BusClass<T> = {
  emit: (name: T) => void
  // emit 中接收的参数,会传递给 callback
  on: (name: T, callback: Function) => void
}

type BusParams = string | number | symbol

type List = {
  [key: BusParams]: Array<Function>
}

class Bus<T extends BusParams> implements BusClass<T> {
  // 调度中心(是个对象)
  list: List

  constructor() {
    // 初始化 list
    this.list = {};
  }

  /**
   * emit
   * @param name 自定义事件名称
   * @description 可传递的参数有多个,所以使用解构赋值 ...args
   */
  emit(name: T, ...args: Array<any>) {
    let eventName: Array<Function> = this.list[name]
    eventName.forEach(ev => {
      ev.apply(this, args)
    })
  }

/**
 * on
 * @param name 自定义事件名称
 * @param callback 自定义事件方法
 */
  on(name: T, callback: Function) {
    // 如果事件已经注册过了,则直接去 list 中寻找;如果没注册过,则返回空数组
    let fn: Array<Function> = this.list[name] || [];
    fn.push(callback)
    this.list[name] = fn
  }
}

export default new Bus<number>()

4.1.2 使用 4.1.1 的 EventBus

在 A 组件中,使用 emit 发出事件

在 B 组件中,使用 on 监听事件

// 兄弟组件A
import Bus form './bus.ts';

// 发送事件
const handleEmit = () => {
  Bus.emit('a-data', 1, 2);
};


// 兄弟组件B
<A @a-data="handleOn"></A>

import Bus form './bus.ts';

// 接收事件
const handleOn = () => {
  Bus.on('a-data', (data1, data2) => {
    console.log('data1 ===', data1);
    console.log('data2 ===', data2);
  });
};

4.2 Mitt

mitt - npmTiny 200b functional Event Emitter / pubsub.. Latest version: 3.0.0, last published: a year ago. Start using mitt in your project by running `npm i mitt`. There are 1346 other projects in the npm registry using mitt.https://www.npmjs.com/package/mitt

4.2.1 安装 mitt

npm install --save mitt

4.2.2 新建 mitt-bus.ts

import mitt from 'mitt';
export default mitt();

4.2.3 使用 mitt 发送事件

import mittBus from "@/utils/mitt-bus";

mittBus.emit('refreshDraftList', '刷新草稿列表');

4.2.4 使用 mitt 接收事件

接收事件监听的页面,在卸载时,一定要记得调用 off 取消事件监听

import mittBus from "@/utils/mitt-bus";

/**
 * 监听:刷新草稿列表页面
 * @desc 发送草稿表单后,会跳转回草稿列表页面,并重新获取草稿
 */
mittBus.on('refreshDraftList', (data: string) => {
  console.log(data);
  // 请求:获取草稿列表
  getDraftList();
})


/**
 * 页面卸载后,取消事件监听
 */
onUnmounted(() => {
  mittBus.off('refreshDraftList');
})

 

4.2.5 在全局实例上使用 mitt

上面的 4.2.1-4.2.4 是单独使用 mitt,不是将 mitt 挂载到全局上,下面展示如何挂载到全局使用

修改 main.ts

import { createApp } from 'vue'
import App from './App.vue'
import mitt from 'mitt'

const Mitts = mitt()

// 必须要拓展 ComponentCustomProperties 类型,才能获得关于 mitt 的 ts 提示
declare module "vue" {
    export interface ComponentCustomProperties {
        $Bus: typeof Mitts
    }
}

const app = createApp(App)

// Vue3 挂载全局 API
app.config.globalProperties.$Bus = Mitts

app.mount('#app')

派发事件

<template>
    <div>
        <h1>我是A</h1>
        <button @click="handleEmit1">handleEmit1</button>
        <button @click="handleEmit2">handleEmit2</button>
    </div>
</template>

<script setup lang='ts'>
import { getCurrentInstance } from 'vue'

const instance = getCurrentInstance();

const handleEmit1 = () => {
    instance?.proxy?.$Bus.emit('emitone', 100)
}

const handleEmit2 = () => {
    instance?.proxy?.$Bus.emit('emittwo', 200)
}
</script>

添加事件监听

注意:如果是监听全部事件,那么会默认接收两个参数:事件名称、事件数据

最后会这么展示:

  • 监听全部事件 === emitone 100
  • 监听全部事件 === emittwo 200
<template>
    <div>
        <h1>我是B</h1>
    </div>
</template>

<script setup lang='ts'>
import { getCurrentInstance } from 'vue'

const instance = getCurrentInstance()

instance?.proxy?.$Bus.on('emit-one', (num) => {
    console.log('监听一个事件 ===', num)
})

instance?.proxy?.$Bus.on('*', (emitName, emitData) => {
    console.log('监听全部事件 ===', emitName, emitData)
})
</script>

移除事件监听

const testEmitOff = (num: number) => {
    console.log(num, '测试事件监听移除')
}

// 添加事件监听
instance?.proxy?.$Bus.on('emitone', testEmitOff)
// 移除事件监听
instance?.proxy?.$Bus.off('emitone', testEmitOff)

清空所有事件监听

instance?.proxy?.$Bus.all.clear()

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐