一、组件的三大组成部分(结构/样式/逻辑)
1.组件的三大组成部分- 注意点说明
2.组件的样式冲突scoped
/*
1.style中的样式 默认是作用到全局的
2.加上scoped可以让样式变成局部样式
组件都应该有独立的样式,推荐加scoped(原理)
-----------------------------------------------------
scoped原理:
1.给当前组件模板的所有元素,都会添加上一个自定义属性
data-v-hash值
data-v-5f6a9d56 用于区分开不通的组件
2.css选择器后面,被自动处理,添加上了属性选择器
div[data-v-5f6a9d56]
*/
3.data 是一个函数
在 Vue 中,将 data
定义为 函数 而不是直接返回对象是 Vue 的核心设计原则之一。
BaseCount.vue
<template>
<div class="base-count">
<button @click="count--">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
</div>
</template>
<script>
export default {
data(){
return {
count:999
}
}
}
</script>
<style scoped>
.base-count{
margin:20px;
}
</style>
App.vue
<template>
<div class="app">
<BaseCount></BaseCount>
<BaseCount></BaseCount>
<BaseCount></BaseCount>
</div>
</template>
<script>
import BaseCount from './components/BaseCount.vue'
export default {
components: {
BaseCount,
},
}
</script>
<style>
</style>
通过将 data
定义为函数,每个组件实例会通过调用 data()
获得独立的 data
对象:
这样,每个组件实例的 count
都是独立的,互不干扰。
如果 data
是一个普通对象(非函数),所有组件实例会共享同一个数据对象。例如:
二、组件通信
1.什么是组件通信
2.不同的组件关系和组件通信方案分类
组件通信解决方案:
父子通信流程图:
Son.vue
<template>
<div style="border:3px solid #000;margin:10px">
<!-- 3.直接使用props的值 -->
我是Son组件 {{title}}
</div>
</template>
<script>
export default {
name: 'Son-Child',
// 2.通过props来接受
props:['title']
}
</script>
<style>
</style>
App.vue
<template>
<div class="app" style="border: 3px solid #000; margin: 10px">
我是APP组件
<!-- 1.给组件标签,添加属性方式 赋值 -->
<Son :title="myTitle"></Son>
</div>
</template>
<script>
import Son from './components/Son.vue'
export default {
name: 'App',
data() {
return {
myTitle: '世界,你好',
}
},
components: {
Son,
},
}
</script>
<style>
</style>
name: 'App'
的作用是:- 调试标识:在 Vue Devtools 中,组件会显示为
App
,方便调试。 - 递归准备:如果未来需要让
App
组件递归调用自身,name
是必需的。 - 代码可读性:明确标识该组件为
App
,便于团队协作和后续维护。
- 调试标识:在 Vue Devtools 中,组件会显示为
Son.vue
<template>
<div class="son" style="border: 3px solid #000; margin: 10px">
我是Son组件 {{ title }}
<button @click="changeFn">修改title</button>
</div>
</template>
<script>
export default {
name: 'Son-Child',
props: ['title'],
methods: {
changeFn() {
// 通过this.$emit() 向父组件发送通知
this.$emit('changTitle','快乐星球')
},
},
}
</script>
<style>
</style>
App.vue
<template>
<div class="app" style="border: 3px solid #000; margin: 10px">
我是APP组件
<!-- 2.父组件对子组件的消息进行监听 -->
<Son :title="myTitle" @changTitle="handleChange"></Son>
</div>
</template>
<script>
import Son from './components/Son.vue'
export default {
name: 'App',
data() {
return {
myTitle: '世界,你好',
}
},
components: {
Son,
},
methods: {
// 3.提供处理函数,提供逻辑
handleChange(newTitle) {
this.myTitle = newTitle
},
},
}
</script>
<style>
</style>
3.什么是prop
UserInfo.vue
<template>
<div class="userinfo">
<h3>我是个人信息组件</h3>
<div>姓名:{{username}}</div>
<div>年龄:{{age}}</div>
<div>是否单身:{{isSingle}}</div>
<div>座驾:{{car.brand}}</div>
<div>兴趣爱好:{{hobby.join('、')}}</div>
</div>
</template>
<script>
export default {
props:['username','age','isSingle','car','hobby']
}
</script>
<style>
.userinfo {
width: 300px;
border: 3px solid #000;
padding: 20px;
}
.userinfo > div {
margin: 20px 10px;
}
</style>
App.vue
<template>
<div class="app">
<UserInfo
:username="username"
:age="age"
:isSingle="isSingle"
:car="car"
:hobby="hobby"
></UserInfo>
</div>
</template>
<script>
import UserInfo from './components/UserInfo.vue'
export default {
data() {
return {
username: '小帅',
age: 28,
isSingle: true,
car: {
brand: '宝马',
},
hobby: ['篮球', '足球', '羽毛球'],
}
},
components: {
UserInfo,
},
}
</script>
<style>
</style>
4.props 校验
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,
required: true, //必须传个值给我
default: 0, //没传值的默认值
validator(val) {
// console.log(val)
if (val >= 100 || val <= 0) {
console.error('传入的范围必须是0-100之间')
return false
} else {
return true //返回true则通过了校验,反之。
}
},
},
},
}
</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>
App.vue
<template>
<div class="app">
<BaseProgress :w="width"></BaseProgress>
</div>
</template>
<script>
import BaseProgress from './components/BaseProgress.vue'
export default {
data() {
return {
width: 50,
}
},
components: {
BaseProgress,
},
}
</script>
<style>
</style>
5.prop & data、单向数据流
6.非父子通信(拓展) - event bus 事件总线
创建事件总线(utils/EventBus.js
)
通过创建空 Vue 实例作为事件总线,供各组件访问:
import Vue from 'vue'
const Bus = new Vue() // 创建 Vue 实例作为事件总线
export default Bus // 导出总线实例
接收方组件(如 A
组件)监听事件
在组件生命周期(如 created
)中,通过 $on
监听总线事件:
import Bus from '../utils/EventBus.js' // 引入事件总线
export default {
created() {
Bus.$on('事件名', (参数) => { // 监听事件,参数为发送方传递的数据
// 处理接收的数据
this.数据 = 参数
})
}
}
发送方组件(如 B
组件)触发事件
通过 $emit
触发总线事件并传参
import Bus from '../utils/EventBus.js' // 引入事件总线
export default {
methods: {
触发事件() {
Bus.$emit('事件名', '传递的数据') // 触发事件并传参
}
}
}
BaseA.vue
<template>
<div class="base-a">
我是A组件(接受方)
<p>{{msg}}</p>
</div>
</template>
<script>
import Bus from '../utils/EventBus'
export default {
data() {
return {
msg: '',
}
},
created() {
Bus.$on('sendMsg', (msg) => {
// console.log(msg)
this.msg = msg
})
},
}
</script>
<style scoped>
.base-a {
width: 200px;
height: 200px;
border: 3px solid #000;
border-radius: 3px;
margin: 10px;
}
</style>
BaseB.vue
<template>
<div class="base-b">
<div>我是B组件(发布方)</div>
<button @click="sendMsgFn">发送消息</button>
</div>
</template>
<script>
import Bus from '../utils/EventBus'
export default {
methods: {
sendMsgFn() {
Bus.$emit('sendMsg', '今天天气不错,适合旅游')
},
},
}
</script>
<style scoped>
.base-b {
width: 200px;
height: 200px;
border: 3px solid #000;
border-radius: 3px;
margin: 10px;
}
</style>
BaseC.vue
<template>
<div class="base-c">
我是C组件(接受方)
<p>{{msg}}</p>
</div>
</template>
<script>
import Bus from '../utils/EventBus'
export default {
data() {
return {
msg: '',
}
},
created() {
Bus.$on('sendMsg', (msg) => {
// console.log(msg)
this.msg = msg
})
},
}
</script>
<style scoped>
.base-c {
width: 200px;
height: 200px;
border: 3px solid #000;
border-radius: 3px;
margin: 10px;
}
</style>
EventBus.js
import Vue from 'vue'
const Bus = new Vue()
export default Bus
App.vue
<template>
<div class="app">
<BaseA></BaseA>
<BaseB></BaseB>
<BaseC></BaseC>
</div>
</template>
<script>
import BaseA from './components/BaseA.vue'
import BaseB from './components/BaseB.vue'
import BaseC from './components/BaseC.vue'
export default {
components:{
BaseA,
BaseB,
BaseC
}
}
</script>
<style>
</style>
7.非父子通信(拓展) - provide & inject
GrandSon.vue
<template>
<div class="grandSon">
我是GrandSon
{{ color }} -{{ userInfo.name }} -{{ userInfo.age }}
</div>
</template>
<script>
export default {
inject: ['color', 'userInfo'],
}
</script>
<style>
.grandSon {
border: 3px solid #000;
border-radius: 6px;
margin: 10px;
height: 100px;
}
</style>
SonA.vue
<template>
<div class="SonA">我是SonA组件
<GrandSon></GrandSon>
</div>
</template>
<script>
import GrandSon from '../components/GrandSon.vue'
export default {
components:{
GrandSon
}
}
</script>
<style>
.SonA {
border: 3px solid #000;
border-radius: 6px;
margin: 10px;
height: 200px;
}
</style>
SonB.vue
<template>
<div class="SonB">
我是SonB组件
</div>
</template>
<script>
export default {
}
</script>
<style>
.SonB {
border: 3px solid #000;
border-radius: 6px;
margin: 10px;
height: 200px;
}
</style>
App.vue
<template>
<div class="app">
我是APP组件
<button @click="change">修改数据</button>
<SonA></SonA>
<SonB></SonB>
</div>
</template>
<script>
import SonA from './components/SonA.vue'
import SonB from './components/SonB.vue'
export default {
provide() {
return {
// 简单类型 是非响应式的
color: this.color,
// 复杂类型 是响应式的
userInfo: this.userInfo,
}
},
data() {
return {
color: 'pink',
userInfo: {
name: 'zs',
age: 18,
},
}
},
methods: {
change() {
this.color = 'red'
this.userInfo.name = 'ls'
},
},
components: {
SonA,
SonB,
},
}
</script>
<style>
.app {
border: 3px solid #000;
border-radius: 6px;
margin: 10px;
}
</style>
三、综合案例:小黑记事本(组件版)
代码详见day04
四、进阶语法
1.v-model 原理
App.vue
<template>
<div class="app">
<input type="text" v-model="msg1" />
<br />
<!-- v-model的底层其实就是:value和 @input的简写 -->
<input type="text" :value="msg2" @input="msg2 = $event.target.value" />
</div>
</template>
<script>
export default {
data() {
return {
msg1: '',
msg2: '',
}
},
}
</script>
<style>
</style>
2.表单类组件封装& v-model 简化代码
BaseSelect.vue
<template>
<div>
<select :value="selectId" @change="selectCity">
<option value="101">北京</option>
<option value="102">上海</option>
<option value="103">武汉</option>
<option value="104">广州</option>
<option value="105">深圳</option>
</select>
</div>
</template>
<script>
export default {
props: {
selectId: String,
},
methods: {
selectCity(e) {
this.$emit('changeCity', e.target.value)
},
},
}
</script>
<style>
</style>
App.vue
<template>
<div class="app">
<BaseSelect
:selectId="selectId"
@changeCity="selectId = $event"
></BaseSelect>
</div>
</template>
<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
data() {
return {
selectId: '102',
}
},
components: {
BaseSelect,
},
}
</script>
<style>
</style>
BaseSelect.vue
<template>
<div>
<select :value="value" @change="selectCity">
<option value="101">北京</option>
<option value="102">上海</option>
<option value="103">武汉</option>
<option value="104">广州</option>
<option value="105">深圳</option>
</select>
</div>
</template>
<script>
export default {
props: {
value: String,
},
methods: {
selectCity(e) {
this.$emit('input', e.target.value)
},
},
}
</script>
<style>
</style>
App.vue
<template>
<div class="app">
<BaseSelect
v-model="selectId"
></BaseSelect>
</div>
</template>
<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
data() {
return {
selectId: '102',
}
},
components: {
BaseSelect,
},
}
</script>
<style>
</style>
3. .sync 修饰符
BaseDialog.vue
<template>
<div class="base-dialog-wrap" v-show="isShow">
<div class="base-dialog">
<div class="title">
<h3>温馨提示:</h3>
<button class="close" @click="closeDialog">x</button>
</div>
<div class="content">
<p>你确认要退出本系统么?</p>
</div>
<div class="footer">
<button>确认</button>
<button>取消</button>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
isShow: Boolean,
},
methods:{
closeDialog(){
this.$emit('update:isShow',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>
App.vue
<template>
<div class="app">
<button @click="openDialog">退出按钮</button>
<!-- isShow.sync => :isShow="isShow" @update:isShow="isShow=$event" -->
<BaseDialog :isShow.sync="isShow"></BaseDialog>
</div>
</template>
<script>
import BaseDialog from './components/BaseDialog.vue'
export default {
data() {
return {
isShow: false,
}
},
methods: {
openDialog() {
this.isShow = true
// console.log(document.querySelectorAll('.box'));
},
},
components: {
BaseDialog,
},
}
</script>
<style>
</style>
4.ref 和$refs
BaseChart.vue
<template>
<div class="base-chart-box" ref="baseChartBox">子组件</div>
</template>
<script>
import * as echarts from 'echarts'
export default {
mounted() {
// 基于准备好的dom,初始化echarts实例
// document.querySelector 会查找项目中所有的元素
// $refs只会在当前组件查找盒子
// var myChart = echarts.init(document.querySelector('.base-chart-box'))
var myChart = echarts.init(this.$refs.baseChartBox)
// 绘制图表
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>
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>
BaseForm.vue
<template>
<div>
<label>
账号:
<input type="text" v-model="account" />
</label>
<br />
<label>
密码:
<input type="password" v-model="password" />
</label>
<br />
<button @click="getFormData">获取数据</button>
<button @click="resetForm">重置数据</button>
</div>
</template>
<script>
export default {
data() {
return {
account: '',
password: ''
};
},
methods: {
getFormData() {
console.log('组件内获取到的账号:', this.account);
console.log('组件内获取到的密码:', this.password);
},
resetForm() {
this.account = '';
this.password = '';
console.log('子组件:表单已重置');
}
}
};
</script>
- 模板(
<template>
):- 定义了一个包含两个输入框(账号和密码)及两个按钮(“获取数据” 和 “重置数据”)的表单。
v-model="account"
和v-model="password"
实现输入框与组件数据account
、password
的双向绑定,输入内容实时同步到对应数据。- 按钮通过
@click
绑定方法getFormData
和resetForm
,点击时触发对应操作。
- 脚本(
<script>
):data
函数返回组件的响应式数据account
和password
,用于存储输入框内容。getFormData
方法:打印当前输入的账号和密码,模拟获取表单数据的业务逻辑。resetForm
方法:将account
和password
重置为空字符串,清空输入框,并打印提示信息。
App.vue
<template>
<div>
<BaseForm ref="baseForm" />
<button @click="callChildComponentMethod">
父组件调用子组件方法
</button>
</div>
</template>
<script>
import BaseForm from './components/BaseForm.vue'; // 确保路径正确
export default {
components: {
BaseForm
},
methods: {
callChildComponentMethod() {
// 通过 $refs 获取子组件实例
const formComponent = this.$refs.baseForm;
if (formComponent) {
// 调用子组件的方法
formComponent.getFormData(); // 输出子组件内获取的数据
formComponent.resetForm(); // 调用子组件的重置方法
}
}
}
};
</script>
- 模板(
<template>
):- 使用
<BaseForm ref="baseForm" />
引入子组件,并通过ref="baseForm"
为子组件实例命名,便于父组件获取。 - 定义一个按钮,点击时触发
callChildComponentMethod
方法。
- 使用
- 脚本(
<script>
):- 先通过
import
引入子组件BaseForm
,再在components
选项中注册,确保模板中可使用<BaseForm>
标签。 callChildComponentMethod
方法:- 通过
this.$refs.baseForm
获取子组件实例(ref
注册的名称需与$refs
访问的名称一致)。 - 若实例存在,调用子组件的
getFormData
和resetForm
方法,实现父组件控制子组件行为(如获取表单数据、重置表单)。
- 通过
- 先通过
整体功能
- 子组件
BaseForm.vue
实现了一个简单的表单交互,包含数据输入、获取和重置功能。 - 父组件通过
ref
和$refs
获取子组件实例,直接调用子组件方法,实现跨组件的逻辑控制,体现了 Vue 中父组件访问子组件实例及方法的能力。
5.Vue异步更新、$nextTick
需求
编辑标题, 编辑框自动聚焦
-
点击编辑,显示编辑框
-
让编辑框,立刻获取焦点
<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() {
// 显示输入框
this.isShowEdit = true
// 获取焦点
this.$refs.inp.focus()
} },
}
</script>
问题
"显示之后",立刻获取焦点是不能成功的!
原因:Vue 是异步更新DOM (提升性能)
解决方案
$nextTick:等 DOM更新后,才会触发执行此方法里的函数体
语法: this.$nextTick(函数体)
this.$nextTick(() => {
this.$refs.inp.focus()
})
注意:$nextTick 内的函数体 一定是箭头函数,这样才能让函数内部的this指向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() {
// 显示输入框
this.isShowEdit = true;
// 获取焦点
this.$nextTick(() => {
this.$refs.inp.focus();
});
},
},
};
</script>