Component Communication
props
props 是使用频率最高的一种通信方式,子组件中使用宏函数 defineProps 定义自定义属性,常用于:父 <=> 子
- 父传子:属性值是非函数
- 子传父:属性值是函数(作为父传子函数的参数)
宏函数
- 宏函数只能在 setup 代码块中使用
- 宏函数不需要显式导入
- 宏函数 defineProps 编译 (Vue -> JS) 时执行,编译为组件的 props
vue
<script lang="ts" setup>
import { ref, reactive } from "vue";
import Child from "./components/Child.vue";
const str_ = "str_parent";
const refStr_ = ref("refStr_parent");
const reactiveArr_ = reactive([6, 6, 6]);
</script>
<template>
<div>Parent: {{ str_ }} {{ refStr_ }} {{ reactiveArr_ }}</div>
<Child :str="str_" :refStr="refStr_" :reactiveArr="reactiveArr_" />
<!-- str_ 不是响应式的,refStr_、reactiveArr_ 是响应式的 -->
<button @click="str_ += '!'">setStr</button>
<button @click="refStr_ += '!'">setRefStr</button>
<button @click="reactiveArr_.push(6)">setReactiveArr</button>
</template>vue
<script lang="ts" setup>
const props = defineProps(["str", "refStr", "reactiveArr"]);
// {str: 'str_parent', refStr: 'refStr_parent', reactiveArr: Proxy(Array)}
console.log("[Child] props:", props);
</script>
<template>
<!-- template 中, 使用 props.propName 或直接使用 propName 都可以 -->
<div>Child: {{ str }} {{ props.refStr }} {{ reactiveArr }}</div>
</template>ts
const props = defineProps({
str: {
type: String,
default: "str_default",
},
refStr: {
type: String,
default: "refStr_default",
},
reactiveArr: {
type: Array<number>,
default: () => [5, 2, 8], // 引用类型必须转换为箭头函数
},
});vue
<script lang="ts" setup>
const props = defineProps<{
str?: string;
refStr?: string;
reactiveArr?: number[];
}>();
console.log("[Child] props:", props);
</script>
<template>
<div>Child: {{ str }} {{ props.refStr }} {{ reactiveArr }}</div>
</template>ts
const props = withDefaults(
defineProps<{
str?: string;
refStr?: string;
reactiveArr?: number[];
}>(),
{
str: "str_default",
refStr: "refStr_default",
reactiveArr: () => [5, 2, 8], // 引用类型必须转换为箭头函数
},
);defineEmits ($emit)
自定义事件常用于:子 => 父
子组件使用 defineEmits 声明可以触发的自定义事件,并通过 emit 方法触发事件,父组件通过 @[eventName]="[callback]" 监听子组件触发的事件,并在回调函数中接收子组件传递的数据
vue
<script lang="ts" setup>
// 子组件使用 defineEmits 定义自定义事件
// 写法一
// const emit = defineEmits(["evName", "evName2"]);
// 写法二,规定参数类型
// const emit = defineEmits<{
// (e: "evName", arg: Event): void;
// (e: "evName2", arg: string, arg2: string): void;
// }>();
// 写法三,具名元组
const emit = defineEmits<{
evName: [arg: Event];
evName2: [arg: string, arg2: string];
}>();
const emitToParent = (ev: Event) => {
// 子组件派发自定义事件,emit 发射参数给父组件
emit("evName", ev);
};
const emitToParent2 = () => {
emit("evName2", "foo", "bar");
};
</script>
<template>
<button @click="(ev) => emitToParent(ev)">emitToParent</button>
<button @click="emitToParent2">emitToParent2</button>
</template>vue
<script setup lang="ts">
import Child from "./components/Child.vue";
// 自定义事件派发时,父组件接收子组件发射的数据,作为回调函数的参数
const receiveFromChild = (...args: unknown[]) => console.log(args);
</script>
<template>
<!-- 父组件为子组件的自定义事件绑定回调函数,监听子组件派发的自定义事件 -->
<Child
@evName="(...args: unknown[]) => receiveFromChild(args)"
@evName2="receiveFromChild"
/>
</template>defineExpose
子组件使用 defineExpose 暴露自身的属性或方法,供父组件通过模板引用(ref)直接访问:父 => 子
vue
<script setup>
import { ref } from "vue";
const count = ref(0);
const increment = () => {
count.value++;
};
defineExpose({
count,
increment,
});
</script>vue
<script setup>
import { ref } from "vue";
import Child from "./Child.vue";
const childRef = ref<InstanceType<typeof Child>>();
const handleAccessChild = () => {
console.log(childRef.value.count);
childRef.value.increment();
};
</script>
<template>
<Child ref="childRef" />
<button @click="handleAccessChild">handleAccessChild</button>
</template>mitt
与消息订阅与发布功能类似,可以实现任意组件间通信
bash
pnpm i mitt新建文件:src/utils/emitter.ts 作为事件总线
ts
import mitt from "mitt";
const emitter = mitt();
/* 在通信组件内部绑定/触发事件
// 绑定事件
emitter.on("event1", (value) => {
console.log("[event1]: ", value);
});
emitter.on("event1", (value) => {
console.log("[event2]: ", value);
});
// 解绑事件
emitter.off("event1", () => {
console.log("This will not be called");
});
setInterval(() => {
// 触发事件
emitter.emit("event1", "triggered");
emitter.emit("event2", "triggered");
}, 1000);
setTimeout(() => {
// 清理事件
emitter.all.clear();
}, 3000);
*/
export default emitter;v-model
v-model 可以实现组件的双向绑定,父 <=> 子
vue
<template>
<Child v-model="countModel" />
</template>vue
<script setup>
// defineModel() 返回的值是一个 ref
const model = defineModel();
function update() {
model.value++;
}
</script>
<template>
<div>Parent bound v-model is: {{ model }}</div>
<button @click="update">Increment</button>
</template>底层机制
defineModel 是一个便利宏。编译器将其展开为以下内容:
- 一个名为
modelValue的 prop,本地 ref 的值与其同步 - 一个名为
update:modelValue的事件,当本地 ref 的值发生变更时触发
vue
<template>
<Child :modelValue="foo" @update:modelValue="($event) => (foo = $event)" />
</template>vue
<script setup>
const props = defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);
</script>
<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>v-model 修饰符
.trim,.number, .lazy,支持自定义修饰符 v-model.customModifier
vue
<script setup lang="ts">
import Child from "./components/Child.vue";
import { ref } from "vue";
const myText = ref("");
</script>
<template>
This input capitalizes the first letter you enter:
<Child v-model:custom-input.capitalize="myText" />
</template>vue
<script lang="ts" setup>
const [model, modifiers] = defineModel("customInput", {
default: "",
type: String, // 类型校验
// 自定义校验函数(控制台警告)
validator(value: string) {
return value.length >= 10;
},
set(value) {
if (modifiers.capitalize) {
return value.charAt(0).toUpperCase() + value.slice(1);
}
return value;
},
});
</script>
<template>
<input type="text" v-model="model" />
</template>UseAttrs ($attrs)
UseAttrs 用于实现当前组件的父组件,向当前组件的子组件通信,祖 => 孙
vue
<script lang="ts" setup>
import { reactive, ref } from "vue";
import Parent from "./components/Parent.vue";
const a = ref(1);
const b = reactive({ value: 2 });
const addA = (delta: number) => (a.value += delta);
</script>
<template>
<div>
<div>[GrandParent] a={{ a }} b={{ b }}</div>
<!-- v-bind="{ p1: "v1", p2: "v2" }" 等价于 :p1="v1" :p2="v2" -->
<Parent :a="a" :b="b" :addA="addA" :="{ p1: 'v1', p2: 'v2' }" />
</div>
</template>vue
<script lang="ts" setup>
import { useAttrs } from "vue";
import Child from "./Child.vue";
const props = defineProps(["a", "b", "addA"]);
// {a: 1, b: Proxy(Object), addA: ƒ}
console.log("[Parent] props:", props);
const attrs = useAttrs();
// {p1: 'v1', p2: 'v2'} 排除了 props 中的属性
console.log("[Parent] attrs:", attrs);
</script>
<template>
<div>
<div>[Parent] a={{ a }} b={{ b }} attrs={{ attrs }}</div>
<Child :a="a" :b="b" :addA="addA" :="attrs" />
</div>
</template>vue
<script lang="ts" setup>
import { useAttrs } from "vue";
const props = defineProps(["p1", "p2"]);
// {p1: 'v1', p2: 'v2'}
console.log("[Child] props:", props);
const attrs = useAttrs();
// {a: 1, b: Proxy(Object)}
console.log("[Child] attrs:", attrs);
</script>
<template>
<div>
<p>[Child] p1={{ p1 }} p2={{ p2 }} attrs={{ attrs }}</p>
<button @click="(attrs.addA as Function)(1)">Add grandparent's a</button>
</div>
</template>标签的 ref 属性
用于普通 DOM 标签时,获取的是 DOM 节点;用于组件标签时,获取的是组件实例对象
使用
<script setup>的组件默认是封闭的,父组件无法直接访问子组件内部的属性和方法,需要通过defineExpose显式暴露
vue
<template>
<input type="text" ref="refInput" id="idInput" />
<button @click="showLog">SHOW</button>
</template>
<script lang="ts" setup>
import { ref, useTemplateRef } from "vue";
// 通过 ref 获取元素
let refInput = ref<HTMLInputElement>();
let input = useTemplateRef("refInput"); // 可以取别名
function showLog() {
// 通过 id 获取元素
const idInput = document.getElementById("idInput");
console.log(refInput.value.value);
console.log(input.value.value);
console.log((idInput as HTMLInputElement).value);
}
</script>函数式 ref
函数式 ref 是把 ref 绑定为一个函数:
- 函数会在组件挂载 / 更新时执行
- 函数的参数是 DOM 元素 / 组件实例
- 函数的返回值会被忽略
vue
<!-- 在 v-for 中使用函数式 ref,避免覆盖 -->
<template>
<div v-for="(item, index) in list" :key="index">
<input
type="text"
:ref="
(el) => {
if (el) inputRefs[index] = el;
}
"
/>
</div>
<button @click="focusFirstInput">focusFirstInput</button>
</template>
<script setup>
import { ref } from "vue";
const list = ref<number[]>([1, 2, 3]);
const inputRefs = ref<HTMLInputElement[]>([]);
const focusFirstInput = () => {
inputRefs.value[0]?.focus();
};
</script>$refs/$parent
$refs:值为对象,包含所有被ref属性标识的DOM元素或组件实例,父 => 子$parent:值为对象,当前组件的父组件实例对象,子 => 父
provide/inject
实现祖孙组件直接通信,祖先 => 后代
- 在祖先组件中通过
provide配置向所有后代组件提供数据 - 在后代组件中通过
inject配置来声明接收数据
ts
const colorVal = ref("lightpink");
// 祖先 provide 提供
provide("colorKey" /** key */, colorVal /** value */);
// 后代 inject 注入
const injectedColor = inject<Ref<string>>(
"colorKey",
ref("unknown-color") /** defaultVal */,
);slot
子组件提供给父组件的占位符,可以插入父组件的 template
vue
<script lang="ts" setup>
import Child from "./components/Child.vue";
</script>
<template>
<div>
<Child>
<template v-slot:default>
<div>content for default slot</div>
</template>
<template v-slot:scoped="{ item, idx }">
<ul>
<li>{{ `Item ${idx}: Name: ${item.name}, Age: ${item.age}` }}</li>
</ul>
</template>
<template #named>
<div>content for named slot</div>
</template>
</Child>
</div>
</template>vue
<script lang="ts" setup>
import { reactive } from "vue";
const users = reactive([
{ name: "Alice", age: 1 },
{ name: "Bob", age: 2 },
{ name: "Charlie", age: 3 },
]);
</script>
<template>
<div>
<header>
<!-- 匿名插槽 name="default" -->
<slot>default: default slot content</slot>
</header>
<main>
<div v-for="(item, idx) of users" :key="idx">
<!-- 作用域插槽 -->
<!-- 数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定 -->
<slot name="scoped" :item="item" :idx="idx"
>default: scoped slot content</slot
>
</div>
</main>
<footer>
<!-- 具名插槽 -->
<slot name="named">default: named slot content</slot>
</footer>
</div>
</template>