大家好, Capybara 将继续与大家一起学习Vue框架。今天依旧是大家的 编程学习小伙伴、前端学习体验家、网课资源品鉴官。
day04
组件的三大组成部分 (结构/样式/逻辑)
scoped样式冲突
当style标签不加scoped
1.style中的样式 默认是作用到全局的
2.加上scoped可以让样式变成局部样式
组件都应该有独立的样式,推荐加scoped(原理)
-----------------------------------------------------
scoped原理:
1.给当前组件模板的所有元素,都会添加上一个自定义属性
data-v-hash值
data-v-5f6a9d56 用于区分开不同的组件
2.css选择器后面,被自动处理,添加上了属性选择器
div[data-v-5f6a9d56]
代码:
<template>
<div class="base-one">
BaseOne
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
/*
1.style中的样式 默认是作用到全局的
2.加上scoped可以让样式变成局部样式
组件都应该有独立的样式,推荐加scoped(原理)
-----------------------------------------------------
scoped原理:
1.给当前组件模板的所有元素,都会添加上一个自定义属性
data-v-hash值
data-v-5f6a9d56 用于区分开不通的组件
2.css选择器后面,被自动处理,添加上了属性选择器
div[data-v-5f6a9d56]
*/
div{
border: 3px solid blue;
margin: 30px;
}
</style>
data是一个函数
一个组件的 data 选项必须是一个函数。→ 保证每个组件实例,维护独立的一份数据对象。
每次创建新的组件实例,都会新执行一次 data 函数,得到一个新对象。
实操代码:
BaseCount.vue
<template>
<div class="base-count">
<button @click="count--">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
</div>
</template>
<script>
export default {
// data() {
// console.log('函数执行了')
// return {
// count: 999,
// }
// },
data: function () {
return {
count: 999,
}
},
}
</script>
<style>
.base-count {
margin: 20px;
}
</style>
在App.vue中注册使用三个BaseCount:
效果:
控制台:
组件通信
什么是组件通信?
父子组件通信
父传子
实操代码:
效果:
子传父
实操代码:
效果:
props详解
什么是 prop
props 校验
实操
正确传值:
错误传值(无错误提示):
类型校验
添加校验(类型校验):
错误提示:
(类型校验)完整写法:
非空校验
不给子组件传值:
错误提示:
默认值
(没传值的时候按默认值来):
传个超大值:
// 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
}
},
},
},
自定义校验
value为父组件传入子组件的值;
return ture则通过校验,反之不通过,提示错误;
单向数据流
子组件随意修改自己内部的数据count:
在子组件中尝试修改父组件传过来的count:
unexpected mutation(意外的改变)
正确做法(儿子通知老爹,让其修改—— 子传父通信 ):
在子组件添加事件监听:
添加事件触发函数,通过 $emit 传信:
父组件添加监听:
父组件处理:
单向数据流:父组件的prop更新,会单向地向下流动,影响到子组件(数据更新)。
综合案例:小黑记事本 (组件版)
小黑记事本组件版-拆分组件
小黑记事本组件版-渲染&添加删除
app.vue中,给子组件传入list
渲染功能
// 1.提供数据: 提供在公共的父组件 App.vue
// 2.通过父传子,将数据传递给TodoMain
// 3.利用 v-for渲染
TodoMain.vue
<template>
<!-- 列表区域 -->
<section class="main">
<ul class="todo-list">
<li class="todo" v-for="(item, index) in list" :key="item.id">
<div class="view">
<span class="index">{{ index + 1 }}.</span>
<label>{{ item.name }}</label>
<button class="destroy" @click="handleDel(item.id)"></button>
</div>
</li>
</ul>
</section>
</template>
<script>
export default {
props: {
list: {
type: Array,
},
},
methods: {
handleDel(id) {
this.$emit('del', id)
},
},
}
</script>
<style>
</style>
添加功能
// 1.手机表单数据 v-model
// 2.监听事件(回车+点击都要添加)
// 3.子传父,讲任务名称传递给父组件 App.vue
// 4.进行添加 unshift(自己的数据自己负责)
// 5.清空文本框输入的内容
// 6.对输入的空数据 进行判断
TodoHeader.vue
<template>
<!-- 输入框 -->
<header class="header">
<h1>小黑记事本</h1>
<input placeholder="请输入任务" class="new-todo" v-model="todoName" @keyup.enter="handleAdd"/>
<button class="add" @click="handleAdd">添加任务</button>
</header>
</template>
<script>
export default {
data(){
return {
todoName:''
}
},
methods:{
handleAdd(){
// console.log(this.todoName)
this.$emit('add',this.todoName)
this.todoName = ''
}
}
}
</script>
<style>
</style>
在app.vue中修改数据
删除功能
// 1.监听事件(监听删除的点击) 携带id
// 2.子传父,讲删除的id传递给父组件的App.vue
// 3.进行删除filter(自己的数据 自己负责)
子组件监听:
通知父组件:
父组件修改数据:
底部功能与持久化
// 底部合计:父传子 传list 渲染
// 清空功能:子传父 通知父组件 → 父组件进行更新
// 持久化存储:watch深度监视list的变化 -> 往本地存储 ->进入页面优先读取本地数据
TodoFooter.vue
<template>
<!-- 统计和清空 -->
<footer class="footer">
<!-- 统计 -->
<span class="todo-count"
>合 计:<strong> {{ list.length }} </strong></span
>
<!-- 清空 -->
<button class="clear-completed" @click="clear">清空任务</button>
</footer>
</template>
<script>
export default {
props: {
list: {
type: Array,
},
},
methods:{
clear(){
this.$emit('clear')
}
}
}
</script>
<style>
</style>
app.vue代码
<template>
<!-- 主体区域 -->
<section id="app">
<TodoHeader @add="handleAdd"></TodoHeader>
<TodoMain :list="list" @del="handelDel"></TodoMain>
<TodoFooter :list="list" @clear="clear"></TodoFooter>
</section>
</template>
<script>
import TodoHeader from './components/TodoHeader.vue'
import TodoMain from './components/TodoMain.vue'
import TodoFooter from './components/TodoFooter.vue'
// 渲染功能:
// 1.提供数据: 提供在公共的父组件 App.vue
// 2.通过父传子,将数据传递给TodoMain
// 3.利用 v-for渲染
// 添加功能:
// 1.手机表单数据 v-model
// 2.监听事件(回车+点击都要添加)
// 3.子传父,讲任务名称传递给父组件 App.vue
// 4.进行添加 unshift(自己的数据自己负责)
// 5.清空文本框输入的内容
// 6.对输入的空数据 进行判断
// 删除功能
// 1.监听事件(监听删除的点击) 携带id
// 2.子传父,讲删除的id传递给父组件的App.vue
// 3.进行删除filter(自己的数据 自己负责)
// 底部合计:父传子 传list 渲染
// 清空功能:子传父 通知父组件 → 父组件进行更新
// 持久化存储:watch深度监视list的变化 -> 往本地存储 ->进入页面优先读取本地数据
export default {
data() {
return {
list: JSON.parse(localStorage.getItem('list')) || [
{ id: 1, name: '打篮球' },
{ id: 2, name: '看电影' },
{ id: 3, name: '逛街' },
],
}
},
components: {
TodoHeader,
TodoMain,
TodoFooter,
},
watch: {
list: {
deep: true,
handler(newVal) {
localStorage.setItem('list', JSON.stringify(newVal))
},
},
},
methods: {
handleAdd(todoName) {
// console.log(todoName)
this.list.unshift({
id: +new Date(),
name: todoName,
})
},
handelDel(id) {
// console.log(id);
this.list = this.list.filter((item) => item.id !== id)
},
clear() {
this.list = []
},
},
}
</script>
<style>
</style>
总结
非父子通信 (拓展) - event bus 事件总线
建立两个非父子组件的通信:
创建 utils/EventBus.js
EventBus.js
import Vue from 'vue'
const Bus = new Vue()
export default Bus
点击B组件中的按钮后,A组件接收到信息并显示:
可以实现一对多通信:
非父子通信 (拓展) - provide & inject
跨层级共享数据:
组件结构:
App.vue中既有简单类型数据,也有复杂类型数据:
简单数据类型(非响应式)
复杂类型(响应式,推荐)
使用provide提供数据
注册点击事件:
点击按钮之后,我们看到数据(简单类型)已经改变(pink->green),但页面并没有响应更新:
修改复杂类型数据:
页面自动更新(zs -> ls):
进阶语法
v-model 原理
不同的input组件,比如checkbox就是checked属性和checked事件的合写。
在模板中不能写e,
而应写$event(获取事件对象)
表单类组件封装 & v-model 简化代码
表单类组件封装
封装自己的表单类组件(BaseSelect)时,
因为单向数据流的存在,而v-model是双向数据绑定,所以需要拆解(不再使用语法糖v-model)
如果在封装表单类组件时(作为子组件使用)使用v-model,
选中其它city,会因为双向绑定,修改子组件中的cityId,
不符合单向数据流(cityId由父组件传入)
完整代码
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
(在父组件中,使用 $event 获取形参)
<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>
Vue中的$event详解
场景1:获取原生DOM事件的事件对象
在DOM事件的回调函数中传入参数$event,可以获取到该事件的事件对象<template> <button @click="getData($event)">按钮</button> </template> <script> export default { setup() { const getData = (e) => { console.log(e) } return { getData } } } </script>
当我们点击button按钮时,可以看到控制台打印出的事件对象,如下图:通过该对象自带的一些属性,我们可以避免过多的冗余代码,细化代码。
场景2:事件注册所传的参数(子组件向父组件传值)
在子组件中通过$emit注册事件,将数据作为参数传入,在父组件中通过$event接收父组件:
<template> <Hello @hello="showData($event)" /> <h4>{{data}}</h4> </template> <script> import Hello from '@/components/Hello.vue' import { ref } from 'vue' export default { components: { Hello }, setup() { const data = ref(null) const showData = (e) => { data.value = e } return { showData, data } } } </script>
子组件:
<template> <button @click="$emit('hello', 'hello')">Hello</button> <!-- $emit()的第一个参数是定义的事件名,第二个参数是要传入的数据 --> </template> <script> export default { } </script>
此时我们点击hello按钮,就会将子组件传入的'hello'字符串在页面上显示出来,如下图
v-model 简化代码
关键:
父子通信时,子组件触发事件名为‘input’的事件(触发事件为input,固定的);
在父组件使用v-mdel语法糖::value=" " @input=" " (所传属性为value,固定的)
总结
.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>
ref 和 $refs
获取dom元素
代码:
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>
效果:
document.querySelector 会查找项目中所有的元素;
$refs只会在当前组件查找盒子。
获取组件实例
效果(点击获取数据):
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>编辑</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
title: '大标题',
isShowEdit: false,
editValue: '',
}
},
methods: {
},
}
</script>
<style>
</style>
this.$refs.inp为undefined
使用$nextTick改进代码:
使用setTimeOut也可以解决问题(但,等待时间不精准):
$nextTick:等 DOM 更新后, 才会触发执行此方法里的函数体
本次Vue学习系列(三)结束,
欢迎大家在评论区留言、讨论。