VUE 21 篇

Vue 是一个 JavaScript 框架,它本质上就是一堆 .js 文件。要开始用 Vue,你需要两样东西:

  1. Node.js — 一个能让你在电脑上运行 JavaScript 的环境(不仅仅是浏览器里)
  2. Vite — 一个帮你创建 Vue 项目的工具(脚手架) 💡 Tip:类比一下 如果把写 Vue 项目比作盖房子:
  • Node.js = 施工队(负责干活)
  • Vite = 图纸 + 工具(帮你搭好骨架)

第一步:安装 Node.js

nodejs.org 下载 LTS 版本(长期支持版,最稳定),像安装普通软件一样点"下一步"装好即可。 装完后打开终端(PowerShell 或 cmd),验证一下:

node -v      # 能看到版本号就说明装好了,比如 v20.11.0
npm -v       # npm 是 Node 自带的包管理器,也会显示版本号

第二步:创建项目

在终端中进入你想放项目的文件夹,然后运行:

npm create vue@latest

这条命令会问你几个问题:

  • Project name(项目名):给你的项目起个名字,比如 my-first-app
  • 是否加 TypeScript?:新手可以先选 No,先学 Vue 本身
  • 是否加 Router?:新手先选 No,后面再学
  • 其他选项全部回车跳过就好 💡 Tip:这些选项以后都能加,不用纠结。

第三步:安装依赖并启动

cd my-first-app      # 进入项目文件夹
npm install           # 下载项目需要的所有依赖(就这一次,以后不用)
npm run dev           # 启动开发服务器

看到类似 Local: http://localhost:5173/ 就说明成功了。用浏览器打开这个地址,你应该能看到一个 Vue 的欢迎页。 📝 Note:开发服务器是什么? npm run dev 会启动一个"开发服务器",它做的事情是:

  1. 把你写的代码实时编译成浏览器能看懂的样子
  2. 当你修改代码并保存时,浏览器自动刷新
  3. 这就是"热更新"——改代码 → 保存 → 立刻看到效果

项目文件夹里有什么?

刚创建的项目结构如下。你不需要一次性全搞懂,先认识最重要的:

my-first-app/
├── index.html          ← 唯一的 HTML 页面(SPA 的入口)
├── package.json         ← 项目配置 + 依赖列表(相当于购物清单)
├── vite.config.js       ← Vite 的配置文件(一般不用动)
├── src/                 ← 你写代码的地方,大部分时间都在这个文件夹里
│   ├── main.js          ← 项目的启动文件(告诉 Vue "从哪开始")
│   ├── App.vue          ← 根组件(整个页面的最外层)
│   └── components/      ← 放各种小组件的文件夹
└── public/              ← 放不经过处理的静态文件(如图片、字体)

📝 Note:.vue 文件是什么? .vue 文件就是 Vue 的"单文件组件"。一个 .vue 文件包含三部分:

  • <template> — HTML 模板(你看到的样子)
  • <script> — JavaScript 逻辑(控制行为)
  • <style> — CSS 样式(控制外观) 后面会逐一详细讲解。

VSCode 插件

如果你用 VSCode 写代码,搜 Vue - Official 安装,它会提供:

  • .vue 文件的语法高亮(代码有颜色)
  • 输入时的智能提示
  • 写错时的类型检查

快速检查

装完后,打开 src/App.vue,找到 <h1> 标签,把里面的文字改成 "Hello, Vue!",保存——浏览器应该马上更新。 如果能做到这一步,环境就搭好了,可以开始学 Vue 了! 下一步: 💡 Tip:Composition API 写法(Vue 3) 环境搭建本身不涉及代码风格差异,创建项目的命令两种 API 风格通用。 详见:

Vue 到底是干什么的?

简单说,Vue 帮你做一件事:把数据和页面自动关联起来。 举个例子:你有一个变量叫 name = "小明",你想在页面上显示"你好,小明"。用原生 JavaScript 你可能要写:

document.getElementById('greeting').textContent = '你好,' + name

但如果 name 变成了 "小红",你得再写一行代码更新页面。数据一变,你得手动更新。 Vue 的思路是:你只管改数据,页面自动更新。这就是"响应式"——数据的变化会自动"响应"到页面上。

项目的启动流程

每个 Vue 项目启动时,都从 main.js 开始执行。打开 src/main.js

import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

这三行代码的意思是: | 代码 | 做什么 | | import { createApp } from 'vue' | 从 Vue 库里借一个 createApp 函数 | | import App from './App.vue' | 把自己写的 App.vue 组件加载进来 | | createApp(App).mount('#app') | 创建应用 → 把 App 组件挂到 #app 这个位置 | 画成图就是这样:

