文章目录
-
- 一、v-model:双向数据绑定指令
- [二、ref 属性:获取 DOM 和组件实例](#二、ref 属性:获取 DOM 和组件实例)
-
- [2.1 ref 属性是什么?](#2.1 ref 属性是什么?)
- [2.2 使用步骤](#2.2 使用步骤)
- [2.3 获取原生 DOM](#2.3 获取原生 DOM)
- [2.4 实战案例:绘制 ECharts 图表](#2.4 实战案例:绘制 ECharts 图表)
-
- [第一步:安装 ECharts](#第一步:安装 ECharts)
- 第二步:编写图表组件
- [第三步:在 App 中使用](#第三步:在 App 中使用)
- [2.5 ref 属性获取组件实例(调用组件方法)](#2.5 ref 属性获取组件实例(调用组件方法))
- [三、nextTick:等 DOM 更新完成再操作](#三、nextTick:等 DOM 更新完成再操作)
-
- [3.1 是什么?](#3.1 是什么?)
- [3.2 为什么要用它?](#3.2 为什么要用它?)
- [3.3 什么时候用?](#3.3 什么时候用?)
- 全文总结
- 核心知识点复盘
本文基于 Vue3
<script setup>语法,从概念到原理再到实战,循序渐进拆解三个高频核心特性。适合有一点点 Vue 基础、想系统深入理解这些知识点的同学阅读。
一、v-model:双向数据绑定指令
1.1 什么是双向数据绑定?
在 Vue 中,数据驱动视图 是我们最熟悉的模式------数据一变,页面自动更新。但很多场景下,我们还需要视图也能反过来修改数据,比如输入框、下拉框、复选框等表单控件。
双向数据绑定 就是:数据 ↔ 视图,互相影响。
- 数据 → 视图:JS 中的响应式数据变化,页面自动渲染新值。
- 视图 → 数据:用户在表单中输入/选择,JS 中的数据同步更新。
v-model 就是 Vue 提供的双向数据绑定指令,一个指令完成上面两件事。
1.2 v-model 的本质:语法糖
核心认知 :
v-model本身只是一个语法糖,它底层由两个东西组合而成。
以最常用的原生 <input> 为例:
html
<script setup>
import { ref } from 'vue'
const msg = ref('')
</script>
<template>
<p>{{ msg }}</p>
<!-- v-model 写法:一行搞定 -->
<input type="text" v-model="msg" />
<!-- 👆 上面那行,完全等价于下面这行 👇 -->
<input
type="text"
:value="msg"
@input="msg = $event.target.value"
/>
</template>
拆解原理:
| 语法糖部分 | 对应的底层实现 | 负责的方向 |
|---|---|---|
v-model="msg" |
:value="msg" |
数据 → 视图(数据变了,input 的 value 跟着变) |
v-model="msg" |
@input="msg = $event.target.value" |
视图 → 数据 (用户输入,$event.target.value 拿到输入内容,更新 msg) |
关键说明 :在原生标签上,$event 就是原生的事件对象,$event.target 指向触发事件的 DOM 元素,$event.target.value 就是用户当前输入的值。
面试重点 :v-model 是语法糖,本质是
:value绑定 +@input监听,数据变了更新视图,视图变了更新数据,双向同步。
1.3 v-model 作用在组件标签上
v-model 不仅可以用于原生表单元素,还可以用在自定义组件上,实现父子组件之间的双向数据流。
基本用法
html
<!-- 父组件:App.vue -->
<script setup>
import { ref } from 'vue'
import BitSelect from './components/BitSelect.vue'
// 当前选中的城市 id
const activeId = ref('333')
</script>
<template>
<!-- 在组件标签上用 v-model -->
<BitSelect v-model="activeId" />
<!-- 👆 完全等价于 👇 -->
<BitSelect
:modelValue="activeId"
@update:modelValue="activeId = $event"
/>
</template>
特别说明 :在组件标签上使用 v-model,本质就是通过
modelValue属性父传子 ,通过update:modelValue事件子传父。
子组件如何接收?
html
<!-- 子组件:BitSelect.vue -->
<script setup>
// 接收父组件传来的 modelValue 属性
const props = defineProps({
modelValue: String
})
// 声明要触发的自定义事件
const emit = defineEmits()
</script>
<template>
<!--
原生 select 标签上的 $event 就是事件对象
$event.target.value:选中的 option 的 value 值
-->
<select
:value="props.modelValue"
@change="emit('update:modelValue', $event.target.value)"
>
<option value="111">北京</option>
<option value="222">上海</option>
<option value="333">深圳</option>
<option value="444">杭州</option>
<option value="555">苏州</option>
</select>
</template>
数据流总结:
父组件 activeId ──:modelValue──► 子组件 props.modelValue(显示选中项)
│
用户选择城市(change 事件)
│
父组件 activeId ◄──emit('update:modelValue', 新值)──┘
重要注意 :组件标签上的
$event和原生标签上的不一样。在组件标签上,$event指的是子组件emit抛出的参数(这里是选中的城市 id),而不是原生事件对象。
1.4 defineModel():简化子组件代码
Vue3.4+ 提供了 defineModel(),让子组件的 v-model 代码大幅简化。
html
<!-- 子组件:BitSelect.vue(使用 defineModel 简化后) -->
<script setup>
// defineModel() 返回一个 ref 响应式数据
// - 初始值来自父组件传递的 v-model 值
// - 可读可写,子组件直接修改会自动同步到父组件
const model = defineModel()
</script>
<template>
<!-- 直接用 v-model 绑定 model,代码大幅精简! -->
<select v-model="model">
<option value="111">北京</option>
<option value="222">上海</option>
<option value="333">深圳</option>
<option value="444">杭州</option>
<option value="555">苏州</option>
</select>
</template>
对比一下:
| 对比维度 | 传统写法 | defineModel() 写法 |
|---|---|---|
| 接收数据 | defineProps({ modelValue: String }) |
❌ 不需要 |
| 声明事件 | defineEmits() |
❌ 不需要 |
| 模板绑定 | :value="props.modelValue" + @change="emit(...)" |
直接 v-model="model" |
| 代码行数 | 约 5-8 行核心逻辑 | 1 行搞定 |
核心原理 :
defineModel()底层自动帮我们做了defineProps({ modelValue })+emit('update:modelValue'),开发者只需要把它当成一个普通的 ref 数据来读和写。
完整的使用范式:
父组件:<Xxx v-model="父的响应式数据" /> ← 父传子 + 子传父,双向
子组件:const model = defineModel() ← 可读可写,自动同步
二、ref 属性:获取 DOM 和组件实例
2.1 ref 属性是什么?
⚠️ 先区分 :这里讲的是标签上的
ref属性 (attribute),不是前面学的ref()函数。两个概念不同,但配合使用。
- ref 属性:写在模板标签上的一个特殊属性,Vue 新增,原生 HTML 不具备。
- 作用 :用来获取原生 DOM 元素 或组件实例(进而调用组件暴露的方法)。
- 使用方式 :准备一个
ref()响应式数据 → 绑定到标签上 → 在合适的时机通过.value获取。
2.2 使用步骤
步骤1:准备一个 ref 响应式数据(例如:const divRef = ref(null))
步骤2:在目标标签上绑定 ref 属性(例如:<div ref="divRef">)
步骤3:在恰当的生命周期时机(如 onMounted),通过 数据.value 获取
2.3 获取原生 DOM
html
<script setup>
import { onMounted, ref } from 'vue'
// 步骤1:准备 ref 数据(初始值给 null)
const divRef = ref(null)
// 步骤3:组件挂载后,DOM 已就绪,可以拿到
onMounted(() => {
// divRef.value 就是原生的 <div> DOM 节点
console.log(divRef.value) // <div>Some Text...</div>
// 可以像原生 JS 一样操作 DOM
divRef.value.style.color = 'red'
})
</script>
<template>
<!-- 步骤2:在目标标签上绑定 ref 属性 -->
<div ref="divRef">Some Text...</div>
</template>
关键时机 :
onMounted之后才能拿到 ref.value,因为在此之前 DOM 还没有渲染出来,ref.value为null。
2.4 实战案例:绘制 ECharts 图表
在 Vue 中使用 ECharts(或任何需要直接操作 DOM 的第三方库),ref 属性是必不可少的一环。
第一步:安装 ECharts
bash
npm i echarts
第二步:编写图表组件
html
<!-- components/bit-chart.vue -->
<script setup>
import * as echarts from 'echarts'
import { onMounted, ref } from 'vue'
// 准备 ref,用于获取图表容器 div
const divRef = ref(null)
onMounted(() => {
// 1. 基于获取到的 DOM,初始化 echarts 实例
const myChart = echarts.init(divRef.value)
// 2. 配置图表选项(此处以玫瑰图为例)
myChart.setOption({
legend: {
top: 'bottom'
},
toolbox: {
show: true,
feature: {
mark: { show: true },
dataView: { show: true, readOnly: false },
restore: { show: true },
saveAsImage: { show: true }
}
},
series: [
{
name: 'Nightingale Chart',
type: 'pie',
radius: [50, 250],
center: ['50%', '50%'],
roseType: 'area',
itemStyle: {
borderRadius: 8
},
data: [
{ value: 40, name: 'rose 1' },
{ value: 38, name: 'rose 2' },
{ value: 32, name: 'rose 3' },
{ value: 30, name: 'rose 4' },
{ value: 28, name: 'rose 5' },
{ value: 26, name: 'rose 6' },
{ value: 22, name: 'rose 7' },
{ value: 18, name: 'rose 8' }
]
}
]
})
})
</script>
<template>
<!-- 图表容器:必须设置宽高,否则 ECharts 无法渲染 -->
<div class="chart" ref="divRef"></div>
</template>
<style lang="scss" scoped>
.chart {
width: 800px;
height: 500px;
margin: 100px auto;
border: 3px solid palevioletred;
}
</style>
第三步:在 App 中使用
html
<script setup>
import BitChart from './components/chart.vue'
</script>
<template>
<BitChart />
</template>
关键点复盘:
| 步骤 | 说明 |
|---|---|
const divRef = ref(null) |
创建 ref 数据,初始为 null |
<div ref="divRef"> |
将 ref 数据绑定到目标 DOM |
onMounted(() => { ... }) |
确保 DOM 渲染完成后获取 |
echarts.init(divRef.value) |
用获取到的原生 DOM 初始化图表 |
| 容器必须有宽高 | ECharts 需要明确的容器尺寸才能绘制 |
2.5 ref 属性获取组件实例(调用组件方法)
ref 不仅能拿原生 DOM,还能获取组件实例,从而调用子组件暴露出来的方法。这在表单校验等场景中非常常用。
场景:表单校验
子组件(MyForm)------提供校验方法:
html
<!-- components/MyForm.vue -->
<script setup>
// 表单组件提供的校验方法(模拟)
const validate = () => {
// 实际项目中这里会有正经的校验逻辑
return Math.random() > 0.5 ? true : false
}
// ⚠️ 关键:必须通过 defineExpose 暴露,父组件才能访问到
defineExpose({
ccc: 100, // 也可以暴露数据
validate // 暴露校验方法
})
</script>
<template>
<div class="form-box">
账号:<input type="text" /><br /><br />
密码:<input type="password" /><br /><br />
</div>
</template>
父组件(App)------使用 ref 调用子组件方法:
html
<script setup>
import { ref } from 'vue'
import MyForm from './components/MyForm.vue'
// 准备 ref,绑定到 MyForm 组件标签上
const formRef = ref(null)
// 点击登录按钮
const onLogin = () => {
// 通过 formRef.value 拿到组件实例
// 再调用子组件暴露的 validate() 方法
if (formRef.value.validate()) {
console.log('校验通过 ✅')
} else {
console.log('校验失败 ❌')
}
}
</script>
<template>
<div class="login-box">
<!-- 在组件标签上绑定 ref -->
<MyForm ref="formRef" />
<button @click="onLogin">登 录</button>
</div>
</template>
🔑 核心知识点 :
<script setup>组件默认是封闭的 ,父组件无法直接访问子组件内部的方法或数据。必须通过defineExpose()显式暴露,父组件才能通过ref.value.xxx访问到。这一点就像 ES6 模块的export。
数据流图:
父组件 子组件 MyForm
┌─────────────┐ ┌─────────────────┐
│ formRef │──── ref 绑定 ─────►│ <script setup> │
│ │ │ │
│ 点击登录 → │ │ validate() 方法 │
│ │ │ ↓ │
│ formRef │◄─── 组件实例 ──────│ defineExpose() │
│ .value │ │ 暴露 validate │
│ .validate() │ │ │
└─────────────┘ └─────────────────┘
三、nextTick:等 DOM 更新完成再操作
3.1 是什么?
nextTick() 是 Vue3 提供的一个工具方法,用于在下次 DOM 更新循环结束后执行回调。
3.2 为什么要用它?
Vue 的 DOM 更新是异步的、批量的。当你修改响应式数据时,Vue 不会立刻更新 DOM,而是把更新任务放进一个队列,在下一个「tick」统一执行。这样做的好处是------如果同一轮事件循环中多次修改同一个数据,Vue 只更新一次 DOM,性能更好。
但副作用是 :数据变了之后,如果你立刻去读取 DOM,读到的还是旧的内容。
html
<script setup>
import { ref, nextTick } from 'vue'
const count = ref(0)
const btnRef = ref(null)
const increment = async () => {
// 1. 修改数据
count.value++
// 2. 此时立即读取 DOM ------ 拿到的还是旧值!
// console.log(btnRef.value.textContent) // 还是 "0",不是 "1"
// 3. 解决方案:用 nextTick 等 DOM 更新完成
// 方式一:async/await(推荐)
await nextTick()
console.log(btnRef.value.textContent) // 现在拿到的是 "1" ✅
// 方式二:回调函数
// nextTick(() => {
// console.log(btnRef.value.textContent) // "1" ✅
// })
}
</script>
<template>
<button @click="increment" ref="btnRef">
{{ count }}
</button>
</template>
两种使用方式对比:
| 方式 | 写法 | 适用场景 |
|---|---|---|
async/await |
await nextTick() |
推荐,代码更直观,像同步代码一样读 |
| 回调函数 | nextTick(() => { ... }) |
传统风格,小型任务也够用 |
3.3 什么时候用?
记忆口诀:数据变了,想操作更新后的 DOM,如果直接拿不到,就用
nextTick。
典型场景:
- 修改数据后需要获取元素的尺寸、位置
- 修改列表数据后需要滚动到某个位置
- 修改
v-if/v-show后需要操作刚出现的元素
不过说实话 ,nextTick 在实际开发中的使用频次不算特别高,但关键时候能救场。脑海中留个印象,知道有这个方法,遇到「改了数据但 DOM 没跟上」的问题时,第一反应就是它。
全文总结
本文围绕 Vue3 的三个核心特性逐一展开:
-
v-model :Vue 的双向数据绑定指令,本质是
:value(数据 → 视图)+@input(视图 → 数据)的语法糖。用在组件上则映射为modelValue属性 +update:modelValue事件。Vue3.4+ 推出的defineModel()进一步简化了子组件代码------一行替代原来的 props + emit 模式。 -
ref 属性 :不同于
ref()函数,它是写在标签上的特殊属性,用于获取原生 DOM 或组件实例。获取 DOM 常用于引入 ECharts 等第三方 DOM 操作库;获取组件实例常用于调子组件暴露的方法(如表单校验),需配合defineExpose()使用。 -
nextTick :因为 Vue 的 DOM 更新是异步的,修改响应式数据后不能立即拿到最新 DOM。这时用
nextTick()等 Vue 完成 DOM 更新再操作即可。
核心知识点复盘
| 知识点 | 核心要点 |
|---|---|
| v-model 本质 | 语法糖 = :value + @input(原生)/ :modelValue + @update:modelValue(组件) |
| defineModel() | 返回可读可写的 ref,直接操作即同步父组件,底层自动处理 props + emit |
| ref 属性 | 获取原生 DOM 或组件实例,需在 onMounted 后才能拿到(DOM 已渲染) |
| defineExpose() | <script setup> 组件默认封闭,暴露给父组件的方法/数据需显式声明 |
原生 vs 组件 $event |
原生标签上是事件对象,组件标签上是子组件 emit 传递的参数 |
| nextTick | Vue DOM 更新是异步的,数据变了想拿最新 DOM,就用 nextTick |
如果这篇文章对你有帮助,欢迎点赞、收藏、转发。有问题也可以在评论区交流讨论 🙌