当前端页面越来越复杂时,只依靠原生 JavaScript 手动获取 DOM、修改内容、绑定事件,会让代码逐渐变得零散而难维护。尤其是当页面中存在大量列表、表单、弹窗、状态切换和组件复用时,我们就需要一个更高效的前端框架来管理界面。
Vue 就是在这种背景下诞生的。它通过声明式渲染、响应式数据和组件化开发,让我们能以更清晰的方式构建用户界面。
如果说前面的 HTML、CSS、JavaScript、Fetch / Ajax 让你具备了开发网页和请求数据的基础能力,那么 Vue 会进一步帮你把这些能力组织成真正的应用结构。
1. 什么是 Vue
Vue 是一个用于构建用户界面的 JavaScript 框架。
它最核心的价值有三点:
- 声明式渲染:你描述“页面应该长什么样”,Vue 负责帮你更新 DOM。
- 响应式数据:数据变化后,页面会自动更新。
- 组件化开发:可以把复杂页面拆成一个个可复用的小模块。
例如,在原生 JavaScript 中,你可能需要这样改文字:
const title = document.getElementById("title");
title.innerText = "欢迎来到 Vue";
而在 Vue 中,你更多是在描述:
<h1>{{ title }}</h1>
只要 title 这个数据变化,页面就会自动更新。
这就是 Vue 最重要的思维方式变化:
不再频繁手动操作 DOM,而是优先操作数据,让框架负责视图更新。
2. Vue 和原生 JavaScript 操作 DOM 的区别
2.1 原生 JavaScript 的特点
原生 JavaScript 并不是不能做页面开发,而是随着页面复杂度提升,会越来越容易出现这些问题:
- DOM 查询代码变多
- 事件绑定分散
- 状态管理混乱
- 多处逻辑相互影响
- 可复用性差
2.2 Vue 的优势
Vue 把“数据”和“页面展示”建立了直接关系:
- 数据驱动页面
- 模板负责结构
- 指令负责逻辑表达
- 组件负责拆分与复用
这意味着:
- 你更关注“状态是什么”
- 不必反复手动改每一个 DOM 节点
- 页面结构更适合维护和扩展
3. 创建第一个 Vue 应用
在真实项目中,Vue 通常会通过工程化方式创建,例如使用 Vite。不过为了先理解基础结构,我们先看最核心的代码形式。
3.1 一个最小示例
import { createApp } from "vue";
// 定义根组件
const App = {
template: `<h1>Hello Vue</h1>`
};
// 创建应用并挂载到页面容器
createApp(App).mount("#app");
这里有三个关键信息:
createApp():创建一个 Vue 应用实例App:当前应用的根组件.mount("#app"):把 Vue 应用挂载到页面中的某个 DOM 容器上
例如页面中通常会有一个容器:
<div id="app"></div>
Vue 会接管这个容器,并把组件渲染进去。
3.2 为什么真实项目常用 Vite
因为 Vue 项目往往会使用:
- 单文件组件
.vue - 模块化导入导出
- 本地开发服务器
- 自动热更新
所以真正开发时,通常会用 Vite + Vue 3 作为项目基础。
3.3 这些代码到底放在哪里
很多初学者第一次看到下面这种代码时,会立刻困惑:
import { createApp } from "vue";
因为在前面学习原生 HTML、CSS、JavaScript 时,我们通常是在浏览器里直接写:
<script>
// 直接写代码
</script>
但 Vue 工程项目通常不是这样运行的。更常见的结构是:
my-vue-app/
index.html
package.json
src/
main.js
App.vue
其中:
index.html:页面入口,里面通常有一个<div id="app"></div>src/main.js:JavaScript 入口文件,通常在这里写createApp(App).mount("#app")src/App.vue:根组件,也就是整个应用最外层的 Vue 组件
也就是说,前面看到的 createApp() 代码,通常并不是随便写在浏览器控制台里,而是写在 src/main.js 中。
3.4 如何用 Vite 创建一个 Vue 3 项目
如果你已经安装了 Node.js,就可以通过命令行快速创建一个 Vue 项目。
最常见的流程如下:
npm create vite@latest my-vue-app -- --template vue
cd my-vue-app
npm install
npm run dev
这几步分别表示:
- 创建一个基于 Vite 的 Vue 项目
- 进入项目目录
- 安装依赖包
- 启动本地开发服务器
启动成功后,终端通常会给你一个本地地址,例如:
http://localhost:5173/
浏览器访问这个地址,就能看到项目跑起来了。
💡 如果你对
Node.js、npm、Vite这些词还不熟悉: 它们并不是 Vue 本身的语法,而是现代前端工程化开发的基础工具。后续最好单独补上这部分知识,否则很容易出现“会写 Vue,但不会启动项目”的情况。
3.5 第一次看懂项目目录
一个最基础的 Vue 3 + Vite 项目里,经常会看到这些文件:
package.json:记录项目依赖和可执行脚本index.html:浏览器入口页面src/main.js:应用启动入口src/App.vue:根组件src/components/:放可复用组件src/assets/:放图片、样式等静态资源
你可以先建立这样一个最简心智模型:
- 浏览器先打开
index.html index.html再加载src/main.jsmain.js创建 Vue 应用并挂载App.vueApp.vue再继续组合其他组件
3.6 为什么 .vue 文件能运行
.vue 文件叫作 单文件组件(Single File Component)。
例如:
<script setup>
const title = "Hello Vue";
</script>
<template>
<h1>{{ title }}</h1>
</template>
<style>
h1 {
color: #42b883;
}
</style>
浏览器本身其实并不直接认识 .vue 文件。
之所以它能运行,是因为 Vite 和 Vue 的相关插件会在开发和构建阶段把它转换成浏览器能理解的 JavaScript、HTML 和 CSS。
这也是为什么:
.vue文件不能直接双击运行import在工程项目里能用- Vue 项目通常需要
npm run dev
它们背后依赖的都是工程化工具链。
4. 模板语法:让数据显示到页面上
Vue 的模板语法本质上是在 HTML 里嵌入和数据相关的表达方式。
4.1 插值表达式
<h1>{{ title }}</h1>
<p>{{ message }}</p>
这里的 {{ }} 表示把 JavaScript 数据插入页面。
4.2 属性绑定
如果要给 HTML 属性绑定动态值,使用 v-bind,简写为 :。
<img :src="imageUrl" :alt="imageDesc">
<a :href="link">查看详情</a>
动态 Class 绑定(高频交互): 我们可以通过对象语法动态切换 class,这在做 Tab 切换、高亮选中时非常常用:
<!-- 当 isActive 为 true 时,加上 'active' 这个类名 -->
<div :class="{ active: isActive }">内容</div>
4.3 事件绑定
给元素绑定事件,使用 v-on,简写为 @。
<button @click="handleClick">点我</button>
5. 常用指令:Vue 模板的基础能力
指令是 Vue 提供的一套特殊语法,用于在模板中表达逻辑。
5.1 v-if 与 v-show:条件渲染
<p v-if="isLogin">欢迎回来</p>
<p v-else>请先登录</p>
当 isLogin 为 true 时显示第一段,否则显示第二段。
💡 面试必考:
v-ifvsv-show
v-if是“真正的”条件渲染,如果不满足条件,DOM 元素会被直接移除/销毁。v-show只是简单地切换 CSS 的display: none。- 选择建议:如果需要非常频繁地切换显示/隐藏,用
v-show(性能更好);如果在运行时条件很少改变,用v-if。
5.2 v-for:列表渲染
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
这里表示遍历 users 数组,把每一项渲染成列表。
💡
key很重要: 它用于帮助 Vue 更准确地识别每一项,提高更新效率,也能减少意外的渲染问题。
5.3 v-model:双向绑定
<input v-model="username" placeholder="请输入用户名">
<p>当前输入:{{ username }}</p>
输入框的内容会同步到 username,而 username 的变化也会自动体现在页面中。
这在表单开发里非常常见。
6. 组合式 API:Vue 3 的主流写法
Vue 3 最推荐的开发方式是 组合式 API(Composition API)。
它的核心思想是:
按“逻辑功能”组织代码,而不是按
data、methods、computed这些固定区域拆开。
在 Vue 3 中,最常见的组合式 API 会放在 setup 或 <script setup> 中编写。
6.1 ref:定义基础响应式数据
import { ref } from "vue";
// ref 适合包裹基础类型数据
const count = ref(0);
function add() {
// 在 JS 中修改 ref 要通过 .value
count.value++;
}
模板中使用时:
<button @click="add">点击次数:{{ count }}</button>
这里有两个关键点:
ref(0)创建了一个响应式数据- 在 JavaScript 中访问它需要
.value
6.2 reactive:定义对象类型数据
import { reactive } from "vue";
// reactive 适合处理对象结构的数据
const user = reactive({
name: "Tom",
age: 20
});
它适合处理对象结构的数据。
例如:
<p>{{ user.name }} - {{ user.age }}</p>
6.3 ref 和 reactive 怎么选
入门阶段可以这样理解:
- 基础类型,如数字、字符串、布尔值,优先用
ref - 对象、数组等结构化数据,可以使用
reactive
不过在很多真实项目中,开发者也会统一更多地使用 ref,因为它的行为更稳定、使用边界更清晰。
6.4 响应式丢失陷阱(新手高频 Bug)
使用 reactive 时,千万不要直接对它进行解构赋值,否则会丢失响应式特性:
const user = reactive({ name: "Tom", age: 20 });
// ❌ 错误做法:这里的 name 变成了普通字符串,修改它页面不会更新!
/*
let { name } = user;
const changeName = () => {
name = "Jerry"
console.log(name) // 控制台打印 Jerry,但页面不会更新
}
*/
// ✅ 正确做法:始终通过 user.name 访问,或使用 toRefs 转换
const changeName = () => {
// 直接修改响应式对象的属性,触发更新
user.name = "Jerry"
}
7. 计算属性与监听
7.1 computed:根据已有数据推导新值
import { ref, computed } from "vue";
const firstName = ref("张");
const lastName = ref("三");
// 根据已有状态派生出新结果
const fullName = computed(() => {
return firstName.value + lastName.value;
});
模板中直接使用:
<p>{{ fullName }}</p>
适合场景:
- 拼接姓名
- 统计购物车总价
- 根据状态生成展示文案
7.2 watch:监听数据变化
import { ref, watch } from "vue";
const keyword = ref("");
// 监听 keyword 变化,执行副作用逻辑
watch(keyword, (newValue, oldValue) => {
console.log("新值:", newValue);
console.log("旧值:", oldValue);
});
适合场景:
- 监听输入变化后发请求
- 监听筛选条件变化后重新加载列表
- 监听某个状态变化后执行副作用逻辑
7.3 进阶:深度监听 (Deep Watch)
在 Vue 3 中,watch 的行为会根据你监听的数据源类型而有所不同:
- 直接监听
reactive对象:Vue 会自动开启深度监听。无论你修改了对象里多深的属性,都会触发回调。 - 监听
ref中保存的对象,或监听返回对象的 getter 函数:默认是浅层监听。如果不加deep: true,只有对象本身被替换时才会触发。
如果你想监听一个 ref 内部对象的深层变化,或者通过 getter 函数监听 reactive 对象内部的某个嵌套对象,就需要手动开启 { deep: true }:
import { ref, reactive, watch } from "vue";
// 场景 1:直接监听 reactive 对象(自动深度监听)
const user = reactive({ profile: { age: 20 } });
watch(user, () => {
console.log("user 内部数据变了!"); // user.profile.age = 21 会触发
});
// 场景 2:监听 ref 对象(需要手动开启深度监听)
const settings = ref({ theme: 'dark', layout: { sidebar: true } });
watch(
settings,
() => {
console.log("settings 内部数据变了!");
},
{ deep: true } // 必须加这个,否则 settings.value.layout.sidebar = false 不会触发
);
// 场景 3:通过 getter 监听 reactive 对象的某个嵌套属性(需要手动开启深度监听)
watch(
() => user.profile,
() => {
console.log("profile 内部数据变了!");
},
{ deep: true } // 必须加这个,否则 user.profile.age = 22 不会触发
);
⚠️ 性能警告: 深度监听会递归遍历对象的所有属性,如果对象非常大,开启
deep: true可能会带来性能开销。因此,如果只需要监听对象中的某一个特定基础属性,更推荐的做法是直接监听那个属性的 getter 函数(不需要 deep):// 只监听 user.profile.age 的变化,性能最好 watch( () => user.profile.age, (newAge) => { console.log("年龄变了:", newAge); } );
💡 区别要记住:
computed更像“根据旧数据算新数据”;watch更像“当数据变化时执行额外动作”。
8. 生命周期:组件什么时候开始工作
Vue 组件从创建到销毁,会经历一系列阶段,这些阶段就叫生命周期。
在组合式 API 中,最常用的是 onMounted。
8.1 onMounted(挂载完毕)
import { onMounted } from "vue";
onMounted(() => {
// 组件首次挂载完成后执行
console.log("组件已经挂载到页面上");
});
它常用于:
- 页面加载完成后请求接口
- 初始化图表
- 获取某个 DOM 节点
如果你后续要在 Vue 中配合 axios 请求接口,最常见的写法就是在 onMounted() 中发起请求。
8.2 onUnmounted(卸载完毕)
import { onUnmounted } from "vue";
onUnmounted(() => {
// 组件销毁前后常用来做清理工作
console.log("组件已经被销毁");
});
它常用于:
- 清除定时器(
clearInterval) - 移除全局事件监听(
window.removeEventListener) - 断开 WebSocket 连接
⚠️ 易错坑点: 如果你在组件里设置了一个定时器,但在组件销毁时没有通过
onUnmounted清除它,这个定时器会在后台一直运行,导致内存泄漏和不可预期的 Bug。
8.3 获取真实 DOM (Template Refs)
虽然 Vue 提倡数据驱动,不建议直接操作 DOM,但有时接入第三方库(如 ECharts 图表)或需要使输入框自动聚焦时,必须获取真实 DOM。
在 Vue 3 中,通过声明一个同名的 ref 变量即可:
<script setup>
import { ref, onMounted } from 'vue';
// 1. 声明一个名字和模板里 ref 属性一样的变量
const myInput = ref(null);
onMounted(() => {
// 3. 在挂载完成后,才能拿到真实的 DOM 元素
myInput.value.focus();
});
</script>
<template>
<!-- 2. 绑定 ref 属性 -->
<input ref="myInput" type="text" />
</template>
9. 组件通信:父子组件如何传数据
Vue 的强大之处,很大程度上来自组件化开发。但组件拆分后,数据就隔离了。父组件如何把数据传给子组件?子组件又如何告诉父组件自己发生了什么?
这就需要用到组件通信的两个核心概念:props 和 emits。
9.1 父传子:使用 defineProps 接收数据
就像你给 HTML 标签传递 id 或 class 属性一样,你可以给自定义组件传递自定义属性。这在 Vue 中叫做 props。
子组件(ArticleCard.vue):声明自己需要哪些数据。
<script setup>
// 使用 defineProps 定义接收的属性
const props = defineProps({
title: String,
author: String
});
</script>
<template>
<div class="card">
<h3>{{ title }}</h3>
<p>作者:{{ author }}</p>
</div>
</template>
父组件(App.vue):向子组件传递数据。
<template>
<!-- 像写 HTML 属性一样传值 -->
<ArticleCard title="Vue 入门教程" author="张三" />
<!-- 如果传递的是动态变量,需要加 : (v-bind) -->
<ArticleCard :title="dynamicTitle" :author="currentAuthor" />
</template>
9.2 子传父:使用 defineEmits 发送事件
子组件内部发生了一些操作(比如用户点击了删除按钮),需要通知父组件去更新列表,这时就需要 emit 抛出自定义事件。
子组件(TodoItem.vue):定义并触发事件。
<script setup>
// 定义当前组件会触发哪些自定义事件
const emit = defineEmits(["remove"]);
function handleRemove() {
// 触发名为 'remove' 的事件,还可以带上参数:emit('remove', 123)
emit("remove");
}
</script>
<template>
<div>
<span>买牛奶</span>
<button @click="handleRemove">删除</button>
</div>
</template>
父组件(App.vue):监听子组件发出的事件。
<script setup>
function deleteTodo() {
console.log("父组件收到了删除通知,准备删除数据!");
}
</script>
<template>
<!-- 使用 @ 监听子组件自定义的 remove 事件 -->
<TodoItem @remove="deleteTodo" />
</template>
🔑 总结:这就是最基础的组件通信模型:数据向下流(Props),事件向上走(Emits)。理解了这个,你就掌握了编写复杂页面的基础。
9.3 父传结构:使用插槽 (<slot>)
除了传数据,很多时候我们需要向子组件传递 HTML 结构(例如自定义弹窗组件,内部的内容由父组件决定)。这就是插槽的作用。
子组件(Modal.vue):留下占位符。
<template>
<div class="modal">
<div class="modal-header">提示</div>
<div class="modal-body">
<!-- 默认插槽:父组件传进来的结构会替换这里 -->
<slot>默认内容</slot>
</div>
</div>
</template>
父组件:传入具体结构。
<template>
<Modal>
<!-- 这里的结构会被塞进子组件的 <slot> 里 -->
<p>确定要删除这条数据吗?</p>
<button>确认</button>
</Modal>
</template>
9.4 暴露子组件方法:defineExpose
在 <script setup> 中,组件默认是封闭的。如果父组件想通过 ref 直接调用子组件里的方法或访问数据,子组件必须使用 defineExpose 显式暴露出来。这在封装复杂组件(如表单校验、弹窗控制)时非常常用。
子组件 (Child.vue):
<script setup>
import { ref } from 'vue'
const isVisible = ref(false)
const open = () => {
isVisible.value = true
}
// 只有暴露出去,父组件才能调用 open 方法
defineExpose({
open
})
</script>
<template>
<div v-if="isVisible">我是一个弹窗</div>
</template>
父组件:
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
// 1. 创建一个 ref 变量,名字要和模板里的 ref 属性一致
const childRef = ref(null)
const handleOpen = () => {
// 2. 通过 .value 访问子组件实例,并调用其暴露的方法
childRef.value.open()
}
</script>
<template>
<button @click="handleOpen">打开子组件弹窗</button>
<!-- 绑定 ref -->
<Child ref="childRef" />
</template>
9.5 双向绑定进阶:defineModel (Vue 3.4+)
在封装自定义输入框或表单组件时,我们希望像原生 <input v-model="val"> 一样使用自定义组件。在 Vue 3.4 之前,这需要结合 props: ['modelValue'] 和 emits: ['update:modelValue'] 来实现,比较繁琐。
Vue 3.4 引入了 defineModel 宏,让自定义组件的双向绑定变得极其简单:
子组件 (CustomInput.vue):
<script setup>
// 声明一个 model,Vue 会自动处理 modelValue prop 和 update:modelValue 事件
const model = defineModel()
</script>
<template>
<!-- 直接绑定到原生 input 上 -->
<input v-model="model" class="custom-input" />
</template>
父组件:
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
const username = ref('')
</script>
<template>
<!-- 完美支持 v-model -->
<CustomInput v-model="username" />
<p>当前输入:{{ username }}</p>
</template>
10. 进阶:DOM 更新机制与 nextTick
在 Vue 中,当你修改了响应式数据时,DOM 不会立即更新。Vue 会将这些更新操作放入一个队列中,等到下一个“Tick”(微任务阶段)才统一执行 DOM 更新。这种机制是为了避免频繁操作 DOM 带来的性能问题。
但这也带来了一个常见问题:修改数据后,立刻去获取 DOM 元素,拿到的还是旧的。
<script setup>
import { ref, nextTick } from 'vue'
const message = ref('Hello')
const textRef = ref(null)
const updateText = async () => {
message.value = 'World'
// 此时 DOM 还没更新,打印出来的还是 'Hello'
console.log('直接获取:', textRef.value.innerText)
// 等待 DOM 更新完成
await nextTick()
// 此时 DOM 已经更新,打印出来的是 'World'
console.log('nextTick 后获取:', textRef.value.innerText)
}
</script>
<template>
<div ref="textRef">{{ message }}</div>
<button @click="updateText">修改文本</button>
</template>
nextTick 在处理需要依赖最新 DOM 状态的场景(如:列表新增数据后自动滚动到底部、弹窗打开后立刻让输入框获取焦点)时是必不可少的利器。
11. 组合式 API 与选项式 API 的区别
很多初学者在学习 Vue 时,都会遇到两个术语:
- 组合式 API(Composition API)
- 选项式 API(Options API)
10.1 选项式 API 长什么样
例如:
export default {
data() {
return {
count: 0
};
},
methods: {
add() {
this.count++;
}
}
};
它会把数据、方法、计算属性分别写在不同区域。
10.2 为什么现在更推荐组合式 API
因为当组件逻辑变复杂时,组合式 API 有几个明显优势:
- 相关逻辑可以写在一起
- 更利于抽离复用逻辑
- 更适合 TypeScript 和中大型项目
- 更符合 Vue 3 的主流生态
10.3 那选项式 API 还要不要学
要知道它的存在,因为:
- 老项目里很常见
- 很多旧教程还在使用
- 面试和工作中仍然可能遇到
但如果你现在是从零开始建立新知识体系,建议主线先学:
Vue 3 + Composition API
12. 一个完整的 Vue 3 组合式 API 小例子
下面是一个简化后的计数器示例:
<script setup>
import { ref, computed } from "vue";
const count = ref(0);
// 计算属性会跟随 count 自动更新
const doubleCount = computed(() => count.value * 2);
function add() {
count.value++;
}
</script>
<template>
<div>
<p>当前计数:{{ count }}</p>
<p>双倍结果:{{ doubleCount }}</p>
<button @click="add">+1</button>
</div>
</template>
这个例子同时体现了:
ref定义响应式数据computed计算派生值- 事件绑定
@click - 模板自动响应更新
13. 用 Vue.js devtools 看懂组件状态
当你开始写 Vue 页面后,很快就会遇到一个问题:数据明明变了,页面为什么没有按预期更新?
这时候,Vue.js devtools 就很有价值了。它是 Vue 官方生态里最常用的调试工具,可以帮助你直接查看当前页面上的组件树,以及每个组件里的 props、响应式状态和事件变化。
你可以把它理解成:专门给 Vue 组件准备的“可视化调试面板”。
12.1 它最适合用来做什么
在日常开发中,Vue.js devtools 最常见的作用有这些:
- 查看当前页面渲染出了哪些组件
- 检查父组件传给子组件的
props是否正确 - 观察
ref、reactive、computed等状态的实时变化 - 排查“数据变了但页面没更新”到底卡在哪一层
比如你写了前面的计数器例子,就可以一边点击按钮,一边在 devtools 中看 count 和 doubleCount 的变化,这比只靠 console.log() 更直观。
12.2 新手应该重点看哪几个地方
通常先看这两个面板就够用了:
Components:查看组件层级、组件props和本地状态Timeline:观察组件更新、事件触发等运行过程
如果你在做中大型项目,后面接入 Pinia 后,它也能帮助你观察全局状态变化。
12.3 为什么它比单纯打印日志更高效
console.log() 更适合看某一个瞬时值,而 Vue.js devtools 更适合看组件关系、状态流动和响应式更新过程。
对于 Vue 初学者来说,越早学会用它,越容易真正理解:
- 数据是从哪里传下去的
- 组件是在哪一层更新的
- 当前看到的页面结果,到底由哪一份状态驱动
14. Vue 基础和后续实战怎么衔接
当你掌握了模板语法、响应式数据、生命周期和组件基础后,就可以把前面学过的网络请求能力接到 Vue 项目里。
例如:
- 在
onMounted中发起请求 - 用
ref保存接口返回的数据 - 用
v-for渲染列表 - 用
v-if控制加载和错误状态 - 用组件拆分页面结构
这就是 Vue 项目最常见的基础开发流程。
15. 小结
Vue 的核心不是“记住多少 API”,而是建立这样一套思维方式:
- 用数据驱动页面
- 用响应式系统自动更新视图
- 用组件拆分复杂界面
- 用组合式 API 组织业务逻辑
掌握这些之后,你就已经完成了从“会写页面”到“会搭应用结构”的关键跨越。