index.html                  main.js                    App.vue
┌───────────┐    启动    ┌──────────────┐    挂载    ┌──────────────┐
│ <div      │ ◄──────── │ createApp(…  │ ◄──────── │ <template>   │
│  id="app">│           │  mount(#app) │           │   整个页面的   │
│ </div>    │           └──────────────┘           │   内容都在这里  │
└───────────┘                                     └──────────────┘

💡 Tip:mount 是什么? "挂载"就是把 Vue 组件"装"到 HTML 的某个位置上。mount('#app') 的意思就是:把 Vue 应用放到 id="app" 的那个 <div> 里。

根组件 App.vue 长什么样

打开 src/App.vue,你会看到这样的结构:

<template>
  <h1>{{ title }}</h1>
</template>
<script>
export default {
  data() {
    return {
      title: 'Hello Vue 3'
    }
  }
}
</script>

这是 Vue 最基础的写法(叫"选项式 API"),我们来拆解每一块:

  • <template> — 就是 HTML,但可以使用 Vue 的特殊语法
  • {{ title }} — 双大括号叫"插值",把 title 这个变量的值显示在这里
  • <script> — 写 JavaScript 的地方
  • data() — 一个函数,return 出去的就是这个组件的"数据"
  • export default — 把这整块代码"导出",让别的地方可以用它 📝 Note:为什么 data 是函数不是对象? 这是 Vue 的一个设计:data 必须写成一个函数(data() { ... }),不能直接写 data: { ... }。 原因很简单:每个组件需要有自己的数据副本,函数保证了每次用的都是"新的一份",不会互相干扰。你暂时记住这个规则就行。

SPA 是什么?

Vue 是 SPA(单页应用) 框架。SPA 的意思是: 整个网站只有一个 .html 文件,切换页面时浏览器不会真正跳转——只是 Vue 在同一个页面里"换掉"显示的内容。 对比一下传统网站和 SPA: | | 传统网页 | SPA(Vue 项目) | | HTML 文件 | 每个页面一个 .html | 整个网站只有一个 index.html | | 页面切换 | 浏览器白屏→重新加载 | 丝滑切换,无白屏 | | 数据请求 | 每次切换页面重新请求 | 按需请求,更快 | | 用户体验 | 有延迟感 | 像 App 一样流畅 | 这就是为什么 Vue 适合做"Web App"——体验接近手机 App。

接下来学什么?

了解了项目结构后,建议按这个顺序学习:

  1. — 了解 Vue 的两种写法
  2. — 怎么把数据显示到页面上
  3. — 动态改 HTML 属性
  4. — 控制元素的显示/隐藏
  5. — 遍历数据显示列表 💡 Tip:Composition API 写法(Vue 3) 上面的示例用了 Options API。用 Composition API 写同一个功能是这样的:
<script setup>
import { ref } from 'vue'
const title = ref('Hello Vue 3')
</script>

详见:

Vue 提供了两种写代码的方式,官方叫"API 风格"。它们功能完全一样,只是写法不同。

选项式 API(Options API)

这种风格的特点是:用固定的"选项"来组织代码。就像填表格一样,数据放 data,方法放 methods,计算结果放 computed

<template>
  <p>点击次数:{{ count }}</p>
  <button @click="addOne">点我 +1</button>
</template>
<script>
export default {
  data() {
    return {
      count: 0       // 数据写这里
    }
  },
  methods: {
    addOne() {
      this.count++   // 方法写这里,用 this. 访问数据
    }
  }
}
</script>

💡 Tip:为什么是 this.count 在选项式 API 中,this 指代"当前组件"。data 里定义的数据会自动挂到 this 上,所以用 this.count 就能读到。

优点

  • 结构固定,新手容易上手——所有组件长一个样
  • 文档好查,每种功能有对应的固定位置

缺点

  • 同一个功能的代码会散落在不同选项里(数据在 data,逻辑在 methods,计算在 computed),功能多了不好维护

组合式 API(Composition API)

这种风格的特点是:按功能组织代码,而不是按选项。用 Vue 提供的函数(refcomputed 等)来写逻辑。

<template>
  <p>点击次数:{{ count }}</p>
  <button @click="addOne">点我 +1</button>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)     // ref() 创建响应式数据
function addOne() {
  count.value++          // 注意!在 JS 里用 .value 访问
}
</script>

⚠️ Warning:两个关键区别

  1. <script setup> 代替了 export default
  2. 数据用 ref() 创建,在 JavaScript 里访问需要 .value(但模板里不用)

优点

  • 同一功能的代码写在一起,逻辑清晰
  • 可以把一段逻辑抽出来复用(这是组合式 API 最大的优势)
  • TypeScript 支持更好

缺点

  • 新手理解"响应式"要多花一点时间
  • 需要记住 ref 要在 JS 里加 .value(忘了会报错)

两种风格怎么选?

| 场景 | 推荐 | | 刚开始学 Vue | 选项式 API(结构清晰,照着抄就行) | | 小项目 / 简单页面 | 选项式 API(够用,不用上组合式) | | 大型项目 / 多人合作 | 组合式 API(好拆分、好复用) | | 需要复用逻辑 | 组合式 API(可以抽成函数到处用) | | 用 TypeScript | 组合式 API(类型推断更好) | 📝 Note:本笔记的教学安排 基础部分会用选项式 API 讲解(更容易理解 Vue 的核心概念),同时每篇末尾附上组合式 API 的对照写法。等你理解了基础概念后,再来看组合式 API 就会发现它只是换了一种组织方式。

一个直观的对比

同一个功能,两种写法放在一起看:

<!-- ========== 选项式 API ========== -->
<script>
export default {
  data() {
    return { name: '小明', age: 18 }
  },
  computed: {
    intro() {
      return `${this.name}今年${this.age}岁`
    }
  },
  methods: {
    growUp() {
      this.age++
    }
  }
}
</script>
<!-- ========== 组合式 API ========== -->
<script setup>
import { ref, computed } from 'vue'
const name = ref('小明')
const age = ref(18)
const intro = computed(() => `${name.value}今年${age.value}岁`)
function growUp() {
  age.value++
}
</script>

功能完全一样,只是"组织代码的方式"不同。

新手常见问题

Q:学了选项式还要学组合式吗? A:先精通一种。建议从选项式入门理解 Vue 核心概念,然后再学组合式。组合式只是写法不同,核心概念(响应式、组件、指令)是一样的。 Q:能不能混着用? A:同一个 .vue 文件里不要混用,但要学的知识点是通用的——条件渲染、列表渲染、事件处理这些在两个风格里用法完全一样。 下一步: 💡 Tip:Composition API 写法速查 详见: 和

Vue 的模板就是 HTML,但加了两种特殊语法:插值表达式指令

插值表达式 {{ }}

插值的意思是"在这里插入值"。Vue 用 双大括号 {{ }} 把数据嵌入到 HTML 中。

<template>
  <p>{{ message }}</p>
  <p>{{ number + 1 }}</p>
  <p>{{ flag ? '是' : '否' }}</p>
</template>
<script>
export default {
  data() {
    return {
      message: 'Hello',
      number: 10,
      flag: true
    }
  }
}
</script>

页面会显示:

Hello
11
是

💡 Tip:{{ }} 里面可以写什么? 可以写合法的 JavaScript 表达式(能算出结果的代码)。比如:

  • 变量:{{ name }}
  • 运算:{{ a + b }}
  • 三元:{{ ok ? '是' : '否' }}
  • 调用方法:{{ message.toUpperCase() }} 但不能写语句!不能写 {{ if (true) { ... } }}{{ const x = 1 }}

原始 HTML — v-html

默认情况下,{{ }} 会把内容当作纯文本处理,HTML 标签会被转义——也就是说,<b> 不会让文字加粗。 如果你真的需要渲染 HTML 字符串,用 v-html

<template>
  <p>{{ rawHtml }}</p>              <!-- 显示: <b>粗体</b> -->
  <p v-html="rawHtml"></p>         <!-- 显示: 粗体 -->
</template>
<script>
export default {
  data() {
    return {
      rawHtml: '<b>粗体</b>'
    }
  }
}
</script>

🚫 Danger:v-html 有安全风险 v-html 要慎用!如果你把用户输入的内容直接用 v-html 渲染,恶意用户可以在里面插入 <script> 标签执行攻击代码(XSS 攻击)。 规则:永远不要对用户输入的内容使用 v-html

什么是"指令"?

指令就是 Vue 提供的特殊属性,以 v- 开头。它们告诉 Vue "这个元素要做什么特殊处理"。 | 指令 | 作用 | 类比 | | v-html | 渲染 HTML | 用 HTML 内容替换元素 | | v-bind | 绑定属性 | 下篇讲 | | v-if | 条件显示 | 后面会讲 | | v-for | 循环渲染 | 后面会讲 | | v-on | 绑定事件 | 后面会讲 | | v-model | 双向绑定 | 后面会讲 | 这六个指令是 Vue 最核心的东西,后面会逐一详细讲解。

模板里只有文本?不,还有指令和表达式

总结一下,Vue 模板里你能用的东西:

<template>
  <!-- 1. 普通 HTML(照搬) -->
  <h1>标题</h1>
  <!-- 2. 插值表达式(动态内容) -->
  <p>{{ message }}</p>
  <!-- 3. 指令(v- 开头,做特殊操作) -->
  <a v-bind:href="url">链接</a>
</template>

这三种东西可以组合使用,构成 Vue 模板的全部能力。

新手常见问题

Q:为什么我的 {{ }} 一闪而过变成了真实数据? A:那是 Vue 还没加载完时,模板原样显示了。解决办法:在 index.html#app 上加 v-cloak(进阶内容,不急)。 Q:{{ }}v-text 有什么区别? A:效果一样,<span v-text="msg"></span> 等于 <span>{{ msg }}</span>。一般直接用插值更直观。 下一步: 💡 Tip:Composition API 写法(Vue 3)

<script setup>
import { ref } from 'vue'
const message = ref('Hello')
const rawHtml = ref('<b>粗体</b>')
</script>

模板部分完全一样,不变。 详见:

为什么需要属性绑定?

HTML 属性(如 hrefsrcclassdisabled)在普通 HTML 里是写死的:

<a href="https://vuejs.org">官网</a>    <!-- href 永远是 vuejs.org -->

但你经常需要动态改变属性值,比如:

  • 切换不同图片:src 要根据数据变化
  • 禁用/启用按钮:disabled 要根据状态变化
  • 跳转不同链接:href 要根据用户操作变化 这时就要用 v-bind

基本用法

v-bind 让 HTML 属性的值可以跟 Vue 的数据绑定:

<template>
  <a v-bind:href="url">前往官网</a>
  <img v-bind:src="imageSrc" alt="logo">
</template>
<script>
export default {
  data() {
    return {
      url: 'https://vuejs.org',
      imageSrc: 'logo.png'
    }
  }
}
</script>

渲染出来的 HTML 就是:

<a href="https://vuejs.org">前往官网</a>
<img src="logo.png" alt="logo">

v-bind 后面跟的属性名 = HTML 属性名,等号后面是数据变量。

简写

v-bind 太长了,Vue 提供了简写——直接用冒号 :

<a :href="url">链接</a>           <!-- 等于 v-bind:href="url" -->
<img :src="imageSrc">             <!-- 等于 v-bind:src="imageSrc" -->

几乎所有 Vue 开发者都用简写,看到 : 就理解为"这是一个动态属性"。

布尔属性

有些 HTML 属性不需要值,存在就生效,比如 disabledcheckedreadonly

<template>
  <button :disabled="isDisabled">提交</button>
</template>
<script>
export default {
  data() {
    return {
      isDisabled: true     // true → 按钮禁用
    }
  }
}
</script>

Vue 特殊处理了布尔属性:

  • true → 属性存在 → 按钮禁用
  • false → 属性移除 → 按钮可用
  • nullundefined → 属性也不渲染

绑定多个属性

如果你有一个对象,想把它的所有属性都绑到元素上:

<template>
  <div v-bind="attrs">内容</div>
</template>
<script>
export default {
  data() {
    return {
      attrs: {
        id: 'myDiv',
        class: 'container',
        title: '提示信息'
      }
    }
  }
}
</script>

渲染结果等于:

<div id="myDiv" class="container" title="提示信息">内容</div>

总结

| 写法 | 含义 | | v-bind:href="url" | 完整写法 | | :href="url" | 简写(推荐) | | :disabled="true" | 布尔属性 | | v-bind="obj" | 批量绑定 | 记住:HTML 属性要动态改变 → 前面加冒号 :。 下一步: 💡 Tip:Composition API 写法(Vue 3)

<script setup>
import { ref } from 'vue'
const url = ref('https://vuejs.org')
const isDisabled = ref(true)
</script>

模板中的 :href:disabled 等写法完全不变。 详见:

v-for 指令用来"循环生成多个相同的元素",就像 JavaScript 的 for...of 循环。

遍历数组

最常用的场景是把数组里的每一项渲染出来:

<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.name }}
    </li>
  </ul>
</template>
<script>
export default {
  data() {
    return {
      items: [
        { id: 1, name: 'Vue' },
        { id: 2, name: 'React' },
        { id: 3, name: 'Angular' }
      ]
    }
  }
}
</script>

页面输出:

• Vue
• React
• Angular

📝 Note:v-for 的语法 v-for="item in items" 读作:"从 items 数组里逐个取出元素,存到变量 item 里,然后用 item 渲染这一行。"

获取索引

如果你还需要知道"当前是第几个",加一个括号:

<li v-for="(item, index) in items" :key="item.id">
  {{ index + 1 }}. {{ item.name }}
</li>

输出:

1. Vue
2. React
3. Angular

💡 Tip:注意 index 从 0 开始,显示的时候一般需要 index + 1

遍历对象

v-for 也能遍历对象的属性:

<template>
  <p v-for="(value, key, index) in user" :key="key">
    {{ index }}. {{ key }}: {{ value }}
  </p>
</template>
<script>
export default {
  data() {
    return {
      user: { name: '张三', age: 18, city: '北京' }
    }
  }
}
</script>

输出:

0. name: 张三
1. age: 18
2. city: 北京

三个参数的顺序是:值 → 键 → 索引

遍历数字范围

<span v-for="n in 5" :key="n">{{ n }} </span>

输出:1 2 3 4 5(从 1 开始,到 5)

key 属性 — 很重要!

每个 v-for 的元素都应该加上 :key,这是 Vue 识别每个元素的身份标识

为什么需要 key?

Vue 更新列表时,需要知道"哪一行是新加的、哪一行是删掉的、哪一行改了"。没有 key,Vue 就只能"按位置对号入座",容易出错。 举个例子:你有一个列表 [A, B, C],你想在最前面插入 D 变成 [D, A, B, C]

  • 没有 key:Vue 把第一行从 A 改成 D,第二行 B 改成 A...四行都要更新
  • 有 key:Vue 一看,D 是新的,放最前面;A、B、C 都在,不用动

用什么做 key?

<!-- ✅ 用唯一 ID -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
<!-- ❌ 不要用索引(index) -->
<li v-for="(item, index) in items" :key="index">{{ item.name }}</li>

