Vue 组件化开发的核心是 "拆分与协作"------ 不仅要会拆组件,还要懂组件的结构、样式、逻辑,以及组件间如何通信。本文结合思维导图,从组件的三大组成讲起,再深入组件通信,附带进阶语法解析。
一、组件的三大组成部分:结构、样式、逻辑
Vue 的单文件组件(.vue)由 <template>(结构)、<script>(逻辑)、<style>(样式) 三部分组成,每个部分都有特殊的注意事项。
1. 结构:<template>
- 只能有一个根元素(Vue 2 要求,Vue 3 支持多根);
- 支持 Vue 的指令(
v-for/v-if等)和插值表达式; - 示例:
vue
<template>
<!-- 唯一根元素 -->
<div class="card">
<h3>{{ title }}</h3>
<p>{{ content }}</p>
</div>
</template>
2. 样式:<style>与scoped
- 普通样式:默认全局生效,可能引发样式冲突;
scoped样式:添加scoped后,样式仅作用于当前组件(通过给 DOM 添加唯一属性实现);scoped样式冲突的解决:若需修改子组件样式,可使用深度选择器(/deep/或::v-deep)。- 示例:
vue
<!-- App.vue -->
<template>
<div id="app">
<BaseOne></BaseOne>
<BaseTwo></BaseTwo>
</div>
</template>
<script>
import BaseOne from './components/BaseOne'
import BaseTwo from './components/BaseTwo'
export default {
name: 'App',
components: {
BaseOne,
BaseTwo
}
}
</script>
vue
<!-- BaseOne.vue -->
<template>
<div class="base-one">
BaseOne
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
/*
组件最终都会渲染到页面上,在任意组件上写的标签选择器自然会选中整个网页中所有匹配的元素并应用样式
对此,Vue 提供了解决方案:scoped
让 style 中的样式只影响当前组件内的标签
scoped 的原理:
1. 给当前组件内的所有标签加一个自定义属性:data-v-hash(网页中唯一)
2. 应用选择器时会自动加上属性选择器
*/
div{
border: 3px solid blue;
margin: 30px;
}
</style>
vue
<!-- BaseTwo.vue -->
<template>
<div class="base-one">
BaseTwo
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
div{
border: 3px solid red;
margin: 30px;
}
</style>
3. 逻辑:<script>与data是函数
data必须是函数:组件复用后,每个实例能拥有独立的data(若为对象,所有实例会共享数据);props:接收父组件传递的数据;methods:定义组件方法;- 示例:
vue
<script>
export default {
// 接收父组件数据
props: {
title: { type: String, required: true }
},
// data是函数,返回独立的响应式数据
data() {
return {
content: "组件的默认内容"
};
},
methods: {
updateContent() {
this.content = "修改后的内容";
}
}
};
</script>
二、组件通信:组件间的数据传递
组件是独立的,需要通过通信机制实现数据交互,核心场景是 "父传子""子传父"。
1. 父传子:props
- 语法:父组件通过属性传递数据,子组件用props接收;
- 步骤:
- 父组件:在子组件标签上绑定属性;
- 子组件:用props声明接收的属性;
- 示例:
vue
<!-- 父组件 App.vue -->
<template>
<div class="app">
<BaseProgress :w="width"></BaseProgress>
<!-- <BaseProgress></BaseProgress> -->
</div>
</template>
<script>
import BaseProgress from './components/BaseProgress.vue'
export default {
data() {
return {
width: 30,
}
},
components: {
BaseProgress,
},
}
</script>
<style>
</style>
vue
<!-- 子组件 BaseProgress.vue -->
<template>
<div class="base-progress">
<div class="inner" :style="{ width: w + '%' }">
<span>{{ w }}%</span>
</div>
</div>
</template>
<script>
export default {
// 1.基础写法(类型校验)
// props: {
// w: Number,
// },
// 2.完整写法(类型、默认值、非空、自定义校验)
props: {
w: {
type: Number, // 类型
// default: 50, // 默认值
required: true, //必须填,必须传入
// 自定义校验
// 参数1:传递过来的数据,可供我们校验
validator(val){
// 返回值绝对校验是否通过
if (val >= 0 && val <=100) {
return true
} else {
console.error('取值范围是 0 ~ 100')
return false
}
// 一般不用,因为这个无法告诉程序员自定义错误的原因是什么
// return val >= 0 && val <= 100
}
}
}
}
</script>
<style scoped>
.base-progress {
height: 26px;
width: 400px;
border-radius: 15px;
background-color: #272425;
border: 3px solid #272425;
box-sizing: border-box;
margin-bottom: 30px;
}
.inner {
position: relative;
background: #379bff;
border-radius: 15px;
height: 25px;
box-sizing: border-box;
left: -3px;
top: -2px;
}
.inner span {
position: absolute;
right: 0;
top: 26px;
}
</style>
2.单项数据流
vue
<!-- App.vue -->
<template>
<div class="app">
<BaseCount @change="hChange" :count="count"></BaseCount>
</div>
</template>
<script>
import BaseCount from './components/BaseCount.vue'
export default {
components:{
BaseCount
},
data(){
return {
count:100
}
},
methods: {
hChange(val){
this.count = val
}
}
}
</script>
<style>
</style>
vue
<!-- BaseCount.vue -->
<template>
<div class="base-count">
<button @click="handleSub">-</button>
<span>{{ count }}</span>
<button @click="handleAdd">+</button>
</div>
</template>
<script>
export default {
// 1.自己的数据随便修改 (谁的数据 谁负责)
// data () {
// return {
// count: 100,
// }
// },
// 2.外部传过来的数据 不能随便修改
props: {
count: {
type: Number,
},
},
methods: {
// props 是单向数据流:
// 数据更新必须是由父组件到子组件这一个的流向,不允许由子组件直接修改 props 的数据,避免后期数据管理混乱
// 如果子组件非要修改 props 传递过来的数据,请使用 $emit 子传父,交给父组件修改
handleSub() {
// this.count--
this.$emit('change', this.count - 1)
},
handleAdd() {
// this.count++
this.$emit('change', this.count + 1)
},
},
}
</script>
<style>
.base-count {
margin: 20px;
}
</style>
3. 子传父:$emit+ 自定义事件
- 语法:子组件通过
this.$emit(事件名, 数据)触发自定义事件,父组件监听事件并接收数据; - 步骤:
- 子组件:触发自定义事件,传递数据;
- 父组件:监听事件,执行方法并接收数据;
- 示例:
vue
<!-- 子组件 Child.vue -->
<template>
<button @click="sendData">向父组件传值</button>
</template>
<script>
export default {
data() {
return { childData: "子组件的数据" };
},
methods: {
sendData() {
// 触发自定义事件,传递数据
this.$emit("child-event", this.childData, 123);
}
}
};
</script>
vue
<!-- 父组件 Parent.vue -->
<template>
<!-- 监听自定义事件,执行方法 -->
<Child @child-event="handleChildEvent" />
<p>子组件传递的数据:{{ childData }}</p>
</template>
<script>
import Child from "./Child.vue";
export default {
components: { Child },
data() {
return { childData: "" };
},
methods: {
// 接收子组件传递的数据
handleChildEvent(data1, data2) {
this.childData = data1;
console.log(data2); // 输出123
}
}
};
</script>
4. 非父子组件通信(扩展)
对于无直接关系的组件,常用方案:
- Vuex/Pinia:全局状态管理(复杂项目);
- 事件总线(EventBus):通过 Vue 实例作为中间件传递事件(简单场景);
$attrs/$listeners:多级组件透传数据(较少用)。
三、进阶语法:提升组件开发效率
1. v-model原理与组件应用
- 原理:
v-model是value属性 +input事件的语法糖; - 组件中使用
v-model:子组件通过props: { value }接收值,通过$emit("input", 新值)更新。 - 示例:
vue
<!-- 子组件 CustomInput.vue -->
<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>
<script>
export default { props: { value: { type: String, default: "" } } };
</script>
vue
<!-- 父组件 -->
<template>
<CustomInput v-model="msg" />
<p>{{ msg }}</p>
</template>
2. sync修饰符:双向绑定单个属性
用于简化 "父传子 + 子传父" 的双向绑定,是update:propName事件的语法糖。
示例:
vue
<!-- 父组件 App.vue -->
<template>
<div class="app">
<button @click="openDialog">退出按钮</button>
<!-- <BaseDialog @close="isShow = $event" :visible="isShow"></BaseDialog> -->
<!-- visible.sync => :visible="isShow" @update:visible="isShow=$event" -->
<!--
.sync 做了两件事:
1. 将数据传递给子组件, :visible
2. 监听一个事件, 事件名为 update:visible
v-model 做了两件事:
1. 将数据传递给子组件, 数据名 value
2. 监听一个事件, 事件名为 input
v-model 的局限性:
1. 数据名必须叫 value
2. 事件名必须叫 input
3. 一个标签只能使用一个 v-model
-->
<BaseDialog :visible.sync="isShow"></BaseDialog>
</div>
</template>
<script>
import BaseDialog from './components/BaseDialog.vue'
export default {
data() {
return {
isShow: false,
}
},
methods: {
openDialog() {
this.isShow = true
},
},
components: {
BaseDialog,
},
}
</script>
<style>
</style>
vue
<!-- 子组件 BaseDialog.vue-->
<template>
<div v-show="visible" class="base-dialog-wrap">
<div class="base-dialog">
<div class="title">
<h3>温馨提示:</h3>
<button @click="hClose" class="close">x</button>
</div>
<div class="content">
<p>你确认要退出本系统么?</p>
</div>
<div class="footer">
<button>确认</button>
<button @click="hClose">取消</button>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
visible: {
type: Boolean,
required: true
}
},
methods: {
hClose() {
// this.visible = false
this.$emit('update:visible', false)
}
}
}
</script>
<style scoped>
.base-dialog-wrap {
width: 300px;
height: 200px;
box-shadow: 2px 2px 2px 2px #ccc;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
padding: 0 10px;
}
.base-dialog .title {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #000;
}
.base-dialog .content {
margin-top: 38px;
}
.base-dialog .title .close {
width: 20px;
height: 20px;
cursor: pointer;
line-height: 10px;
}
.footer {
display: flex;
justify-content: flex-end;
margin-top: 26px;
}
.footer button {
width: 80px;
height: 40px;
}
.footer button:nth-child(1) {
margin-right: 10px;
cursor: pointer;
}
</style>
3. ref与$refs:直接操作组件 / 元素
通过ref给组件 / 元素打标记,用this.$refs.标记名直接访问实例 / 元素。
示例:
vue
<!-- 父组件 App.vue -->
<template>
<div class="app">
<div class="base-chart-box">
这是一个捣乱的盒子
</div>
<BaseChart></BaseChart>
</div>
</template>
<script>
import BaseChart from './components/BaseChart.vue'
export default {
components:{
BaseChart
}
}
</script>
<style>
.base-chart-box {
width: 300px;
height: 200px;
}
</style>
vue
<!-- 子组件 BaseChart.vue -->
<template>
<div ref="box" class="base-chart-box">子组件</div>
</template>
<script>
import * as echarts from 'echarts'
export default {
mounted() {
// 基于准备好的dom,初始化echarts实例
// document.querySelector 会查找项目中所有的元素
// $refs只会在当前组件查找盒子
// 1. 给标签添加 ref 属性
// 2. 使用 this.$refs.属性名 来获取 DOM 元素
// const myChart = echarts.init(document.querySelector('.base-chart-box'))
const myChart = echarts.init(this.$refs.box)
// 绘制图表
myChart.setOption({
title: {
text: 'ECharts 入门示例',
},
tooltip: {},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'],
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20],
},
],
})
},
}
</script>
<style scoped>
.base-chart-box {
width: 400px;
height: 300px;
border: 3px solid #000;
border-radius: 6px;
}
</style>
4. $nextTick:等待 DOM 更新后执行
Vue 更新 DOM 是异步的,$nextTick可确保在 DOM 更新完成后执行回调。
示例:
vue
<template>
<div class="app">
<div v-if="isShowEdit">
<input type="text" v-model="editValue" ref="inp" />
<button>确认</button>
</div>
<div v-else>
<span>{{ title }}</span>
<button @click="editFn">编辑</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
title: "大标题",
isShowEdit: false,
editValue: "",
};
},
methods: {
editFn() {
// 1.显示文本框
this.isShowEdit = true;
// 在 Vue 中数据变化视图会更新, 关键点: 视图的更新是异步任务
// DOM 更新是异步的, 是为了性能优化
// 2.让文本框聚焦 -> $nextTick()
// setTimeout(() => {
// this.$refs.inp.focus()
// }, 0)
this.$nextTick(() => {
// 这个回调函数就是在 DOM 更新后才执行
this.$refs.inp.focus();
});
},
},
};
</script>
<style>
</style>