实践练习:使用自定义事件和作用域插槽构建可重用组件
构建可重用的组件是高效 Vue.js 开发的基石。本课重点介绍如何通过自定义事件和范围插槽来增强组件的可重用性,从而实现更灵活和动态的组件交互。我们将探索如何定义和发出自定义事件,使子组件能够与它们的父组件通信。此外,我们将深入研究 scoped slots,这是一种强大的机制,用于为组件提供模板,同时保持对组件内部数据的访问。通过掌握这些技术,您将能够创建适应性强且可维护的组件,这些组件可以轻松集成到应用程序的各个部分。
自定义事件:发出和处理
自定义事件允许子组件与其父组件通信。对于子组件需要发出状态更改信号或向其父组件请求作的场景,这种通信至关重要。
使用 $emit
发出自定义事件
$emit
方法在组件中用于触发自定义事件。$emit
的第一个参数是事件的名称,后续参数是要与事件一起传递的任何数据。
<template>
<button @click="handleClick">Click me</button>
</template>
<script>
export default {
methods: {
handleClick() {
this.$emit('custom-event', { message: 'Hello from child!' });
}
}
};
</script>
在此示例中,单击按钮时,将调用 handleClick
方法。然后,此方法发出一个名为 custom-event
的自定义事件,将包含消息的对象作为数据传递。
处理父组件中的自定义事件
父组件可以使用 v-on
(或其简写 @
)监听其子组件发出的自定义事件。事件处理程序可以是父组件中定义的方法,也可以是内联表达式。
<template>
<div>
<child-component @custom-event="handleCustomEvent"></child-component>
<p>Message from child: {{ message }}</p>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
data() {
return {
message: ''
};
},
methods: {
handleCustomEvent(eventData) {
this.message = eventData.message;
}
}
};
</script>
在这里,父组件监听 ChildComponent
发出的自定义事件
。触发事件时,将调用 handleCustomEvent
方法,该方法使用从子对象传递的数据更新 message
data 属性。
具有自定义事件的事件修饰符
虽然像 .prevent
和 .stop
这样的事件修饰符通常用于原生 DOM 事件,但 Vue 也提供了在处理自定义事件时有用的修饰符,尤其是 .once
。
.once
:此修饰符确保事件处理程序仅触发一次。
<template>
<child-component @custom-event.once="handleCustomEvent"></child-component>
</template>
在这种情况下,handleCustomEvent
只会在第一次发出 custom-event
时调用。
示例:实现 counter 组件
让我们创建一个可重用的 counter 组件,当计数递增或递减时,它会发出自定义事件。
// Counter.vue
<template>
<div>
<button @click="decrement">-</button>
<span>{{ count }}</span>
<button @click="increment">+</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
};
},
methods: {
increment() {
this.count++;
this.$emit('increment', this.count);
},
decrement() {
this.count--;
this.$emit('decrement', this.count);
}
}
};
</script>
// ParentComponent.vue
<template>
<div>
<Counter @increment="onIncrement" @decrement="onDecrement" />
<p>Current Count: {{ currentCount }}</p>
</div>
</template>
<script>
import Counter from './Counter.vue';
export default {
components: {
Counter
},
data() {
return {
currentCount: 0
};
},
methods: {
onIncrement(count) {
this.currentCount = count;
},
onDecrement(count) {
this.currentCount = count;
}
}
};
</script>
在此示例中,每当单击相应的按钮时,Counter
组件都会发出 increment
和 decrement
事件。父组件侦听这些事件并更新其自己的 currentCount
数据属性。
Scoped Slots:灵活的组件渲染
作用域插槽为父组件提供了一种将模板注入子组件的方法,同时仍然可以访问子组件中的数据。这允许高度可定制和可重用的组件。
了解作用域插槽
作用域插槽是一种特殊类型的插槽,它允许父组件从子组件的作用域访问数据。这是通过将数据作为插槽的属性传递来实现的。
在子组件中定义作用域插槽
要定义作用域插槽,请使用 <slot>
元素并将数据作为属性绑定到它。
// ListComponent.vue
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot name="item" :item="item">{{ item.name }}</slot>
</li>
</ul>
</template>
<script>
export default {
props: {
items: {
type: Array,
required: true
}
}
};
</script>
在此示例中,ListComponent
呈现项目列表。名称为 “item” 的 <slot>
元素是有作用域的槽。 物品
数据作为插槽的属性传递。如果没有为插槽提供内容,则将呈现回退内容 {{ item.name }}
。
在父组件中使用 Scoped 插槽
要使用作用域插槽,请在子组件标签中的 <template>
元素上使用 v-slot
指令(或其简写 #
)。v-slot
的值是插槽的名称,该值也可以解构以访问插槽的属性。
// ParentComponent.vue
<template>
<div>
<ListComponent :items="items">
<template #item="{ item }">
<strong>{{ item.name }}</strong> - {{ item.description }}
</template>
</ListComponent>
</div>
</template>
<script>
import ListComponent from './ListComponent.vue';
export default {
components: {
ListComponent
},
data() {
return {
items: [
{ id: 1, name: 'Apple', description: 'A red fruit' },
{ id: 2, name: 'Banana', description: 'A yellow fruit' }
]
};
}
};
</script>
在此示例中,父组件使用 ListComponent
并为 “item” 插槽提供自定义模板。v-slot=“{ item }”
语法将 item
属性从插槽的范围内解构出来,允许父组件访问项目
数据并将其渲染为 <strong>{{ item.name }}</strong> - {{ item.description }}
.
命名作用域插槽
组件可以有多个作用域插槽,每个插槽都有一个唯一的名称。这允许在自定义组件的渲染时具有更大的灵活性。
// LayoutComponent.vue
<template>
<div class="layout">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot> <!-- Default slot -->
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
// ParentComponent.vue
<template>
<LayoutComponent>
<template #header>
<h1>My Page Title</h1>
</template>
<template #default>
<p>Main content of the page.</p>
</template>
<template #footer>
<p>© 2023</p>
</template>
</LayoutComponent>
</template>
<script>
import LayoutComponent from './LayoutComponent.vue';
export default {
components: {
LayoutComponent
}
};
</script>
在这个例子中,LayoutComponent
有三个插槽:“header”、“default”(未命名)和 “footer”。父组件为每个插槽提供自定义内容,允许它定义布局的结构和内容。
示例:创建具有 Scoped 插槽的 Table 组件
让我们创建一个可重用的 table 组件,该组件使用作用域插槽来允许父组件自定义每个单元格的渲染。
// TableComponent.vue
<template>
<table>
<thead>
<tr>
<th v-for="header in headers" :key="header">{{ header }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in data" :key="row.id">
<td v-for="header in headers" :key="header">
<slot :name="header" :row="row">{{ row[header] }}</slot>
</td>
</tr>
</tbody>
</table>
</template>
<script>
export default {
props: {
headers: {
type: Array,
required: true
},
data: {
type: Array,
required: true
}
}
};
</script>
// ParentComponent.vue
<template>
<div>
<TableComponent :headers="headers" :data="data">
<template #name="{ row }">
<strong>{{ row.name }}</strong>
</template>
<template #age="{ row }">
<span v-if="row.age >= 18">Adult</span>
<span v-else>Minor</span>
</template>
</TableComponent>
</div>
</template>
<script>
import TableComponent from './TableComponent.vue';
export default {
components: {
TableComponent
},
data() {
return {
headers: ['name', 'age', 'city'],
data: [
{ id: 1, name: 'John Doe', age: 25, city: 'New York' },
{ id: 2, name: 'Jane Doe', age: 17, city: 'Los Angeles' }
]
};
}
};
</script>
在此示例中,TableComponent
根据提供的 headers
和 data
props 呈现一个表。对于每个 cell,它使用与 header name 对应的命名范围槽。父组件为 “name” 和 “age” 列提供自定义模板,允许它自定义这些单元格的呈现。