index 做 key 的坏处:当你对数组做插入/删除时,元素的 index 会变,Vue 就分不清谁是谁了。 💡 Tip:记住 只要有 v-for,就一定要加 :key,且 key 要用唯一的、不变的值。

<template> 上使用 v-for

v-if 一样,v-for 也可以加在 <template> 上同时渲染多个元素:

<template v-for="item in items" :key="item.id">
  <h3>{{ item.name }}</h3>
  <p>{{ item.desc }}</p>
</template>

实际例子:待办事项列表

<template>
  <div>
    <input v-model="newTodo" @keyup.enter="addTodo" placeholder="输入待办事项">
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        {{ todo.text }}
        <button @click="removeTodo(todo.id)">删除</button>
      </li>
    </ul>
  </div>
</template>
<script>
export default {
  data() {
    return {
      newTodo: '',
      todos: [
        { id: 1, text: '学 Vue' },
        { id: 2, text: '写代码' }
      ],
      nextId: 3
    }
  },
  methods: {
    addTodo() {
      if (this.newTodo.trim()) {
        this.todos.push({ id: this.nextId++, text: this.newTodo })
        this.newTodo = ''     // 清空输入框
      }
    },
    removeTodo(id) {
      this.todos = this.todos.filter(t => t.id !== id)
    }
  }
}
</script>

新手常见问题

Q:不写 :key 会怎样? A:能运行,但 Vue 会在控制台给你一个警告,而且列表更新时可能出 bug。养成习惯,有 v-for 必有 :keyQ:为什么我的列表没更新? A:检查你是不是用 this.items[0] = xxx 这样直接赋值。Vue 2 不会检测这种改动,要用 splice 或整体替换数组。Vue 3 没这个问题。 下一步: 💡 Tip:Composition API 写法(Vue 3)

<script setup>
import { ref } from 'vue'
const items = ref([{ id: 1, name: 'Vue' }, { id: 2, name: 'React' }])
</script>

模板部分 (v-for:key) 完全不变。 详见:

条件渲染就是"根据条件决定显示还是隐藏某个元素"。Vue 提供了两个指令:v-ifv-show

v-if / v-else-if / v-else

和 JavaScript 的 if...else 逻辑一模一样,控制元素是否在 DOM 中存在。

<template>
  <p v-if="score >= 90">优秀</p>
  <p v-else-if="score >= 60">及格</p>
  <p v-else>不及格</p>
</template>
<script>
export default {
  data() {
    return { score: 75 }
  }
}
</script>

结果:页面只显示"及格"那一行。另外两行根本不在 HTML 里(你可以打开浏览器开发者工具验证)。 📝 Note:规则

  • v-if / v-else-if / v-else 必须紧挨着,中间不能插其他元素
  • 只有第一个带条件(v-if),后面的 v-else-ifv-else 跟着就行

v-show

v-show 也能控制显示隐藏,但原理不同——元素始终在 DOM 中,只是通过 CSS display: none 隐藏了

<template>
  <p v-show="visible">我一直都在 DOM 里</p>
</template>
<script>
export default {
  data() {
    return { visible: true }
  }
}
</script>

visiblefalse 时:

<p style="display: none;">我一直都在 DOM 里</p>

v-if vs v-show,怎么选?

| | v-if | v-show | | 原理 | 条件为假时,元素从 DOM 中完全移除 | 条件为假时,元素还在,只是隐藏display: none) | | 初始渲染 | 条件为假时不渲染,更快(省资源) | 始终渲染,初始时慢一点点 | | 切换开销 | 每次切换要销毁/重建,开销大 | 只改 CSS,开销极小 | | 适用场景 | 条件很少改变的情况(如用户是否登录) | 条件频繁切换的情况(如选项卡切换) | 筛选分类时就可以用show减少css开销 💡 Tip:记住一个口诀 少切换用 v-if,多切换用 v-show。

<template> 上使用 v-if

如果你想用 v-if 控制多个元素,但不想多包一层 <div>,可以用 <template>

<template v-if="loggedIn">
  <h2>欢迎回来</h2>
  <p>今天天气不错</p>
  <button>开始使用</button>
</template>

<template> 是一个"隐形容器"——它本身不会渲染成任何 HTML,只是用来组织逻辑。 📝 Note:注意 v-show 不能用在 <template> 上,因为 <template> 没有实际 DOM 元素。

实际例子:登录状态切换

<template>
  <div>
    <p v-if="user">欢迎,{{ user }}</p>
    <button v-else @click="login">请登录</button>
  </div>
</template>
<script>
export default {
  data() {
    return { user: null }
  },
  methods: {
    login() {
      this.user = '小明'    // 数据一变,v-if 自动重新判断
    }
  }
}
</script>

点击按钮 → usernull 变成 "小明" → Vue 自动把"请登录"替换成"欢迎,小明"。

新手常见问题

Q:v-if 和 v-show 可以同时用吗? A:分开用就行,别同时加一个元素上。先判断用哪个:切换频繁用 v-show,否则用 v-ifQ:为什么 v-if 里用 v-for 会告警? A:Vue 规定 v-ifv-for 不能放在同一个元素上(执行优先级会出问题)。应该先用 v-for 循环,再在里面用 v-if。 下一步: 💡 Tip:Composition API 写法(Vue 3)

<script setup>
import { ref } from 'vue'
const score = ref(75)
const visible = ref(true)
const user = ref(null)
</script>

模板部分完全不变。 详见:

用户点击按钮、输入文字、移动鼠标……这些都是"事件"。Vue 用 v-on 指令来监听这些事件。

基本用法

v-on 用于绑定事件处理函数:

<template>
  <button v-on:click="handleClick">点我</button>
</template>
<script>
export default {
  methods: {
    handleClick() {
      alert('按钮被点了!')
    }
  }
}
</script>

v-on:click="handleClick" 的意思是:当这个按钮被点击时,调用 handleClick 方法

简写

v-on: 太长,日常用 @ 简写:

<button @click="handleClick">点我</button>     <!-- 等于 v-on:click -->
<input @input="handleInput">                   <!-- 等于 v-on:input -->
<form @submit="handleSubmit">                  <!-- 等于 v-on:submit -->

看到 @ 就理解为"当...事件发生时"。

给方法传参

<template>
  <button @click="say('Hello')">打招呼</button>
  <button @click="say('再见')">说再见</button>
</template>
<script>
export default {
  methods: {
    say(msg) {
      alert(msg)
    }
  }
}
</script>

💡 Tip:需要事件对象怎么办? 如果你既要传参,又要拿到事件对象,用 $event

<button @click="handle('参数', $event)">点我</button>

事件修饰符

很多事件需要加上"额外操作",比如阻止表单自动提交、防止事件冒泡。Vue 提供了修饰符,在事件后面加 .xxx 就行。 | 修饰符 | 作用 | 不用 Vue 你要写 | | .prevent | 阻止默认行为(如表单提交、a 标签跳转) | e.preventDefault() | | .stop | 阻止事件冒泡(点击子元素不会触发父元素) | e.stopPropagation() | | .once | 事件只触发一次 | 手动加标志位 | | .self | 只在事件源是自身时触发 | if (e.target === e.currentTarget) | | .capture | 在捕获阶段触发 | 第三个参数 true |

<template>
  <!-- 表单提交时不会刷新页面 -->
  <form @submit.prevent="onSubmit">
    <input type="text">
    <button type="submit">提交</button>
  </form>
  <!-- 点击"点我"不会触发灰色区域的点击事件 -->
  <div @click="onOuterClick" style="background: gray; padding: 20px">
    <button @click.stop="onInnerClick">点我</button>
  </div>
</template>

按键修饰符

监听键盘事件时,可以指定按键:

<!-- 只有按回车时触发 -->
<input @keyup.enter="onEnter">
<!-- 只有按 Esc 时触发 -->
<input @keyup.escape="onCancel">

支持的按键别名:.enter.tab.delete.esc.space.up.down.left.right 也可以直接用键码:@keyup.13(13 = 回车键码,不过用别名更直观)

组合键

<!-- Ctrl + 回车 -->
<input @keyup.ctrl.enter="onCtrlEnter">
<!-- 精确匹配:只有 Ctrl 被按下(不能同时按其他键) -->
<button @click.ctrl.exact="onCtrlOnly">精确 Ctrl</button>

内联处理

简单逻辑可以直接写在模板里,不用定义方法:

<template>
  <button @click="count++">点了 {{ count }} 次</button>
</template>
<script>
export default {
  data() {
    return { count: 0 }
  }
}
</script>

适合只有一行逻辑的简单操作。复杂的逻辑还是应该写到 methods 里。

实际例子:计数器

<template>
  <div>
    <p>当前计数:{{ count }}</p>
    <button @click="count++">+1</button>
    <button @click="count--">-1</button>
    <button @click="count = 0">重置</button>
  </div>
</template>
<script>
export default {
  data() {
    return { count: 0 }
  }
}
</script>

新手常见问题

Q:@click="fn"@click="fn()" 有区别吗? A:有。@click="fn" 会把事件对象传给 fn;@click="fn()" 则什么都不传。如果不需要事件对象,两种写法效果一样。 Q:事件处理函数里怎么访问 data? A:用 this.xxx。Vue 自动把 data 里的数据挂到了 this 上。 下一步: 💡 Tip:Composition API 写法(Vue 3)

<script setup>
import { ref } from 'vue'
const count = ref(0)
function handleClick() { count.value++ }
</script>

模板中的 @click 等写法完全不变。 详见:

v-model 是 Vue 最常用的指令之一,实现表单控件和数据之间的双向绑定

什么是双向绑定?

普通的 :value 只能做到"数据 → 页面"。v-model 能做到:

数据 ↔ 页面:改数据,页面变;用户输入,数据也变。
<template>
  <input v-model="message">
  <p>你输入的是:{{ message }}</p>
</template>
<script>
export default {
  data() {
    return { message: '' }
  }
}
</script>

