大家好, 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学习系列(三)结束,
欢迎大家在评论区留言、讨论。