Vue3 核心特性深度解析:v-model、ref 属性与 nextTick 完全指南

文章目录

    • 一、v-model:双向数据绑定指令
      • [1.1 什么是双向数据绑定?](#1.1 什么是双向数据绑定?)
      • [1.2 v-model 的本质:语法糖](#1.2 v-model 的本质:语法糖)
      • [1.3 v-model 作用在组件标签上](#1.3 v-model 作用在组件标签上)
      • [1.4 defineModel():简化子组件代码](#1.4 defineModel():简化子组件代码)
    • [二、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.valuenull


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 的三个核心特性逐一展开:

  1. v-model :Vue 的双向数据绑定指令,本质是 :value(数据 → 视图)+ @input(视图 → 数据)的语法糖。用在组件上则映射为 modelValue 属性 + update:modelValue 事件。Vue3.4+ 推出的 defineModel() 进一步简化了子组件代码------一行替代原来的 props + emit 模式。

  2. ref 属性 :不同于 ref() 函数,它是写在标签上的特殊属性,用于获取原生 DOM 或组件实例。获取 DOM 常用于引入 ECharts 等第三方 DOM 操作库;获取组件实例常用于调子组件暴露的方法(如表单校验),需配合 defineExpose() 使用。

  3. 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

如果这篇文章对你有帮助,欢迎点赞、收藏、转发。有问题也可以在评论区交流讨论 🙌

相关推荐
东方小月2 小时前
从零开发一个 Coding Agent(四):使用状态机校验大模型事件流
前端·人工智能·后端
想做后端的前端2 小时前
WebGL实现FPS游戏
vue.js·游戏·webgl
Csvn3 小时前
🧩 ESM vs CJS 混用的 7 个「天坑」——从 TypeScript 编译到 Node 与浏览器
前端
Csvn3 小时前
🎯 Web 性能 API 集合:Performance Observer 的 5 个冷门妙用
前端
Csvn3 小时前
深入 React 闭包陷阱:从根源上理解并根治 stale closure
前端
whyfail3 小时前
前端学 Spring Boot(8):接口为什么越用越慢?
前端·spring boot·后端
用户059540174464 小时前
LangChain 记忆测试踩坑实录:这两个坑让我排查了 4 小时
前端·css
程序员黑豆4 小时前
鸿蒙应用开发:@Monitor 装饰器使用教程
前端·harmonyos
用户938515635074 小时前
从零构建《天龙八部》知识库:EPUB 加载→文本分割→向量嵌入→Milvus 存储→RAG 问答,一条链路打通
javascript·人工智能·全栈
SamChan905 小时前
在Web应用中集成PDF多语言翻译功能:PDFTranslator API实战指南
前端·python·ai·pdf·yapi·机器翻译