你在输入框里打字 → message 自动更新 → {{ message }} 自动刷新。两个方向都是自动的。 💡 Tip:v-model 的本质 v-model="message" 等价于同时做了两件事:

  1. :value="message" — 把数据绑到输入框
  2. @input="message = $event.target.value" — 监听输入并更新数据 所以它叫"双向绑定",一行顶两行。

不同表单控件的用法

文本框

<input v-model="text">

文本域

<textarea v-model="content"></textarea>

📝 Note:注意 <textarea> 不要写 {{ }} 插值,用 v-model

单选复选框

<input type="checkbox" v-model="checked">
<span>{{ checked ? '已选' : '未选' }}</span>

checked 的值是 truefalse

多选复选框(绑定到数组)

<template>
  <div>
    <input type="checkbox" v-model="hobbies" value="读书"> 读书
    <input type="checkbox" v-model="hobbies" value="运动"> 运动
    <input type="checkbox" v-model="hobbies" value="音乐"> 音乐
    <p>你选了:{{ hobbies }}</p>
  </div>
</template>
<script>
export default {
  data() {
    return {
      hobbies: []          // 必须是数组
    }
  }
}
</script>

单选框

<input type="radio" v-model="gender" value="男"> 男
<input type="radio" v-model="gender" value="女"> 女

下拉列表

<select v-model="city">
  <option disabled value="">请选择城市</option>
  <option value="beijing">北京</option>
  <option value="shanghai">上海</option>
</select>

修饰符

v-model 提供了三个方便的修饰符: | 修饰符 | 作用 | 什么时候用 | | .lazy | 失去焦点时才更新数据(而不是每次输入都更新) | 输入大段文字时减少计算 | | .number | 自动把输入转成数字 | 输入年龄、价格 | | .trim | 自动去掉首尾空格 | 输入用户名、邮箱 |

<input v-model.lazy="message">     <!-- 按回车或失焦才更新 -->
<input v-model.number="age">       <!-- 输入 "25" → age 是数字 25 -->
<input v-model.trim="username">    <!-- " 小明 " → "小明" -->

实际例子:登录表单

<template>
  <form @submit.prevent="handleLogin">
    <div>
      <label>用户名:</label>
      <input v-model.trim="username" placeholder="请输入用户名">
    </div>
    <div>
      <label>密码:</label>
      <input v-model="password" type="password" placeholder="请输入密码">
    </div>
    <div>
      <label>
        <input type="checkbox" v-model="remember"> 记住我
      </label>
    </div>
    <button type="submit">登录</button>
  </form>
</template>
<script>
export default {
  data() {
    return {
      username: '',
      password: '',
      remember: false
    }
  },
  methods: {
    handleLogin() {
      console.log(`登录:${this.username}, 记住:${this.remember}`)
      // 这里发登录请求
    }
  }
}
</script>

新手常见问题

Q:input 事件和 v-model 冲突吗? A:不会。你仍然可以单独用 @input 做额外的处理,v-model 会继续正常运行。 Q:为什么我的 checkbox 绑定到数组,数据没更新? A:检查你的数组是用 ref 还是 reactive。组合式 API 中如果用 ref([]),用的时候要 .valueQ:v-model 能绑定 computed 吗? A:能,但需要给 computed 提供 setter。如果只是只读 computed,v-model 会报错。 下一步: 💡 Tip:Composition API 写法(Vue 3)

<script setup>
import { ref } from 'vue'
const message = ref('')
const checked = ref(false)
</script>

模板中 v-model 写法完全不变。 详见:

什么是计算属性?

计算属性(computed)是一类特殊的属性,它的值由其他数据计算得出,而且 Vue 会缓存结果。 简单说:有依赖别人才能算出来的值,就放 computed 里

一个例子看懂

<template>
  <p>原始数组:{{ numbers }}</p>
  <p>偶数数组:{{ evenNumbers }}</p>
</template>
<script>
export default {
  data() {
    return {
      numbers: [1, 2, 3, 4, 5, 6]
    }
  },
  computed: {
    evenNumbers() {
      return this.numbers.filter(n => n % 2 === 0)
    }
  }
}
</script>

输出:

原始数组:[1, 2, 3, 4, 5, 6]
偶数数组:[2, 4, 6]

evenNumbers 不是存死的数据,而是每次根据 numbers 自动算出来的。只要 numbers 不变,evenNumbers 就用缓存的结果,不会重新算。

计算属性 vs 方法 — 关键区别

两种写法都能实现同样的功能,但行为完全不同:

<template>
  <!-- 方法:每次访问都会重新执行 -->
  <p>{{ getEvenNumbers() }}</p>
  <p>{{ getEvenNumbers() }}</p>
  <p>{{ getEvenNumbers() }}</p>
  <!-- 计算属性:第一次算出后缓存,后面直接用 -->
  <p>{{ evenNumbers }}</p>
  <p>{{ evenNumbers }}</p>
  <p>{{ evenNumbers }}</p>
</template>
<script>
export default {
  data() { return { numbers: [1, 2, 3, 4, 5, 6] } },
  computed: {
    evenNumbers() {
      console.log('computed 被调用了')     // 只打印 1 次
      return this.numbers.filter(n => n % 2 === 0)
    }
  },
  methods: {
    getEvenNumbers() {
      console.log('method 被调用了')       // 打印 3 次
      return this.numbers.filter(n => n % 2 === 0)
    }
  }
}
</script>

| | computed | methods | | 缓存 | 有(依赖不变就复用上次结果) | 无(每次重新计算) | | 调用方式 | 像属性一样,不带括号:{{ evenNumbers }} | 像函数一样,带括号:{{ getEven() }} | | 适用场景 | 需要根据数据计算的展示值 | 需要执行的动作(如事件处理) | | 何时用 | 读数据时 | 做操作时 | 💡 Tip:记住 computed 就像 Excel 里的公式:=SUM(A1:A10)。只要 A1:A10 的值没变,结果就不会重新算。

计算属性的 getter 和 setter

计算属性默认是"只读"的(getter)。但你也可以让它可写(setter):

<template>
  <p>{{ fullName }}</p>
  <button @click="changeFullName">改名字</button>
</template>
<script>
export default {
  data() {
    return {
      firstName: '张',
      lastName: '三'
    }
  },
  computed: {
    fullName: {
      get() {                               // 读取时调用
        return this.firstName + this.lastName
      },
      set(newValue) {                       // 设置时调用
        this.firstName = newValue[0]
        this.lastName = newValue.slice(1)
      }
    }
  },
  methods: {
    changeFullName() {
      this.fullName = '李四'     // 触发 setter
    }
  }
}
</script>

不过 getter+setter 的计算属性用得比较少,大部分场景下只读就够了。

什么时候用 computed?

凡是符合"这个值由其他值决定"的逻辑,都应该用 computed:

  • 过滤列表(如搜索功能)
  • 格式化显示(如价格显示为 ¥ 符号)
  • 判断状态(如购物车是否为空)
  • 拼接字符串(如全名 = 姓 + 名)

新手常见问题

Q:computed 里能写异步代码吗? A:不行。computed 是同步的,必须立即返回值。如果需要异步操作,用 watchmethodsQ:computed 里能修改 data 吗? A:不能!computed 是"只读"的,在里面修改 data 会造成无限循环。改数据去 methods。 下一步: 💡 Tip:Composition API 写法(Vue 3)

<script setup>
import { ref, computed } from 'vue'
const numbers = ref([1, 2, 3, 4, 5, 6])
const evenNumbers = computed(() => numbers.value.filter(n => n % 2 === 0))
</script>

详见:

watch(侦听器)用来监听某个数据的变化,然后执行一些"副作用"

和 computed 有什么区别?

| | computed | watch | | 做什么 | 根据数据算一个新值出来 | 数据变了,干一件事 | | 返回 | 必须返回一个值 | 不返回值,执行操作 | | 用途 | 格式化、过滤、拼接 | 发请求、存本地、调第三方库 | | 本质 | 派生数据 | 副作用处理 | 💡 Tip:一句话区别 computed:数据变了,算出新结果。 watch:数据变了,去做某件事。

基本用法

<template>
  <input v-model="keyword" placeholder="搜索">
  <p>结果:{{ result }}</p>
</template>
<script>
export default {
  data() {
    return {
      keyword: '',
      result: ''
    }
  },
  watch: {
    keyword(newVal, oldVal) {               // 新值在前,旧值在后
      console.log(`从 "${oldVal}" 变为 "${newVal}"`)
      this.result = `搜索:${newVal}`
    }
  }
}
</script>

每次你输入文字 → keyword 变化 → watch 里的函数自动执行 → result 更新 → 页面刷新。 📝 Note:参数顺序 keyword(newVal, oldVal)新值在前,旧值在后。别写反了。

deep — 深度监听

默认情况下,watch 只监听对象引用的变化(地址变了没有),不监听对象内部属性的变化。

<script>
export default {
  data() {
    return {
      user: { name: '张三', age: 18 }
    }
  },
  watch: {
    user: {
      handler(newVal) {
        console.log('user 变了', newVal)
      },
      deep: true          // 开启深度监听
    }
  }
}
</script>

开启 deep: true 后,修改 user.nameuser.age 也会触发 watch。 ⚠️ Warning:deep 有性能代价 deep: true 会递归遍历对象所有属性,对象很大会影响性能。只在确实需要监听嵌套属性时才开。

immediate — 立即执行

默认情况下,watch 只在数据变化时才触发。如果你需要在页面加载时就执行一次,加 immediate: true

<script>
export default {
  watch: {
    keyword: {
      handler(newVal) {
        console.log('当前值:', newVal)
      },
      immediate: true     // 组件创建时立即执行一次
    }
  }
}
</script>

监听对象中的某个属性

如果只想监听对象的某个属性,而不是整个对象:

watch: {
  'user.name'(newName) {           // 用引号包起来,加点号
    console.log('名字变了:', newName)
  }
}

实际例子:防抖搜索

