08 自定义事件
v-model 原理
v-model 本质上是一个语法糖。例如应用在输入框上,就是
value属性 和input事件 的合写vue<template> <div id="app"> <input v-model="msg" type="text" /> <br /> <input :value="msg" @input="msg = $event.target.value" type="text" /> </div> </template>
作用
提供数据的双向绑定
- 数据变,视图跟着变
:value - 视图变,数据跟着变
@input
注意
$event 用于在模板中,获取事件的形参
vue
<template>
<div class="app">
<!-- v-model 的底层其实就是:value 和 @input 的简写 -->
<input type="text" v-model="msg1" />
<br />
<input type="text" :value="msg2" @input="msg2 = $event.target.value" />
</div>
</template>
<script>
export default {
data () {
return {
msg1: 'hello',
msg2: 'world',
}
},
}
</script>
<style>
input {
width: 200px;
height: 30px;
font-size: 20px;
padding: 0 10px;
border: 1px solid #ccc;
border-radius: 5px;
margin-bottom: 10px;
}
</style>js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: (h) => h(App),
}).$mount('#app')v-model 使用在其他表单元素上的原理
- 不同的表单元素,
v-model在底层的处理机制是不一样的。 - 比如给
checkbox使用v-model,底层处理的是checked属性和change事件。 - 不过咱们只需要掌握应用在文本框上的原理即可
表单类组件封装
- 实现子组件和父组件数据的双向绑定(实现 App.vue 中的 selectId 和子组件选中的数据进行双向绑定)
- 父组件通过
v-model简化代码,实现子组件和父组件数据 双向绑定
思路
v-model 其实就是 :value 和 @input 事件的简写
- 子组件:
props通过value接收数据,事件触发input - 父组件:
v-model直接绑定数据
相关代码
vue
<template>
<div>
<!-- 向父组件传递数据 -->
<select :value="value" @change="$emit('input', $event.target.value)">
<option value="101">北京</option>
<option value="102">上海</option>
<option value="103">武汉</option>
<option value="104">广州</option>
<option value="105">深圳</option>
</select>
<p>
value 为<span>{{ value }}</span>
</p>
</div>
</template>
<script>
export default {
props: {
value: String,
},
}
</script>
<style scoped>
select {
width: 200px;
height: 30px;
font-size: 20px;
padding: 0 10px;
border: 1px solid #ccc;
border-radius: 5px;
margin-bottom: 10px;
}
span {
background-color: #ffc0cb;
}
</style>vue
<template>
<div class="app">
<div class="box1">
<h2>使用 v-model</h2>
<BaseSelect v-model="selectId"></BaseSelect>
</div>
<!-- v-model 本质上是语法糖,等价于 :value="selectId" @input="selectId = $event" -->
<!--
使用 v-model 前提:
1. 组件内部 props 必须有 value 属性,用于接收父组件传递的数据
2. 组件内部必须有 input 事件,用于向父组件传递数据
-->
<div class="box2">
<h2>不使用 v-model</h2>
<BaseSelect :value="selectId" @input="selectId = $event"></BaseSelect>
</div>
</div>
</template>
<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
components: {
BaseSelect,
},
data () {
return {
selectId: '101',
}
},
}
</script>
<style scoped>
.app {
display: flex;
justify-content: flex-start;
gap: 20px;
margin: 20px 0 0 20px;
}
.box1,
.box2 {
width: 266px;
padding: 10px;
border: 2px solid #ccc;
background-color: #bfd8af;
border-radius: 6px;
box-shadow: 0 0 6px 6px #d4e7c5;
}
</style>.sync 修饰符
作用
- 可以实现 子组件 与 父组件数据 的 双向绑定,简化代码
- 简单理解:子组件可以修改父组件传过来的 props 值
场景
- 封装弹框类的基础组件,visible 属性 (true 显示、false 隐藏)
本质
.sync修饰符 就是:属性名和@update:属性名合写
语法
父组件
js// .sync 写法 <BaseDialog :visible.sync="isShow" /> // 完整写法 <BaseDialog :visible="isShow" @update:visible="isShow = $event" />子组件
jsprops: { visible: Boolean }, this.$emit('update:visible', false)
代码示例
vue
<script>
export default {
props: {
isShow: Boolean,
},
methods: {
closeDialog () {
this.$emit('update:isShow', false)
},
},
}
</script>
<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 @click="closeDialog">确认</button>
<button @click="closeDialog">取消</button>
</div>
</div>
</div>
</template>
<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>vue
<script>
import BaseDialog from './components/BaseDialog.vue'
export default {
components: {
BaseDialog,
},
data () {
return {
isShow: false,
}
},
methods: {
openDialog () {
this.isShow = true
// console.log(document.querySelectorAll('.box'));
},
},
}
</script>
<template>
<div class="app">
<div class="box box1">
<h2>使用完整写法</h2>
<button @click="openDialog">显示退出按钮</button>
<BaseDialog :isShow="isShow" @update:isShow="isShow = $event"></BaseDialog>
</div>
<!-- isShow.sync 等价于 :isShow="isShow" @update:isShow="isShow = $event" -->
<div class="box box2">
<h2>使用简写</h2>
<button @click="openDialog">显示退出按钮</button>
<BaseDialog :isShow.sync="isShow"></BaseDialog>
</div>
</div>
</template>
<style scoped>
.app {
display: flex;
flex-wrap: wrap;
}
.box1,
.box2 {
width: 200px;
margin: 20px;
padding: 10px;
border: 2px solid #ccc;
background-color: #bfd8af;
border-radius: 6px;
}
button {
width: 150px;
height: 36px;
margin: 20px 0 0 20px;
font-size: 20px;
background-color: #ccc;
border-radius: 5px;
border: none;
outline: none;
cursor: pointer;
}
</style>总结
父组件如果想让子组件修改传过去的值 必须加什么修饰符?
- 在 Vue 中,父组件通过
props将数据传递给子组件。如果父组件希望子组件能够修改这些通过props传递的值,可以使用.sync修饰符。 - 使用
.sync修饰符时,子组件可以直接修改propValue,并且父组件的propValue会在子组件内部修改后自动更新。这样可以更方便地实现子组件修改父组件传递的值的需求。
vue<!-- ParentComponent.vue --> <template> <div> <!-- 使用 .sync 修饰符传递数据给子组件 --> <ChildComponent :propValue.sync="parentValue" /> <p>{{ parentValue }}</p> </div> </template> <script> import ChildComponent from './ChildComponent.vue' export default { components: { ChildComponent, }, data() { return { parentValue: 'Initial value', } }, } </script>vue<!-- ChildComponent.vue --> <template> <div> <!-- 使用 .sync 修饰符修改父组件传递的值 --> <button @click="updateParentValue">Update Parent Value</button> </div> </template> <script> export default { props: { // 使用 .sync 修饰符声明 prop propValue: { type: String, default: '', }, }, methods: { // 使用 .sync 修饰符触发 update 事件,通知父组件更新 prop updateParentValue() { this.$emit('update:propValue', 'Updated value from child') }, }, } </script>- 在子组件中,使用
.sync修饰符声明的propValue可以直接在子组件内部修改,然后通过触发update:propValue事件通知父组件更新。 - 这样就实现了父组件将数据传递给子组件,并允许子组件修改传递的值的需求。
- 在 Vue 中,父组件通过
子组件要修改父组件的
props值 必须使用什么语法?父组件
js// .sync 写法 <BaseDialog :visible.sync="isShow" /> // 完整写法 <BaseDialog :visible="isShow" @update:visible="isShow = $event" />子组件
jsprops: { visible: Boolean }, this.$emit('update:visible', false)
ref 和 $refs
作用
- 利用
ref和$refs可以用于 获取 dom 元素 或 组件实例
- 利用
特点
- 查找范围 → 当前组件内 (更精确稳定)
ref
ref被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的$refs对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。html<!-- `vm.$refs.p` will be the DOM node --> <p ref="p">hello</p> <!-- `vm.$refs.child` will be the child component instance --> <child-component ref="child"></child-component>当
v-for用于元素或组件的时候,引用信息将是包含 DOM 节点或组件实例的数组。
关于 ref 注册时间的重要说明
- 因为
ref本身是作为渲染结果被创建的,在初始渲染的时候你不能访问它们 - 它们还不存在!$refs也不是响应式的,因此你不应该试图用它在模板中做数据绑定。 $refs只会在组件渲染完成之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组件的 "逃生舱"——你应该避免在模板或计算属性中访问$refs。
语法
给要获取的盒子添加
ref属性html<div ref="chartRef">我是渲染图表的容器</div>获取时通过
$refs获取this.$refs.chartRef获取jsmounted () { console.log(this.$refs.chartRef) }
注意
- 之前只用
document.querySelect('.box')获取的是整个页面中的盒子
代码示例
vue
<template>
<div class="base-chart-box1" ref="baseChartBox">子组件</div>
</template>
<script>
// pnpm i -D echarts
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口
import * as echarts from 'echarts'
export default {
mounted () {
// 基于准备好的 dom,初始化 echarts 实例
// document.querySelector 会查找项目中所有的元素
// $refs 只会在当前组件查找盒子
// var myChart = echarts.init(document.querySelector('.base-chart-box'))
const myChart = echarts.init(this.$refs.baseChartBox)
// 绘制图表
myChart.setOption({
title: {
text: '基础柱状图 Basic Bar',
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
},
yAxis: {
type: 'value',
},
series: [
{
data: [120, 200, 150, 80, 70, 110, 130],
type: 'bar',
},
],
})
},
}
</script>
<style scoped>
.base-chart-box1 {
width: 400px;
height: 300px;
border: 3px solid #000;
border-radius: 6px;
}
</style>vue
<template>
<div class="base-chart-box2" ref="baseChartBox">子组件</div>
</template>
<script>
// pnpm i -D echarts
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口
import * as echarts from 'echarts'
export default {
mounted () {
// 基于准备好的 dom,初始化 echarts 实例
// document.querySelector 会查找项目中所有的元素
// $refs 只会在当前组件查找盒子
// var myChart = echarts.init(this.$refs.baseChartBox);
const myChart = echarts.init(document.querySelector('.base-chart-box2'))
// 绘制图表
myChart.setOption({
title: {
text: '基础柱状图 Basic Bar',
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
},
yAxis: {
type: 'value',
},
series: [
{
data: [120, 200, 150, 80, 70, 110, 130],
type: 'bar',
},
],
})
},
}
</script>
<style scoped>
.base-chart-box2 {
width: 400px;
height: 300px;
border: 3px solid #000;
border-radius: 6px;
}
</style>vue
<template>
<div class="app">
<!--
1. 子组件中使用 echarts.init(document.querySelector('.base-chart-box')); 会查找项目中所有的 .base-chart-box,所以会在父组件中查找到 .base-chart-box
2. 子组件中使用 echarts.init(this.$refs.baseChartBox); 只会在当前组件(子组件)中查找 .base-chart-box
-->
<div class="box1">
<h2>使用 this.$refs</h2>
<div class="base-chart-box1">这是一个捣乱的盒子 (和子组件中的要渲染的元素类名同名)</div>
<BaseChartA></BaseChartA>
</div>
<div class="box2">
<h2>使用 document.querySelector</h2>
<div class="base-chart-box2">这是一个捣乱的盒子 (和子组件中的要渲染的元素类名同名)</div>
<BaseChartB></BaseChartB>
</div>
</div>
</template>
<script>
import BaseChartA from './components/BaseChartA.vue'
import BaseChartB from './components/BaseChartB.vue'
export default {
components: {
BaseChartA,
BaseChartB,
},
}
</script>
<style scoped>
.app {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.box1,
.box2 {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
width: 500px;
height: 700px;
border: 2px solid #ccc;
border-radius: 6px;
background-color: #f5f5f5;
}
.base-chart-box1,
.base-chart-box2 {
width: 400px;
height: 300px;
border: 3px solid #000;
border-radius: 6px;
}
</style>异步更新 & $nextTick
Vue.nextTick( [callback, context] ):在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。语法:
this.$nextTick(函数体)jsthis.$nextTick(() => { this.$refs.inp.focus() })
注意
$nextTick 内的函数体 一定是箭头函数,这样才能让函数内部的 this 指向 Vue 实例
vue
<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()
})
// this.$nextTick: 用于在下次 DOM 更新循环结束之后执行回调。它会在 Vue 组件更新完毕之后执行,可以确保你在回调中访问到最新的 DOM
// setTimeout: 主要用于在一定的延迟之后执行一些代码,不一定与 DOM 更新相关
// setTimeout(() => {
// this.$refs.inp.focus()
// }, 0)
},
},
}
</script>
<template>
<div class="app">
<div v-if="isShowEdit">
<input type="text" v-model="editValue" ref="inp" />
<button @click="isShowEdit = false">确认</button>
</div>
<div v-else>
<span>{{ title }}</span>
<button @click="editFn">编辑</button>
</div>
</div>
</template>
<style scoped></style>