Web前端-Vue2+Vue3基础入门到实战项目-Day4
- 组件的三大组成部分(结构/样式/逻辑)
- scoped样式冲突
- data是一个函数
- 组件通信
- 组件通信语法
- 父传子
- 子传父
- props详解
- 什么是props
- props检验
- props与data的区别
- 非父子(扩展)
- 事件总线 (event bus)
- provide - inject
- 案例 - 小黑记事本(组件版)
- App.vue
- TodoHeader.vue
- TodoMain.vue
- TodoFooter.vue
- 进阶语法
- v-model详解
- v-model原理
- 表单类组件封装
- v-model简化代码
- sync修饰符
- ref和$refs
- $nextTick
- 来源
组件的三大组成部分(结构/样式/逻辑)
scoped样式冲突
- 全局样式: 默认的style样式, 会作用于全局
- 局部样式: 加上scoped属性的style样式, 只会作用于当前组件
- scoped原理:
- 给当前组件模板的所有元素, 添加一个自定义属性
data-v-hash值: 根据hash值区分不同的组件 - css选择器后面, 被自动处理, 添加上了属性选择器
div[data-v-hash]
- 给当前组件模板的所有元素, 添加一个自定义属性
<template><div class="base-one">BaseOne</div>
</template><script>
export default {}
</script><style scoped>
div {border: 3px solid blue;margin: 30px;
}
</style>
data是一个函数
- data必须是一个函数 -> 保证每个组件实例, 维护独立的一个数据对象
- 每次创建新的组件实例, 都会新执行一次data函数, 得到一个新对象
<template><div class="base-count"><button @click="count--">-</button><span>{{ count }}</span><button @click="count++">+</button></div>
</template><script>
export default {data() {return {count: 100,}},
}
</script><style>
.base-count {margin: 20px;
}
</style>
组件通信
组件通信语法
- 组件关系和对应的通信方案
- 父子关系:
props, $emit
- 非父子关系:
provide, inject
或eventbus
- 通用方案:
vuex
- 父子关系:
- 父子通信方案的核心流程
- 父传子props:
- 父中给子添加属性传值
- 子props接收
- 使用
- 子传父$emit
- 子$emit发送消息
- 父中给子添加消息监听
- 父中实现处理函数
- 父传子props:
父传子
<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",components: {Son,},data() {return {myTitle: "学前端,就来黑马程序员",}},
}
</script><style>
</style><!-- Son.vue -->
<template><div class="son" 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>
子传父
<template><div class="app" style="border: 3px solid #000; margin: 10px">我是APP组件<!-- 2. 父组件, 对消息进行监听 --><Son :title="myTitle" @changeTitle="handleChange"></Son></div>
</template><script>
import Son from "./components/Son.vue"
export default {name: "App",components: {Son,},data() {return {myTitle: "学前端,就来黑马程序员",}},methods: {// 3. 提供处理函数, 提供逻辑handleChange(newTitle){this.myTitle = newTitle}}
}
</script><style>
</style><!-- 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(){// 1. 通过$emit, 向父组件发送消息通知this.$emit('changeTitle', "传智教育")}}
}
</script><style></style>
props详解
什么是props
- 定义: 组件上注册的一些自定义属性
- 作用: 向子组件传递数据
- 特点:
- 可以传递任意数量的prop
- 可以传递任意类型的prop
父组件
<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>
子组件
<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>
props检验
- 作用: 为组件的prop指定验证要求, 不符合要求, 控制台会有错误提示
- 语法
- 类型检验
- 非空检验
- 默认值
- 自定义检验
父组件
<template><div class="app"><BaseProgress :w="width"></BaseProgress></div>
</template><script>
import BaseProgress from './components/BaseProgress.vue'
export default {data() {return {width: 23,}},components: {BaseProgress,},
}
</script><style>
</style>
子组件
<template><div class="base-progress"><div class="inner" :style="{ width: w + '%' }"><span>{{ w }}%</span></div></div>
</template><script>
export default {// props: ["w"],// 1.基础写法(类型校验)// props: {// w: Number // Number String Boolean Array Object// }// 2.完整写法(类型、是否必填、默认值、自定义校验)props: {w: {type: Number,// required: truedefault: 0,validator (value) {if(value >= 0 && value <= 100){return true}console.error('传入的prop w, 必须是0-100的数字')return false}}}
}
</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>
props与data的区别
- 共同点: 都可以给组件提供数据
- 区别:
- data的数据是自己的 -> 随便改
- prop的数据是外部的 -> 不能直接改, 要遵循单向数据流
- 单向数据流: 父级prop的数据更新, 会向下流动, 影响子组件. 这个数据流动是单向的.
父组件
<template><div class="app"><BaseCount @changeCount="handleChange":count="count"></BaseCount></div>
</template><script>
import BaseCount from './components/BaseCount.vue'
export default {components:{BaseCount},data(){return {count:100}},methods:{handleChange(value){this.count = value}}
}
</script><style></style>
子组件
<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.外部传过来的数据 不能随便修改// 单向数据流: 父组件的prop更新, 会单向向下流动, 影响到子组件.props: {count: Number},methods: {handleAdd(){this.$emit('changeCount', this.count+1)},handleSub(){this.$emit('changeCount', this.count-1)}}}
</script><style>
.base-count {margin: 20px;
}
</style>
非父子(扩展)
事件总线 (event bus)
- 作用: 非父子组件之间, 进行简易消息传递(复杂场景 -> vuex)
- 语法:
- 创建一个都能访问的事件总线(空vue实例) -> utils/EventBus.js
import Vue from 'vue' const Bus = new Vue() export default Bus
- A组件(接受方), 监听Bus实例的事件
created() {Bus.$on('sendMsg', (msg) => {// console.log(msg)this.msg = msg}) }
- B组件(发送方), 触发Bus实例的事件
Bus.$emit('sendMsg', '今天天气不错,适合旅游')
provide - inject
- 作用: 跨层级共享数据
- 语法:
- 父组件provide提供数据
provide() {return {// 简单类型 是非响应式的color: this.color,// 复杂类型 是响应式的userInfo: this.userInfo,} }
- 子/孙组件 inject 取值使用
<script> export default {inject: ['color', 'userInfo'], } </script>
案例 - 小黑记事本(组件版)
核心步骤
- 拆分基础组件
新建组件 -> 拆分存放结构 -> 导入注册使用 - 渲染待办任务
提供数据(公共父组件) -> 父传子传递list -> v-for渲染 - 添加任务
收集数据v-model -> 监听事件 -> 子传父传递任务 -> 父组件unshift - 删除任务
监听删除id -> 子传父传递id -> 父组件filter删除 - 底部合计和清空功能
底部合计: 父传子list -> 合计展示
清空功能: 监听点击 -> 子传父通知父组件 -> 父组件清空 - 持久化存储: watch监视数据变化, 持久化到本地
App.vue
<template><!-- 主体区域 --><section id="app"><TodoHeaderVue @add="handleAdd"></TodoHeaderVue><TodoMainVue @del="handleDel" :list="list"></TodoMainVue><TodoFooterVue @clear="handleClear" :list="list"></TodoFooterVue></section>
</template><script>
import TodoHeaderVue from './components/TodoHeader.vue'
import TodoMainVue from './components/TodoMain.vue'
import TodoFooterVue from './components/TodoFooter.vue'// 渲染功能:
// 1. 提供数据-> 提供在公共的父组件 App.vue
// 2. 通过父传子, 奖数据传递给 TodoMain
// 3. 利用v-for渲染// 添加功能
// 1. 收集表单数据 -> v-model
// 2. 监听事件 (回车 + 点击都要进行添加)
// 3. 子传父, 将任务名称传递给父组件App.vue
// 4. 进行添加 unshift// 删除功能
// 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: '逛街'},]}},methods: {handleAdd(todoName){this.list.unshift({id: +new Date(),name: todoName})},handleDel(id){this.list = this.list.filter(item => item.id!==id)},handleClear(){this.list = []}},watch: {list: {deep: true,handler(newValue){localStorage.setItem('list', JSON.stringify(newValue))}}},components: {TodoHeaderVue,TodoMainVue,TodoFooterVue}
}
</script><style></style>
TodoHeader.vue
<template><div><!-- 输入框 --><header class="header"><h1>小黑记事本</h1><input v-model.trim="todoName" @keyup.enter="handleAdd" placeholder="请输入任务" class="new-todo"/><button @click="handleAdd" class="add">添加任务</button></header></div>
</template><script>
export default {data(){return {todoName: ''}},methods: {handleAdd(){if(this.todoName.trim() === ''){alert('任务名称不能为空')return }this.$emit('add', this.todoName)this.todoName = ''}}
}
</script><style>
</style>
TodoMain.vue
<template><div><!-- 列表区域 --><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 @click="handleDel(item.id)" class="destroy"></button></div></li></ul></section></div>
</template><script>
export default {props: {list: Array},methods: {handleDel(id){this.$emit('del', id)}}
}
</script><style></style>
TodoFooter.vue
<template><div><!-- 统计和清空 --><footer class="footer"><!-- 统计 --><span class="todo-count">合 计:<strong> {{list.length}} </strong></span><!-- 清空 --><button @click="clear" class="clear-completed">清空任务</button></footer></div>
</template><script>
export default {props: {list: Array},methods: {clear(){this.$emit('clear')}}
}
</script><style></style>
进阶语法
v-model详解
v-model原理
- 原理: v-model本质上是一个语法糖. 例如应用在输入框上, 就是value属性和input事件的合写.
- 作用: 提供数据的双向绑定
- 数据发生变化, 视图自动变化: value
- 视图发生变化, 数据自动变化: @input
$event
: 用在模板中, 获取事件的形参
<div class="app"><input v-model="msg1" type="text" /> <br /><input :value="msg2" @input="msg2 = $event.target.value" type="text" >
</div>
表单类组件封装
实现子组件和父组件数据的双向绑定
- 父传子: 数据 由父组件props传递, v-model拆解绑定数据
- 子传父: 监听输入, 子传父传值给父组件修改
父组件
<template><div class="app"><BaseSelect :selectId="selectId" @change="selectId = $event"></BaseSelect></div>
</template><script>
import BaseSelect from './components/BaseSelect.vue'
export default {data() {return {selectId: '102',}},components: {BaseSelect,},methods: {}
}
</script><style>
</style>
子组件
<template><div><select :value="selectId" @change="handleChange"><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: {handleChange(e){this.$emit('change', e.target.value)}}
}
</script><style>
</style>
v-model简化代码
父组件v-model简化实现子组件和父组件数据双向绑定
- 子组件: props通过value接收, 事件触发input
- 父组件: v-model绑定数据 (:value + @input)
父组件
<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>
子组件
<template><div><select :value="value" @change="handleChange"><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: {handleChange(e){this.$emit('input', e.target.value)}}
}
</script><style>
</style>
sync修饰符
- 作用: 实现子组件与父组件的数据双向绑定, 简化代码
- 特点: prop属性名, 可以自定义, 非固定为value
- 场景: 封装弹框类的基础组件, visible属性 true显示 false隐藏
- 本质:
:属性名 + @update:属性名
父组件
<template><div class="app"><button @click="isShow = true">退出按钮</button><BaseDialog :visible.sync="isShow"></BaseDialog></div>
</template><script>
import BaseDialog from "./components/BaseDialog.vue"
export default {data() {return {isShow: false}},methods: {},components: {BaseDialog,},
}
</script><style>
</style>
子组件
<template><div v-show="visible" class="base-dialog-wrap"><div class="base-dialog"><div class="title"><h3>温馨提示:</h3><button @click="close" class="close">x</button></div><div class="content"><p>你确认要退出本系统么?</p></div><div class="footer"><button>确认</button><button>取消</button></div></div></div>
</template><script>
export default {props: {visible: Boolean},methods: {close(){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>
ref和$refs
- 作用: 通过
ref
和$refs
可以获取dom元素和组件实例 - 使用:
- 目标组件 - 添加ref属性
<div ref="test"></div>
- 通过this.$refs.
ref属性值
获取目标组件
this.$refs.test
- 获取dom
<div ref="mychart" class="base-chart-box">子组件</div>const myChart = echarts.init(this.$refs.mychart)
- 获取组件
父组件
子组件<template><div class="app"><BaseForm ref="baseFrom"></BaseForm><button @click="handleGet">获取数据</button><button @click="handleReset">重置数据</button></div></template><script> import BaseForm from './components/BaseForm.vue' export default {components: {BaseForm,},methods: {handleGet(){console.log(this.$refs.baseFrom.getValues())},handleReset(){this.$refs.baseFrom.resetValues()}} } </script><style> </style>
<template><div class="app"><div>账号: <input v-model="username" type="text"></div><div>密码: <input v-model="password" type="text"></div></div> </template><script> export default {data() {return {username: 'admin',password: '123456',}},methods: {getValues() {return {username: this.username,password: this.password}},resetValues() {this.username = ''this.password = ''console.log('重置表单数据成功');},} } </script><style scoped> .app {border: 2px solid #ccc;padding: 10px; } .app div{margin: 10px 0; } .app div button{margin-right: 8px; } </style>
$nextTick
- Vue是异步更新DOM的
$nextTick
: 在DOM更新完成之后做某件事
<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="handleEdit">编辑</button></div></div>
</template><script>
export default {data() {return {title: '大标题',isShowEdit: false,editValue: '',}},methods: {handleEdit(){// 1. 显示输入框 (异步dom更新)this.isShowEdit = true// 2. 让输入框显示焦点// console.log(this.$refs.inp) // undefinedthis.$nextTick(()=>{this.$refs.inp.focus()})}},
}
</script><style>
</style>
来源
黑马程序员. Vue2+Vue3基础入门到实战项目