<template>
  <div>
    <input v-model="keyword" placeholder="搜索用户">
    <ul>
      <li v-for="user in filteredUsers" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>
<script>
export default {
  data() {
    return {
      keyword: '',
      users: [
        { id: 1, name: '张三' },
        { id: 2, name: '李四' },
        { id: 3, name: '王五' }
      ],
      filteredUsers: [],
      timer: null
    }
  },
  watch: {
    keyword() {
      clearTimeout(this.timer)                // 清除上一次定时器
      this.timer = setTimeout(() => {
        this.filteredUsers = this.users.filter(
          u => u.name.includes(this.keyword)
        )
      }, 300)                                  // 300ms 后才真正搜索
    }
  },
  created() {
    this.filteredUsers = this.users            // 初始显示全部
  }
}
</script>

新手常见问题

Q:computed 和 watch,我该用哪个? A:先想"我需要的是一个值,还是一个动作"——如果是值用 computed,是动作用 watch。 Q:watch 里能监听 computed 吗? A:可以。computed 的值变了也能被 watch 监听到。 Q:能不能同时监听多个数据? A:在选项式 API 中,每个数据单独写一个 watch。在组合式 API 中可以用数组:

watch([keyword, category], ([newKw, newCat]) => { ... })

下一步: 💡 Tip:Composition API 写法(Vue 3)

<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
watch(keyword, (newVal, oldVal) => {
  console.log(`从 ${oldVal} 变为 ${newVal}`)
})
</script>

watch 作为函数调用而非配置对象。另有 watchEffect(自动追踪依赖,立即执行)。 详见:

大多数时候你用 Vue 的数据绑定就够了。但偶尔你需要直接操作 DOM 元素——比如让输入框自动聚焦、获取滚动位置、操作 canvas 画布。 这时就用 ref 属性配合 $refs

获取 DOM 元素

<template>
  <input ref="inputRef" type="text" placeholder="我会自动获得焦点">
  <button @click="focusInput">手动聚焦</button>
</template>
<script>
export default {
  methods: {
    focusInput() {
      this.$refs.inputRef.focus()     // 直接操作 DOM
    }
  }
}
</script>

步骤:

  1. 在元素上加 ref="名字"
  2. 在 JavaScript 中用 this.$refs.名字 获取这个元素
  3. 然后就能调用原生 DOM 方法了(focus()scrollIntoView() 等)

访问时机 — mounted

$refs 在组件挂载完成后才可用。如果在 datacreated 里访问,拿到的是 undefined

<script>
export default {
  mounted() {
    this.$refs.inputRef.focus()     // ✅ 这里可以
  },
  created() {
    // this.$refs.inputRef  → undefined  ❌ 太早了
  }
}
</script>

📝 Note:组件的生命周期顺序 data 初始化 → created(数据有了,DOM 还没)→ mounted(DOM 渲染完了) $refs 要到 mounted 之后才能用。

在 v-for 中获取多个元素

v-for 的元素上使用 ref$refs 会返回一个数组

<template>
  <ul>
    <li v-for="item in list" :key="item" ref="items">{{ item }}</li>
  </ul>
</template>
<script>
export default {
  data() {
    return { list: ['A', 'B', 'C'] }
  },
  mounted() {
    console.log(this.$refs.items)   // [li, li, li]
  }
}
</script>

⚠️ Warning:注意顺序 v-for 中 ref 数组的顺序不一定和列表顺序一致,取决于你的 :key 和数据变化。不要依赖它们的顺序来做业务逻辑。

获取组件实例

ref 不仅能获取 DOM 元素,还能获取子组件实例

<template>
  <ChildComponent ref="childRef" />
  <button @click="callChildMethod">调用子组件方法</button>
</template>
<script>
export default {
  methods: {
    callChildMethod() {
      this.$refs.childRef.doSomething()    // 调用子组件的方法
    }
  }
}
</script>

这样就能"父组件主动调用子组件的方法"。不过这种方式不建议频繁使用,尽量通过 props 和 events 来通信。

什么时候需要直接操作 DOM?

Vue 的"响应式"已经覆盖了 95% 的情况。以下场景才需要 $refs: | 场景 | 示例 | | 聚焦输入框 | refs.xxx.focus() | | 获取元素尺寸/位置 | refs.xxx.getBoundingClientRect() | | Canvas 操作 | refs.canvas.getContext('2d') | | 集成第三方 DOM 库 | 如 Chart.js、Swiper | | 播放控制(视频/音频) | refs.video.play() |

新手常见问题

Q:为什么我的 this.$refs.xxx 是 undefined? A:最常见的原因是在 mounted 之前访问了。确保你的代码在 mounted 或之后(如按钮点击时)调用。 Q:ref 能在组件内部用吗? A:能。组件内任何元素都能用 ref 标记,只要是这个组件渲染的就能通过 this.$refs 访问。 Q:refv-bind:value)有什么区别? | | v-bind(数据绑定) | ref(模板引用) | | 做什么 | 把数据绑定到属性或 prop | 获取 DOM 元素或组件实例 | | 数据流 | JS → 模板(声明式) | 模板 → JS(命令式) | | 典型场景 | :value="msg" 显示数据 | ref="input" 然后 focus() | | 何时可用 | 任何时刻 | 只能在 mounted 之后 |

  • v-bind声明式的:你说"数据是什么,模板就渲染什么",Vue 帮你搞定
  • ref命令式的:你拿到元素自己操作,就像用 document.querySelector

能用 v-bind 解决的,优先用 v-bindref 只在你需要突破数据绑定限制(比如调用 DOM 方法)时才用。 下一步: 💡 Tip:Composition API 写法(Vue 3)

<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => { inputRef.value?.focus() })
</script>

组合式 API 中 ref 的变量名要和模板中 ref="inputRef" 的名字一致。 详见:

Vue 能自动检测数据变化并更新页面——但有些操作它能侦测到,有些不行。这一篇讲清楚规则。

数组的"变更方法"

Vue 特别对待了 JavaScript 中 7 个会修改原数组的方法。只要用它们操作数组,Vue 就能自动更新视图: | 方法 | 做什么 | 示例 | | push() | 末尾添加 | list.push('D') | | pop() | 末尾删除 | list.pop() | | shift() | 开头删除 | list.shift() | | unshift() | 开头添加 | list.unshift('A') | | splice() | 删除/插入/替换 | list.splice(1, 1, 'X') | | sort() | 排序 | list.sort() | | reverse() | 反转 | list.reverse() |

<template>
  <ul>
    <li v-for="item in list" :key="item">{{ item }}</li>
  </ul>
  <button @click="addItem">添加 D</button>
</template>
<script>
export default {
  data() {
    return { list: ['A', 'B', 'C'] }
  },
  methods: {
    addItem() {
      this.list.push('D')      // ✅ Vue 能侦测到,页面自动更新
    }
  }
}
</script>

数组的"非变更方法"

有些方法不会修改原数组,而是返回一个新数组: | 方法 | 说明 | | filter() | 过滤 | | concat() | 拼接 | | slice() | 截取 | | map() | 映射 | 这些方法不会触发视图更新,因为原数组没变。解决方法是用新数组替换旧数组

// ❌ 这样不会更新视图
this.numbers.filter(n => n > 0)
// ✅ 要整体替换
this.numbers = this.numbers.filter(n => n > 0)
this.numbers = this.numbers.concat([4, 5, 6])

📝 Note:为什么整体替换可以? Vue 的内部机制会优化这个操作——它不会真的销毁重建整个列表,而是尽可能复用已有的 DOM 元素。所以用 filter 整体替换和用 splice 删除,性能差不多。

Vue 2 的限制(已废弃的 Vue.set)

⚠️ Warning:Vue 2 中已废弃,Vue 3 已修复 以下问题只存在于 Vue 2。在 Vue 3 中,这些问题已经被 Proxy 机制解决了,你不需要担心这些限制。 Vue 2 无法侦测到以下两种操作:

// ❌ Vue 2 无法侦测:直接通过索引设置数组元素
this.items[0] = '新值'
// ❌ Vue 2 无法侦测:直接给对象添加新属性
this.user.age = 18    // 如果 age 不在初始 data 中

Vue 2 需要用 Vue.set()this.$set() 来处理:

// Vue 2 的解决方案
this.$set(this.items, 0, '新值')
this.$set(this.user, 'age', 18)

但在 Vue 3 中直接用 this.items[0] = '新值' 就行,不需要 $set

响应式原理(通俗版)

你不需要理解底层实现,但有个概念对以后有帮助:

  1. Vue 在创建实例时,把你 data 里的数据都"劫持"了
  2. 每次你读写这些数据,Vue 都能感知到
  3. 数据一变,Vue 就知道"哪些页面元素依赖这个数据",然后自动更新它们 就像一个管家,你只管说"茶杯放桌子上",管家会自动把旧杯子拿走、新杯子放上。你不用管"怎么更新页面",Vue 包了。

新手常见问题

Q:为什么我的列表改动后页面没反应? A:检查你是否做了以下操作:

  1. 直接改数组索引:this.list[0] = 'X'(Vue 2 不行,Vue 3 可以)
  2. 用了 filter 但没赋值:this.list.filter(...) → 应该是 this.list = this.list.filter(...)
  3. 也别忘了在 JavaScript 里组合式 API 用 ref 时需要用 .value Q:对象属性变化能侦测到吗? A:Vue 3 可以,Vue 2 只能在初始 data 中存在的属性才能被侦测。新增属性在 Vue 2 中需要用 $set。 下一步: 💡 Tip:Composition API 写法(Vue 3)
<script setup>
import { ref } from 'vue'
const list = ref(['A', 'B', 'C'])
function addItem() { list.value.push('D') }
</script>

在 Vue 3 的组合式 API 中,数组操作更直观,没有 Vue 2 的那些限制。 详见:

在 Vue 中通过数据驱动 CSS 样式,有两种方式:Class 绑定Style 绑定


Class 绑定

对象语法 — 按条件切换

