1 组件组成部分
1.1 scoped 解决样式冲突
写在组件中的样式会 全局生效 → 因此很容易造成多个组件之间的样式冲突问题。
全局样式: 默认组件中的样式会作用到全局,任何一个组件中都会受到此样式的影响
局部样式: 可以给组件加上scoped 属性,可以让样式只作用于当前组件
scoped 原理
- 当前组件内标签都被添加 data-v-hash值 的属性
- css选择器都被添加 [data-v-hash值] 的属性选择器
最终效果: 必须是当前组件的元素, 才会有这个自定义属性, 才会被这个样式作用到
1.2 data 必须是一个函数
一个组件的 data 选项必须 是一个函数。目的是为了:保证每个组件实例,维护 独立 的一份 数据 对象。
每次创建新的组件实例,都会新 执行一次data 函数,得到一个新对象。
2 组件通信
2.1 什么是组件通信?
组件通信,就是指 组件与组件 之间的 数据传递
- 组件的数据是独立的,无法直接访问其他组件的数据。
- 想使用其他组件的数据,就需要组件通信
2.2 组件之间如何通信
2.3 组件关系分类
- 父子关系
- 非父子关系
2.4 通信解决方案
2.5 父子通信流程
- 父组件通过 props 将数据传递给子组件
- 子组件利用 $emit 通知父组件修改更新
2.5.1 父向子通信代码示例
父组件通过 props 将数据传递给子组件
父组件App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <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>
|
子组件Son.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <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>
|
父向子传值步骤
- 给子组件以添加属性的方式传值
- 子组件内部通过 props 接收
- 模板中直接使用 props接收的值
2.5.2 子向父通信代码示例
子组件利用 $emit 通知父组件,进行修改更新
子向父传值步骤
- $emit 触发事件,给父组件发送消息通知
- 父组件监听 $emit 触发的事件
- 提供处理函数,在函数的性参中获取传过来的参数
3 props
3.1 props 基础
Props 定义
组件上注册的一些 自定义属性
Props 作用
向子组件传递数据
特点
- 可以 传递 任意数量 的 prop
- 可以 传递 任意类型 的 prop
代码演示
父组件App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <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>
|
子组件UserInfo.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <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>
|
3.2 props校验
为组件的 prop 指定 验证要求,不符合要求,控制台就会有 错误提示 → 帮助开发者,快速发现错误
语法
4.代码演示
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <template> <div class="app"> <BaseProgress :w="width"></BaseProgress> </div> </template>
<script> import BaseProgress from './components/BaseProgress.vue' export default { data() { return { width: 30, } }, components: { BaseProgress, }, } </script>
<style> </style>
|
BaseProgress.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <template> <div class="base-progress"> <div class="inner" :style="{ width: w + '%' }"> <span>{{ w }}%</span> </div> </div> </template>
<script> export default { props: { w: Number, }, } </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>
|
3.3 props校验完整写法
3.3.1 语法
1 2 3 4 5 6 7 8 9 10 11
| props: { 校验的属性名: { type: 类型, // Number String Boolean ... required: true, // 是否必填 default: 默认值, // 默认值 validator (value) { // 自定义校验逻辑 return 是否通过校验 } } },
|
3.3.2 代码实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <script> export default { // 完整写法(类型、默认值、非空、自定义校验) 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 } }, }, }, } </script>
|
3.3.3 注意
1.default 和 required 一般不同时写(因为当时必填项时,肯定是有值的)
2.default 后面如果是简单类型的值,可以直接写默认。如果是复杂类型的值,则需要以函数的形式 return 一个默认值
3.4 props&data、单向数据流
3.4.1 共同点
都可以给组件提供数据
3.4.2 区别
- data 的数据是 自己 的 → 随便改
- prop 的数据是 外部 的 → 不能直接改,要遵循 单向数据流
3.4.3 单向数据流
父级 props 的数据更新,会向下流动,影响子组件。这个数据流动是单向的
4.代码演示
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <template> <div class="app"> <BaseCount :count="count" @changeCount="handleChange"></BaseCount> </div> </template>
<script> import BaseCount from './components/BaseCount.vue' export default { components: { BaseCount, }, data() { return { count: 100, } }, methods: { handleChange(newVal) { // console.log(newVal); this.count = newVal }, }, } </script>
<style> </style>
|
BaseCount.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <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: { handleSub() { this.$emit('changeCount', this.count - 1) }, handleAdd() { this.$emit('changeCount', this.count + 1) }, }, } </script>
<style> .base-count { margin: 20px; } </style>
|
5.口诀
谁的数据谁负责
4 非父子通信
4.1 event bus 事件总线
非父子组件之间,进行简易消息传递。(复杂场景→ Vuex)
步骤
创建一个都能访问的事件总线 (空 Vue 实例)
1 2 3
| import Vue from 'vue' const Bus = new Vue() export default Bus
|
A 组件(接受方),监听 Bus 的 $on 事件
1 2 3 4 5
| created () { Bus.$on('sendMsg', (msg) => { this.msg = msg }) }
|
B 组件(发送方),触发 Bus 的 $emit 事件
1
| Bus.$emit('sendMsg', '这是一个消息')
|
代码示例
EventBus.js
1 2 3
| import Vue from 'vue' const Bus = new Vue() export default Bus
|
BaseA.vue(接受方)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <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(发送方)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <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>
|
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <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>
|
4.2 provide&inject
跨层级共享数据
4.2.1 场景
4.2.2 语法
- 父组件 provide 提供数据
1 2 3 4 5 6 7 8 9 10
| export default { provide () { return { color: this.color, userInfo: this.userInfo, } } }
|
2.子/孙组件 inject 获取数据
1 2 3 4 5 6
| export default { inject: ['color','userInfo'], created () { console.log(this.color, this.userInfo) } }
|
4.2.3 注意
- provide 提供的简单类型的数据不是响应式的,复杂类型数据是响应式。(推荐提供复杂类型数据)
- 子/孙组件通过 inject 获取的数据,不能在自身组件内修改
5 v-model
5.1 原理
v-model 本质上是一个语法糖。例如应用在输入框上,就是 value 属性和 input 事件的合写
1 2 3 4 5 6 7 8
| <template> <div id="app" > <input v-model="msg" type="text"> <!-- 等价于 --> <input :value="msg" @input="msg = $event.target.value" type="text"> </div> </template>
|
5.1.1 作用
提供数据的双向绑定
- 数据变,视图跟着变
:value
- 视图变,数据跟着变
@input
5.1.2 注意
$event 用于在模板中,获取事件的形参
5.1.3 代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <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>
|
5.1.4 v-model 使用在其他表单元素上的原理
不同的表单元素, v-model 在底层的处理机制是不一样的。比如给 checkbox 使用 v-model,底层处理的是 checked 属性和 change 事件。
不过咱们只需要掌握应用在文本框上的原理即可
5.2 表单类组件封装
需求目标
实现子组件和父组件数据的双向绑定 (实现App.vue中的selectId和子组件选中的数据进行双向绑定)
代码演示
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <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>
|
5.3 v-model简化代码
5.3.1 目标
父组件通过 v-model 简化代码,实现子组件和父组件数据 双向绑定
5.3.2 如何简化
v-model 其实就是 :value 和 @input 事件的简写
- 子组件:props 通过 value 接收数据,事件触发 input
- 父组件:v-model 直接绑定数据
5.3.3 代码示例
子组件
这里 props 必须使用 value 这个词,发送时必须使用 input 这个函数名
1 2 3 4 5 6 7 8 9
| <select :value="value" @change="handleChange">...</select> props: { value: String }, methods: { handleChange (e) { this.$emit('input', e.target.value) } }
|
父组件
1
| <BaseSelect v-model="selectId"></BaseSelect>
|
6 .sync修饰符
6.1 作用
可以实现 子组件 与 父组件数据 的 双向绑定,简化代码
简单理解:子组件可以修改父组件传过来的props值
6.2 场景
封装弹框类的基础组件, visible 属性 true 显示 false 隐藏
6.3 本质
.sync修饰符 就是 :属性名 和 @update:属性名 合写
6.4 语法
父组件
1 2 3 4 5 6 7 8
| //.sync写法 <BaseDialog :visible.sync="isShow" /> -------------------------------------- //完整写法 <BaseDialog :visible="isShow" @update:visible="isShow = $event" />
|
子组件
名字需要注意,这里需要写 update:数据名
1 2 3 4 5
| props: { visible: Boolean },
this.$emit('update:visible', false)
|
6.5 代码示例
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <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>
|
BaseDialog.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| <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>
|
7 ref和$refs
7.1 作用
利用ref 和 $refs 可以用于 获取 dom 元素 或 组件实例
7.2 特点
查找范围 → 当前组件内(更精确稳定)
7.3 语法
1.给要获取的盒子添加 ref 属性
1
| <div ref="chartRef">我是渲染图表的容器</div>
|
2.获取时通过 $refs 获取 this.\$refs.chartRef
获取
1 2 3
| mounted () { console.log(this.$refs.chartRef) }
|
7.4 注意
之前只用document.querySelect(‘.box’) 获取的是整个页面中的盒子。如果遇到同名的元素,就无法区分。
7.5 代码示例
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <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>
|
BaseChart.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| <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>
|
8 异步更新 & $nextTick
8.1 需求
编辑标题, 编辑框自动聚焦
- 点击编辑,显示编辑框
- 让编辑框,立刻获取焦点
8.2 代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <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 // 2.让文本框聚焦 (会等dom更新完之后 立马执行nextTick中的回调函数) // this.$nextTick(() => { // console.log(this.$refs.inp) // this.$refs.inp.focus() // })
setTimeout(() => { this.$refs.inp.focus() }, 0) }, }, } </script>
<style> </style>
|
8.3 问题
“显示之后”,立刻获取焦点是不能成功的!
原因:Vue 是异步更新DOM (提升性能)
8.4 解决方案
$nextTick:等 DOM更新后,才会触发执行此方法里的函数体
语法: this.$nextTick(函数体)
1 2 3
| this.$nextTick(() => { this.$refs.inp.focus() })
|
注意:$nextTick 内的函数体 一定是箭头函数,这样才能让函数内部的 this 指向 Vue 实例。