day04
目录- day04
scoped 解决样式冲突
- 因为只写
<style>
的话是全局样式,影响所有组件,而scoped
下样式只作用于当前组件
默认情况
-
写在组件中的样式会全局生效
- 因此很容易造成多个组件之间的样式冲突问题
-
全局样式:默认组件中的样式会作用到全局,任何一个组件中都会受到此样式的影响
- 子组件的也会影响到父组件和兄弟组件
-
局部样式:可以给组件加上 scoped 属性,可以让样式只作用于当前组件
scoped 原理
-
当前组件内标签都被添加
data-v-hash值
的属性- 不同组件有不同的哈希值就可以来区分了
-
css 选择器都被添加
data-v-hash值
的属性选择器-
F12 查看样式就会发现标签的后面都加了
[data-v-hash值]
(被自动处理,添加上了属性选择器)
-
-
最终效果:必须是当前组件的元素,才会有这个自定义属性,才会被这个样式作用到
data 必须是一个函数
-
一个组件的 data 选项必须是一个函数。目的是为了:保证每个组件实例,维护独立的一份数据对象
- 即:
data() { return { ...... }}
- 即:
-
每次创建新的组件实例,都会新执行一次 data 函数,得到一个新对象
- 即一个父组件中引入使用了若干个子组件,每个子组件的 data 互不影响、相互独立
- 若是还是写对象
data: { ...... }
就会报错
组件通信
-
组件通信,就是指组件与组件之间的数据传递
-
组件的数据本是独立的,无法直接访问其他组件的数据
-
想使用其他组件的数据,就需要组件通信
通信解决方案
- 下图三种
父子通信流程
- 父组件通过
props
将数据传递给子组件- 父组件
<子组件 :xxx="data里return的数据"></子组件>
- 子组件
props:['xxx']
便可以获取使用xxx
值了
- 父组件
- 子组件利用
$emit
通知父组件修改更新- 子组件加个事件触发,在事件里
this.$emit('yyy', '想传的值')
- 父组件
@yyy="父组件里的事件名"
获取,父组件里的事件名(值){ 就可以对值进行修改了 }
- 子组件加个事件触发,在事件里
props
-
Props 定义:组件上注册的一些自定义属性
-
Props 作用:向子组件传递数据
特点
- 可以传递任意数量的 prop
- 可以传递任意类型的 prop
props 校验
作用
- 为组件的 prop 指定验证要求,不符合要求,控制台就会有错误提示,帮助开发者,快速发现错误
语法
- 类型校验
- 非空校验
- 默认值
- 自定义校验
props 校验完整写法
props: {
校验的属性名: {
type: 类型, // Number String Boolean ...
required: true, // 是否必填
default: 默认值, // 默认值
validator (value) { // value就是传来的值
// 自定义校验逻辑
return 布尔类型
}
}
},
注意
-
default 和 required 一般不同时写(因为当时必填项时,肯定是有值的)
-
default 后面如果是简单类型的值,可以直接写默认。如果是复杂类型的值,则需要以函数的形式 return 一个默认值
props & data、单向数据流
共同点
- 都可以给组件提供数据
区别
-
data 的数据是自己的
- 随便改
-
prop 的数据是外部的
- 不能直接改,要遵循单向数据流
单向数据流:
- 父级 props 的数据更新,会向下流动,影响子组件。这个数据流动是单向的
非父子通信 — event bus 事件总线
作用
- 非父子组件之间,进行简易消息传递(复杂场景还是:Vuex)
步骤
-
创建一个都能访问的事件总线(空的 Vue 实例)
import Vue from 'vue' const Bus = new Vue() // 创建一个空的实例 export default Bus // 导出,使其他组件可以引入
- 最好可以新建一个文件,在新文件里创建 js 文件来放空的 Vue 实例部分
- Bus 就是一个中间总线,使不相关的 A 和 B 组件有了相互的响应
-
A 组件(接受方),监听 Bus 的 $on 事件
created () { // 一进页面就监听 Bus.$on('sendMsg', (msg) => { // $on 就是进行事件监听 this.msg = msg }) }
-
B 组件(发送方),触发 Bus 的 $emit 事件(例如
@click
触发)Bus.$emit('sendMsg', '这是一个消息')
- 触发此事件后,任何监听
sendMsg
这一个消息的都能后收到做出操作
- 触发此事件后,任何监听
-
这样在 B 点击事件触发后,A 组件就会更新信息为 B 的
'这是一个消息'
非父子通信 — provide & inject
作用
- 跨层级共享数据
- 实际还是有所联系,只不过可能是爷孙而非父子关系
场景
语法
-
父组件 provide 提供数据
-
vue3 就又是不同的写法了
provide('changeDiv', () => { 触发自定义事件() })
export default { provide () { return { // 普通类型【非响应式】 color: this.color, // 复杂类型【响应式】 userInfo: this.userInfo, } } }
-
-
子 / 孙组件 inject 获取数据
- vue3 就又是不同的写法了
const changeDiv = inject('changeDiv')
,自定义事件触发changeDiv()
export default { inject: ['color','userInfo'], created () { console.log(this.color, this.userInfo) } }
- vue3 就又是不同的写法了
注意
- provide 提供的简单类型的数据不是响应式的,复杂类型数据是响应式(所以还是推荐提供复杂类型数据)
- 子 / 孙组件通过 inject 获取的数据,不能在自身组件内修改
v-model
原理
-
v-model 本质上是一个语法糖(代码语法的简写)。例如应用在输入框上,就是 value 属性和 input 事件的合写
<template> <div id="app" > <input v-model="msg" type="text"> <input :value="msg" @input="msg = $event.target.value" type="text"> </div> </template>
作用
-
提供数据的双向绑定
-
数据变,视图跟着变
:value
-
视图变,数据跟着变
@input
注意
- 在模板中,用来获取事件的形参的是
$event
- 放进
@input
里,形参就是事件对象e
,e.target
就是触发事件的事件源 —— 输入框,.value
拿到输入框的值,然后实时地赋给msg
v-model 使用在其他表单元素上的原理
- 不同的表单元素, v-model 在底层的处理机制是不一样的。比如给 checkbox 使用 v-model
- 底层处理的是 checked 属性和 change 事件
- 但设计思路一样
表单类组件封装
需求目标
-
父表单组件里有个下拉的子组件
- 子组件的下拉数据来自于父
- 而下拉是更改,但子本不能改父传来的值
- 所以要
v-model
拆解开,取其底层来绑定数据
-
实现子组件和父组件数据的双向绑定
代码演示
- 父 App.vue
<template>
<div class="app">
<!-- 这里直接用 $event 就可以拿到子传来的形参:e.target.value -->
<BaseSelect :cityId="selectId" @事件名="selectedId = $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="cityId" @change="handleChange">
<!-- 这里就不能用v-model,但是可以用拆解后的样式实现 -->
<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: {
cityId: String
},
methods: {
handleChange(e){
// 每次把新值传给父
this.$emit('事件名', e.target.value)
}
}
}
</script>
<style>
</style>
v-model 简化上述的代码
目标
- 父组件通过
v-model
简化代码,实现子组件和父组件数据双向绑定
如何简化
-
v-model
其实就是:value
和@input
事件的简写 -
子组件:
props
通过value
接收数据,事件触发input
-
父组件:
v-model
直接绑定数据
代码示例
- 子组件
<select :value="value" @change="handleChange">...</select>
props: {
<!-- 这里的值得固定是 value 了 -->
value: String
},
methods: {
handleChange (e) {
<!-- 这里也得固定是 input 事件名了 -->
this.$emit('input', e.target.value)
}
}
- 父组件
<BaseSelect v-model="selectId"></BaseSelect>
<!-- :value + @input -->
.sync 修饰符
作用
-
可以实现子组件与父组件数据的双向绑定,简化代码
- prop 是自定义的,非固定为 value
- 需要传的并不全是 value 类型的,例如弹窗类的组件都是用 visible 属性双向绑定传布尔值的
-
简单理解:子组件可以修改父组件传过来的 props 值
本质
- .sync 修饰符就是
:属性名
和@update:属性名
合写
语法
-
父组件
// .sync写法 <BaseDialog :visible.sync="isShow" /> // 等价于: <BaseDialog :visible="isShow" @update:visible="isShow = $event" />
-
子组件(就需要配合父组件固定写为 visiable 和
update:visiable
了)props: { visible: Boolean }, // 方法里 this.$emit('update:visible', false)
总结
- 当想父子组件双向绑定更新,但传值不是 value(就没法用 v-model),而是别的类型的 prop 的话,就用
xxx属性名.sync=""
来绑定,子组件用xxx属性名
和update:xxx属性名
配合
ref 和 $refs
作用
- 利用
ref
和$refs
可以用于获取 dom 元素或组件实例
特点
- 查找范围:当前组件内(更精确稳定)
语法
-
给要获取的盒子添加 ref 属性
<div ref="chartRef">我是渲染图表的容器</div>
-
获取时,在恰当的时候(dom 渲染完之后)通过
this.$refs.chartRef
获取mounted () { console.log(this.$refs.chartRef) }
代码示例 —— 获取 dom 元素
- App.vue
<template>
<div class="app">
<div class="base-chart-box">不需要的但class相同的div</div>
<BaseChart></BaseChart>
</div>
</template>
<script>
import BaseChart from './components/BaseChart.vue'
export default {
components:{
BaseChart
}
}
</script>
- BaseChart.vue
<template>
<div class="base-chart-box" ref="baseChartBox">正确需要的子组件盒子</div>
</template>
<script>
// yarn add echarts 或者 npm i echarts
import * as echarts from 'echarts'
export default {
mounted() {
// 基于准备好的dom,初始化echarts实例
// const myChart = echarts.init(document.querySelect('.base-chart-box'))
const myChart = echarts.init(this.$refs.mychart) // 更好的方法
// 绘制图表
myChart.setOption({
title: {...},
tooltip: {...},
xAxis: {...},
yAxis: {...},
series: [...],
// ......
})
},
}
</script>
- 原始的
document.querySelect('.base-chart-box')
查找的是整个页面,若是有多个 class 为base-chart-box
的 div 盒子的话(例如在父组件里就有),就会找第一个盒子(但所需要的是子组件里本组件的 div 盒子)
代码示例 —— 获取组件实例
- 在子组件
BaseForm
中有一个方法,父组件也想直接使用这个方法:- 先给父组件调用子组件的标签加上 ref:
<BaseForm ref="baseForm"></BaseForm>
- 在父组件里就可以使用
this.$refs.baseForm
获取到组件实例,可以打印出来看看,内部展开就有子组件的方法,那就可以直接调用了 - 在父组件的方法里调用子组件已有的方法:
this.$refs.baseForm.任一方法名()
- 先给父组件调用子组件的标签加上 ref:
异步更新 & $nextTick
需求
- 编辑标题, 编辑框自动聚焦:
- 点击编辑,显示编辑框
- 显示的时候,立刻让编辑框获取焦点
代码实现
<template>
<div class="app">
<div v-if="isShowEdit">
<input type="text" v-model="editValue" ref="xxinp" />
<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
// 用ref获取元素,从而获取焦点
this.$refs.xxinp.focus()
}
},
}
</script>
问题
- 上述代码看似没问题,但实际不是如此
- " 显示之后 ",立刻获取焦点是不能成功的
- 原因:Vue 是异步更新 DOM(为了提升性能)
- 虽然显示了,但 dom 其实还没真正解析渲染完成
解决方案
-
$nextTick
:等 DOM 更新后,才会(立刻)触发执行此方法里的函数体 -
语法:
this.$nextTick(函数体)
methods: { editFn() { // 显示输入框 this.isShowEdit = true // 使用$nextTick this.$nextTick(() => { this.$refs.inp.focus() }) } }, // 虽然用setTimeout(() => { this.$refs.inp.focus() }, 1000)一般也没有问题,一秒钟足够渲染成功了,但是 ——> 还是$nextTick更精准(等 DOM 更新后就立刻执行函数体)
-
注意:
$nextTick
内的函数体一定是箭头函数,这样才能让函数内部的 this 指向 Vue 实例