最常用的方式:class 名写在左边,条件写在右边。

<template>
  <p :class="{ active: isActive, error: hasError }">状态文本</p>
  <button @click="isActive = !isActive">切换 active</button>
</template>
<script>
export default {
  data() {
    return {
      isActive: false,
      hasError: true
    }
  }
}
</script>

isActivefalsehasErrortrue 时,渲染结果:

<p class="error">状态文本</p>

规则:{ class名: 条件 } — 条件为真就加这个 class,为假就不加。

把对象放 data 里

当 class 很多时,把对象放 data 里,模板更干净:

<template>
  <p :class="classObject">文本</p>
</template>
<script>
export default {
  data() {
    return {
      classObject: {
        active: false,
        error: true,
        'text-large': true     // 带横杠的 class 名要加引号
      }
    }
  }
}
</script>

数组语法 — 直接拼 class

<template>
  <p :class="[baseClass, statusClass]">文本</p>
</template>
<script>
export default {
  data() {
    return {
      baseClass: 'text-lg',
      statusClass: 'color-red'
    }
  }
}
</script>

渲染结果:<p class="text-lg color-red">文本</p>

混合使用

数组和对象可以组合:

<p :class="['base', { active: isActive }]">文本</p>

'base' 始终存在,active 根据 isActive 决定。

:class 和普通 class 共存

:class 不会覆盖普通 class,它们会合并:

<button class="btn" :class="{ disabled: isDisabled }">提交</button>
<!-- 渲染: <button class="btn disabled">提交</button> -->

Style 绑定

对象语法

<template>
  <p :style="{ color: textColor, fontSize: size + 'px' }">彩色文字</p>
</template>
<script>
export default {
  data() {
    return {
      textColor: 'red',
      size: 18
    }
  }
}
</script>

CSS 属性名有两种写法:

  • 驼峰式(推荐):fontSize
  • 短横线式(加引号):'font-size'

完整对象放 data 里

<template>
  <div :style="styleObj">带样式的区域</div>
</template>
<script>
export default {
  data() {
    return {
      styleObj: {
        color: 'red',
        fontSize: '18px',
        fontWeight: 'bold'
      }
    }
  }
}
</script>

数组语法

<p :style="[baseStyle, highlightStyle]">文本</p>
<!-- 后面的样式会覆盖前面的同名属性 -->

Class vs Style,什么时候用哪个?

| 用 Class 绑定 | 用 Style 绑定 | | 样式是预定义好的(在 CSS 里写好) | 样式是动态算出来的(如用户滑条调颜色) | | 切换多个 class | 改个别属性 | | 更利于维护(样式和逻辑分离) | 需要精确控制像素值 |

💡 Tip:优先用 Class 绑定。 大部分场景用 Class 绑定就够了,Style 绑定只在需要动态计算数值时才用。

什么是组件?

组件是 Vue 的核心概念。把页面拆成一个个独立、可复用的小块,每个小块就是一个组件。

页面 = 拼积木
┌──────────────────────────────┐
│  Header(顶部导航组件)        │
├──────────────────────────────┤
│  Main(主要内容区组件)        │
├──────────────────────────────┤
│  Footer(底部组件)            │
└──────────────────────────────┘

每个组件就是一个 .vue 文件,封装了 HTML、JavaScript 和 CSS。

单文件组件(SFC)的结构

<template>
  <!-- 这个组件的 HTML -->
</template>
<script>
// 这个组件的 JavaScript 逻辑
</script>
<style scoped>
/* 这个组件的 CSS 样式 */
</style>

组件嵌套关系

Vue 应用本质上是一棵组件树——一个组件里放另一个组件,层层嵌套。

App.vue(根组件)
├── Header.vue(顶部导航)
├── Main.vue(主内容区)
│   ├── Article.vue(文章卡片)
│   └── Article.vue(文章卡片...复用多次)
└── Footer.vue(底部)

父组件写法

<template>
  <div class="layout">
    <Header />
    <Main />
    <Aside />
  </div>
</template>
<script>
import Header from './components/Header.vue'
import Main from './components/Main.vue'
import Aside from './components/Aside.vue'
export default {
  components: { Header, Main, Aside }
}
</script>

三个步骤import 导入 → components 注册 → <template> 中使用。

数据流向:从上到下

数据从父组件流向子组件,从根往叶子流。父组件通过 props 把数据传给子组件,子组件不能直接改父组件的数据——这是 Vue 的核心设计原则。


组件注册方式

局部注册(推荐)

只在当前组件内可用:

<template>
  <MyButton />
</template>
<script>
import MyButton from './MyButton.vue'
export default {
  components: { MyButton }
}
</script>

优点:依赖关系清晰,支持 tree-shaking,好维护。

全局注册

main.js 中注册,整个应用任何地方都能用:

import { createApp } from 'vue'
import App from './App.vue'
import MyButton from './components/MyButton.vue'

const app = createApp(App)
app.component('MyButton', MyButton)
app.mount('#app')

缺点main.js 会越来越臃肿,无法 tree-shaking。

场景 用哪种
通用组件(按钮、弹窗...到处用) 全局注册
特定页面的组件 局部注册
不确定是否会复用 先局部,需要时再提升

💡 Tip:先用局部注册,等这个组件确实在 3 个以上地方被引用了,再考虑全局注册。


<slot> — 预留的"内容洞"

组件里的 <slot> 就像是一个"占位符":

<!-- MyCard.vue -->
<template>
  <div class="card">
    <slot></slot>               <!-- 默认插槽 -->
    <slot name="footer"></slot> <!-- 具名插槽 -->
  </div>
</template>
<!-- 使用时 -->
<MyCard>
  <p>这是卡片的内容</p>
  <template v-slot:footer>
    <button>查看详情</button>
  </template>
</MyCard>

scoped — 组件样式隔离

<style scoped>
.title { color: red; }    /* 只影响本组件内的 .title */
</style>

不加 scoped = 全局样式 = 影响整个应用。开发时默认加 scoped 是个好习惯。

组件命名规范

  • 多个单词MyButton 而不是 Button(避免冲突)
  • PascalCase:MyButtonUserProfile
  • 模板中 <MyButton /><my-button></my-button> 都行

props — 父传子的"快递通道"

props 是父组件向子组件传递数据的机制,单向:父传子,子不能直接改。

父组件(有数据)
  │
  │  props(单向)
  ↓
子组件(接收数据,用在自己的模板里)

最简单的例子

父组件

<template>
  <Child title="来自父组件的数据" />
</template>
<script>
import Child from './Child.vue'
export default {
  components: { Child }
}
</script>

子组件(Child.vue)

<template>
  <p>父组件说的是:{{ title }}</p>
</template>
<script>
export default {
  props: ['title']
}
</script>

动态绑定 — 传变量而不是固定值

传变量时加冒号:

<Child :title="parentMsg" />
写法 传给子组件的
title="hello" 字符串 "hello"
:title="hello" 变量 hello 的值
:title="'hello'" 字符串 "hello"(表达式)

加了冒号,等号后面的是 JavaScript 表达式,不是字符串。

单向数据流

props 是单向的,子不能直接改父传过来的值:

// ❌ 错误
this.title = '新标题'      // 会导致 Vue 报错

子组件确实需要改时:

  1. 把 prop 赋给本地 data(子组件内部自己用)
  2. 通过事件通知父组件改

传递多种数据类型

规则:传非字符串类型必须加冒号 :

数字

<Child :age="25" />           <!-- ✅ 传数字 -->
<Child age="25" />            <!-- ❌ 传字符串 "25" -->

数组

<Child :names="['张三', '李四', '王五']" />

对象

<Child :user="userInfo" />

布尔值

<Child :is-admin="true" />

Props 校验

给 prop 加上类型、必填、默认值等规则,让组件更健壮。

从简单到完整

// 第 1 级:只声明名字
props: ['title']

// 第 2 级:指定类型
props: {
  title: String
}

// 第 3 级:加更多规则
props: {
  title: {
    type: String,
    required: true,
    default: '默认标题'
  }
}

完整示例

props: {
  title: {
    type: String,
    required: true
  },
  age: {
    type: Number,
    default: 18
  },
  tags: {
    type: Array,
    default() {              // ⚠️ 对象/数组必须用函数返回
      return []
    }
  },
  status: {
    type: String,
    validator(value) {
      return ['active', 'inactive'].includes(value)
    }
  }
}
选项 作用
type 期望的类型:String, Number, Boolean, Array, Object, Date, Function
required 是否必传
default 默认值(对象/数组必须用函数返回)
validator 自定义校验函数,返回 true 通过

支持多种类型

width: {
  type: [Number, String],
  default: 100
}

类型校验

当 prop 类型不对时,控制台会打印 warning(不是 error,页面仍能运行):

[Vue warn]: Invalid prop: type check failed for prop "age".
Expected Number, got String with value "25".

💡 Tip:type 校验只在开发环境生效,不影响生产性能。

新手常见问题

Q:我的 prop 传了但子组件拿不到?

  • 子组件有没有在 props 里声明
  • 父组件传的时候有没有写冒号
  • prop 名字拼写是否一致

Q:prop 名字在父模板里怎么写?

  • 父模板用小写横杠:my-title
  • 子组件 JS 用驼峰:myTitle

Q:为什么对象/数组的 default 必须是函数?

  • 确保每个组件实例拿到的是独立副本,而不是共享同一个引用

<script setup> 是 Vue 3 的推荐写法,它是组合式 API 的"语法糖"——把繁琐的样板代码省掉了,代码更简洁。

从"老写法"到"语法糖"

<!-- ❌ 老写法(组合式 API 但不用语法糖) -->
<script>
import { ref } from 'vue'
export default {
  setup() {
    const count = ref(0)
    function addOne() {
      count.value++
    }
    return { count, addOne }     // 必须手动 return 才能在模板中用
  }
}
</script>
<!-- ✅ 语法糖(<script setup>) -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
function addOne() {
  count.value++
}
// 不用 return!顶层的变量和函数自动暴露给模板
</script>

💡 Tip:"语法糖"是什么意思? 语法糖 = 写起来更甜的语法。功能没变,就是让你少写几行代码。<script setup> 做的事和老写法完全一样,只是帮你去掉了 export defaultsetup()return 这些样板。

核心规则

<script setup> 后,记住这几点:

  1. 导入即注册import 组件后直接能用,无需 components: {}
  2. 顶层变量自动暴露 — 模板中直接使用,不用 return
  3. 顶层的 await — 可以直接用 const data = await fetch(...)(需要组件外层包 <Suspense>

defineProps — 接收父组件数据

<script setup>
const props = defineProps<{
  title: string
  count?: number       // ? 表示可选
}>()
// JS 中用 props.title 访问(不用 .value)
console.log(props.title)
</script>
<template>
  <p>{{ title }}</p>     <!-- 模板中可以解构,直接写 title -->
</template>

对比选项式 API:props: ['title'] → 现在写成 defineProps<{ title: string }>()

defineEmits — 向父组件发事件

<script setup>
const emit = defineEmits<{
  (e: 'update', id: number): void
  (e: 'delete', name: string): void
}>()
function handleClick() {
  emit('update', 1)          // 触发 update 事件,带参数 1
}
</script>
<template>
  <button @click="handleClick">点我更新</button>
</template>

对比选项式 API:在 methods 里用 this.$emit('event') → 现在用 defineEmits 声明并得到 emit 函数。

useSlots / useAttrs

当你不通过 props 传数据,而是用插槽或额外属性时:

<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
console.log(slots.default?.())      // 默认插槽的内容
console.log(attrs.class)            // 父组件传进来的 class
</script>

这两个用得比较少,初期了解即可。

基础篇所有知识在这里的对照

| 选项式 API | <script setup> | | data() { return { x: 1 } } | const x = ref(1) | | props: ['title'] | defineProps<{ title: string }>() | | methods: { fn() {} } | function fn() {} | | computed: { val() {} } | const val = computed(() => ...) | | watch: { x() {} } | watch(x, (newVal) => ...) | | this.$emit('e') | const emit = defineEmits(); emit('e') | | components: { Comp } | 不用写,import 即可 |

新手常见问题

Q:<script setup> 比选项式 API 快吗? A:性能几乎一样。<script setup> 的好处是开发体验(更少代码、更好 TS 支持、更好逻辑复用)。 Q:能在同一个 .vue 文件里同时用 <script><script setup> 吗? A:可以,但很少需要这样做。一个文件里只有一个 <script setup>。 下一步: 📝 Note:基础篇回顾 如果你对 Vue 核心概念(条件渲染、事件处理、组件通信等)还不太熟,建议先回到 基础篇 看完,再来深入这里。

Vue 3 的响应式系统有多种工具函数,每个有特定的用途。本文详细介绍。

为什么需要 ref?

在 JavaScript 中,普通变量没有"通知机制":

let count = 0
count++    // 变了,但没人知道

Vue 需要在数据变化时自动更新 UI。普通变量做不到这一点,所以 Vue 提供了 ref —— 给变量包一层"响应式外壳":

const count = ref(0)
count.value++  // ✅ 变了,Vue 知道,UI 自动更新

ref 的本质:一个带有 .value 属性的响应式对象。ref(0) 返回的不是 0,而是 { value: 0 } 的代理对象。

可以把 ref 想象成快递追踪号——包裹还是那个包裹(值不变),但有了追踪号(ref),你就能实时知道它的状态变化。

ref — 基础类型首选

ref 创建一个响应式引用,适用于基本类型(数字、字符串、布尔值)。

<script setup>
import { ref } from 'vue'
const count = ref(0)
const name = ref('张三')
function increment() {
  count.value++      // ⚠️ JS 中必须用 .value
}
</script>
<template>
  <p>{{ count }}</p>   <!-- 模板中自动解包,不用 .value -->
  <button @click="increment">+1</button>
</template>

⚠️ Warning:ref 的关键规则

  • JS 代码里:访问/修改需要 .valuecount.value++
  • 模板里:自动解包,直接写 {{ count }}
  • 这是新手最容易忘的地方!

reactive — 对象类型独家

reactive 创建一个响应式对象,适合包含多个属性的数据。

<script setup>
import { reactive } from 'vue'
const user = reactive({
  name: '张三',
  age: 25,
  hobbies: ['读书', '运动']
})
function growUp() {
  user.age++          // ✅ 无需 .value,直接访问属性
}
</script>
<template>
  <p>{{ user.name }} - {{ user.age }}岁</p>
  <button @click="growUp">长大一岁</button>
</template>

ref vs reactive 全景对比

| | ref | reactive | | 适用类型 | 任何类型,但更推荐基础类型 | 对象、数组 | | 访问方式 | JS 中要 .value,模板中自动解包 | 直接访问属性,无 .value | | 重新赋值 | count.value = 5 ✅ | state = { x: 1 } ❌ 会丢失响应式 | | 解构 | 可以(配合 toRefs) | 不能直接解构(会丢响应式) | | 底层原理 | reactive 包装一层 | Proxy 直接代理 |

computed — 派生数据

和选项式 API 的 computed 一样,但写成函数调用:

<script setup>
import { ref, computed } from 'vue'
const price = ref(100)
const tax = computed(() => price.value * 0.1)        // 只读
const total = computed({                             // 可读写
  get: () => price.value + tax.value,
  set: (val) => { price.value = val / 1.1 }
})
</script>

watch — 监听变化

<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
// 监听单个
watch(keyword, (newVal, oldVal) => {
  console.log(`${oldVal} → ${newVal}`)
})
// 监听多个
const a = ref(1)
const b = ref(2)
watch([a, b], ([newA, newB]) => {
  console.log(newA, newB)
})
// 深度监听
watch(user, (newVal) => {}, { deep: true })
// 立即执行
watch(keyword, (val) => {}, { immediate: true })
</script>

watchEffect — 自动追踪 + 立即执行

watchEffectwatch 更"智能":它自动追踪内部用到的所有响应式数据,并且立即执行一次。

<script setup>
import { ref, watchEffect } from 'vue'
const count = ref(0)
const doubled = ref(0)
watchEffect(() => {
  doubled.value = count.value * 2    // 自动追踪 count
  console.log(`count 变了:${count.value}`)
})
// 组件创建时立即执行一次
// 之后 count 每变一次,就执行一次
</script>

watch vs watchEffect

  • watch:你知道要监听谁,明确指定
  • watchEffect:你不知道依赖了谁,让 Vue 自己追踪

shallowRef — 浅响应(性能优化)

ref 会深层追踪,对大对象可能浪费性能。shallowRef 只追踪 .value 的变化:

<script setup>
import { shallowRef } from 'vue'
const data = shallowRef({ name: 'Vue', version: 3 })
data.value = { name: 'New' }      // ✅ 触发更新(整个 .value 换了)
data.value.name = 'New'           // ❌ 不触发(内部属性变化不管)
// 需要强制更新时:
import { triggerRef } from 'vue'
data.value.name = 'New'
triggerRef(data)                  // 手动触发
</script>

readonly — 只读保护

把数据变成只读,防止意外修改:

<script setup>
import { reactive, readonly } from 'vue'
const original = reactive({ count: 0 })
const copy = readonly(original)
original.count++     // ✅ 改原始对象可以
copy.count++         // ❌ 警告:只读,不能改
</script>

toRef / toRefs — 保持响应式的解构

reactive 对象解构会丢失响应式,用 toRef / toRefs 保持连接:

<script setup>
import { reactive, toRef, toRefs } from 'vue'
const state = reactive({
  name: 'Vue',
  version: 3
})
const name = toRef(state, 'name')     // 取出单个属性
const { version } = toRefs(state)     // 取出多个属性(解构)
// name 和 state.name 保持同步
state.name = 'Vue 3'
console.log(name.value)          // 'Vue 3'
</script>

快速选择指南

| 场景 | 用什么 | | 基本类型(数字、字符串、布尔) | ref | | 普通对象 | reactiveref(看个人偏好) | | 根据其他数据计算 | computed | | 监听数据变化做操作 | watch / watchEffect | | 大对象、性能敏感 | shallowRef | | 防止修改 | readonly | | reactive 解构保持响应 | toRef / toRefs | 💡 Tip:一句话口诀

  • 基本类型用 ref,对象用 reactive
  • 计算用 computed,变化做 watch
  • reactive 解构加 toRefs,组件传参记得 defineProps 📝 Note:从选项式迁移过来? 如果你之前学的是选项式 API,建议对着 vue代码风格 里的对照表,把基础篇的概念逐一映射到组合式写法。

除了基础的 ref() / reactive(),Vue 还提供了一批 ref 工具函数 来处理特殊场景。

isRef / unref — 类型判断与自动解包

<script setup>
import { ref, isRef, unref } from 'vue'
const count = ref(0)
const plain = 42
console.log(isRef(count))     // true
console.log(isRef(plain))     // false
// unref:如果是 ref 则返回 .value,否则返回原始值
console.log(unref(count))     // 0
console.log(unref(plain))     // 42
// 等价于:
function myUnref(r) {
  return isRef(r) ? r.value : r
}
</script>

适用于工具函数——你不想限制参数必须是 ref 还是普通值:

<script setup>
function formatMsg(msg) {
  return `消息:${unref(msg)}`
}
const msgRef = ref('Hello')
console.log(formatMsg(msgRef))   // 消息:Hello
console.log(formatMsg('World'))  // 消息:World
</script>

shallowRef — 浅响应优化

ref 会递归地把整个数据变成响应式。如果数据很大不需要深层响应,用 shallowRef 省性能。

<script setup>
import { shallowRef, triggerRef } from 'vue'
const logs = shallowRef([])
// 直接推入元素:数组变了,但 shallowRef 没检测到
logs.value.push('新日志')    // ❌ 不触发更新
// 替换整个引用:检测到
logs.value = [...logs.value, '新日志']  // ✅ 触发更新
// 或者用 triggerRef 手动通知
logs.value.push('新日志')
triggerRef(logs)            // ✅ 手动触发更新
</script>

典型场景

| 场景 | 说明 | | 大型列表 | 几千条日志、表格数据,不需要逐条追踪 | | 第三方数据 | 从外部库拿到的不可变数据 | | 频繁替换 | 每次都重新赋值整个对象 |

shallowRef + triggerRef 搭配使用,兼顾性能和灵活性。

customRef — 自定义 ref

customRef 让你完全控制 ref 的依赖追踪触发更新时机。最常见的场景是防抖

<script setup>
import { customRef } from 'vue'
function debouncedRef(value, delay = 300) {
  let timer
  return customRef((track, trigger) => {
    return {
      get() {
        track()          // 告诉 Vue:这个值被依赖了
        return value
      },
      set(newValue) {
        clearTimeout(timer)
        timer = setTimeout(() => {
          value = newValue
          trigger()      // 告诉 Vue:值变了,通知依赖方
        }, delay)
      }
    }
  })
}
const keyword = debouncedRef('', 500)  // 500ms 防抖
</script>
<template>
  <input v-model="keyword" placeholder="防抖搜索" />
</template>

customRef 接收一个工厂函数,参数是 tracktrigger

  • track() — 在 get 里调用,标记依赖追踪
  • trigger() — 在 set 里调用,触发更新通知

useTemplateRef — 模板引用新写法(Vue 3.5+)

Vue 3.5 引入了 useTemplateRef,统一了模板 ref 的声明方式,不再要求变量名和模板 ref 名一致

<script setup>
import { useTemplateRef, onMounted } from 'vue'
const input = useTemplateRef('myInput')
// 等价于 <script setup> 中 const input = ref(null) + ref="myInput"
onMounted(() => {
  input.value?.focus()
})
</script>
<template>
  <input ref="myInput" type="text" />
</template>

优点:

  • 变量名和 ref 名可以不同
  • 类型推导更好
  • 语义清晰:一看就知道是模板引用

在 v-for 中使用

<script setup>
import { useTemplateRef, onMounted } from 'vue'
const items = useTemplateRef('items')
onMounted(() => {
  console.log(items.value)  // HTMLElement[]
})
</script>
<template>
  <li v-for="item in list" :key="item" ref="items">{{ item }}</li>
</template>

defineExpose — 控制子组件暴露的内容

父组件通过 ref 获取子组件实例时,默认什么都拿不到(<script setup> 是关闭的)。 需要用 defineExpose 显式暴露

<!-- Child.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
const internal = ref('秘密')  // 不暴露
function reset() { count.value = 0 }
defineExpose({ count, reset })
</script>
<!-- Parent.vue -->
<script setup>
import { useTemplateRef } from 'vue'
import Child from './Child.vue'
const childRef = useTemplateRef('child')
function handleClick() {
  childRef.value?.reset()
  console.log(childRef.value?.count)
}
</script>
<template>
  <Child ref="child" />
  <button @click="handleClick">重置</button>
</template>

选项式 API 中,$refs 会自动暴露所有 datamethods,不需要 defineExpose

函数式 ref

ref 属性也可以传一个函数,在元素挂载或卸载时调用:

<script setup>
import { ref } from 'vue'
const divEl = ref(null)
function setRef(el) {
  divEl.value = el
  console.log('元素挂载了', el)
}
</script>
<template>
  <div :ref="setRef">内容</div>
</template>

函数接收元素本身作为参数,卸载时传 null。适合在需要动态处理 ref 时使用。

TypeScript 类型安全

ref 类型

<script setup lang="ts">
import { ref, type Ref } from 'vue'
const count = ref<number>(0)
const user = ref<{ name: string; age: number } | null>(null)
// 显式标注 Ref 类型(不常用)
const message: Ref<string> = ref('hello')
</script>

组件 ref 类型

InstanceType 获取组件实例类型:

<script setup lang="ts">
import { useTemplateRef } from 'vue'
import MyInput from './MyInput.vue'
type MyInputInstance = InstanceType<typeof MyInput>
const inputRef = useTemplateRef<MyInputInstance>('input')
onMounted(() => {
  inputRef.value?.focus()
})
</script>
<template>
  <MyInput ref="input" />
</template>

或者用 typeof + defineExpose 配合:

<script setup lang="ts">
// 只声明暴露的类型,不暴露实现细节
defineExpose({
  count: ref(0),
  reset: () => {}
})
</script>

模板 ref 类型

<script setup lang="ts">
const divRef = useTemplateRef<HTMLDivElement>('div')
const inputRef = useTemplateRef<HTMLInputElement>('input')
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvas')
</script>
<template>
  <div ref="div">块</div>
  <input ref="input" />
  <canvas ref="canvas"></canvas>
</template>

速查表

| API | 用途 | 层级 | | isRef() | 判断是否是 ref | 工具 | | unref() | 自动解包(ref 取 .value,否则原值) | 工具 | | shallowRef() | 浅响应,大对象性能优化 | 响应式 | | triggerRef() | 手动触发 shallowRef 更新 | 响应式 | | customRef() | 自定义追踪/触发逻辑(防抖等) | 自定义 | | useTemplateRef() | 声明模板引用(Vue 3.5+) | 模板 | | defineExpose() | 控制组件暴露给父级的属性 | 组件 | | 函数式 :ref | 动态处理 ref 回调 | 模板 | 📝 Note 基础 ref() 用法见 ,模板 ref 基础见 。

推荐几个 Vue 开发必备的 VSCode 插件。

Vue - Official(必装)

这是 Vue 官方出品的 VSCode 插件,提供:

  • .vue 文件语法高亮(模板、脚本、样式分颜色显示)
  • 代码智能提示(输入时自动补全组件名、属性名)
  • 类型检查(TypeScript 集成)
  • <slot> 名称提示
  • CSS 类名补全 安装: VSCode 扩展商店搜索 Vue - Official

能获得什么?

安装后:

  1. 输入 v- 会提示所有 Vue 指令(v-ifv-forv-model...)
  2. <MyButton 自动提示导入路径
  3. defineProps 的类型会即时检查
  4. 模板中鼠标悬停变量显示类型

其他推荐插件

| 插件 | 作用 | | Error Lens | 把错误信息直接显示在行尾,不用鼠标悬停 | | Auto Rename Tag | 改标签名时自动同步改闭合标签 | | ESLint | 代码规范检查和自动修复 | | Prettier | 代码自动格式化 | | Path Intellisense | 文件路径自动补全 |

与 ESLint 配合

Vue - Official 内置了格式化能力,但如果你同时装了 Prettier,建议在 VSCode 设置中指定: .vscode/settings.json

{
  "editor.defaultFormatter": "Vue.volar",
  "[javascript]": {
    "editor.defaultFormatter": "Vue.volar"
  }
}

这样 .vue 文件就用 Vue - Official 的格式化,避免和 Prettier 打架。

整理 Vue 开发中最常遇到的报错,帮你快速定位问题。

找不到模块 ./App.vue 的声明文件

无法找到模块"./App.vue"的声明文件。"path/App.vue"隐式拥有 "any" 类型。

这是 TypeScript 不认识 .vue 文件导致的。在 src/ 下创建文件 env.d.ts

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

如果项目是用 npm create vue@latest 创建的,这个文件已经自带了,不用手动加。

ref 忘了用 .value

const count = ref(0)
// ❌ console.log(count)       // 打印 RefImpl { ... } 对象,不是数字
// ✅ console.log(count.value)  // 打印 0
// ❌ count++                   // 语法上能过但不会触发响应式
// ✅ count.value++

💡 Tip:记住<script> 里操作 ref 必须用 .value,模板里不需要。

reactive 整体替换丢失响应式

// ❌ 这样会丢响应式
let state = reactive({ count: 0 })
state = { count: 1 }           // state 指向了新对象,Proxy 包装没了
// ✅ 修改属性
state.count = 1
// 或者用 ref 代替,支持整体替换
const state = ref({ count: 0 })
state.value = { count: 1 }     // ✅ ref 支持替换

defineProps 类型声明错误

// ✅ 纯类型声明
const props = defineProps<{ title: string }>()
// ✅ 运行时声明(不用 TS 时可以这样)
const props = defineProps({ title: { type: String, required: true } })
// ❌ 不能混合使用
const props = defineProps({ title: String })<{ title: string }>

两种声明方式只能选一种。

v-for 缺少 :key

[vue/require-v-for-key] Elements in iteration expect to have 'v-bind:key' directives.

v-for 但没加 :key。加上就好了:

<li v-for="item in items" :key="item.id">{{ item.name }}</li>

组件命名冲突

[Vue warn]: Do not use built-in or reserved HTML elements as component id: header

组件名和 HTML 原生标签重名了。解决:用多单词命名,如 AppHeader 而不是 Header

$refs 访问时机错误

TypeError: Cannot read properties of undefined (reading 'focus')

mounted 之前访问了 $refs。确保在 mounted() 生命周期或之后(如事件回调中)访问。

v-html 没有任何内容显示

<div v-html="htmlContent"></div>

检查 htmlContent 的值——如果它是 null 或空字符串,自然什么都不显示。另外注意:v-html覆盖元素内的所有内容

双向绑定不生效

<input v-model="formData.name" />   <!-- ❌ formData 不是响应式的 -->

formData 必须是用 reactive 创建的,或 formData.nameref。如果 formData 是个普通对象,v-model 不会生效。

快速排错清单

| 症状 | 首先检查 | | 页面没更新 | 数据是不是响应式的(ref/reactive/data) | | 子组件收不到数据 | props 声明了吗?传值加冒号了吗? | | 事件不触发 | 方法名拼写对不对?@click 写了吗? | | 样式不生效 | scoped 了吗?class 名对不对? | | 组件报错不显示 | 检查浏览器控制台(F12)的错误信息 |