Vue3 基础入门
2026/3/20大约 9 分钟
Vue3 基础入门:拥抱下一代前端框架
从 Vue2 迁移到 Vue3,就像从功能机换到智能机。虽然基本操作相似,但底层架构和使用体验都有了质的飞跃。本文将带你全面认识 Vue3,从环境搭建到核心概念,开启 Vue3 开发之旅。
Vue3 带来了什么?
相比 Vue2 的主要改进
| 特性 | Vue2 | Vue3 |
|---|---|---|
| 响应式系统 | Object.defineProperty | Proxy |
| API 风格 | Options API | Composition API + Options API |
| TypeScript | 需要额外配置 | 原生支持 |
| 性能 | 优秀 | 更优秀(体积更小、渲染更快) |
| Tree-shaking | 部分支持 | 完全支持 |
| 多根节点 | 不支持 | 支持(Fragment) |
为什么要升级 Vue3?
- 更好的性能:重写的虚拟 DOM,编译时优化,更快的渲染速度
- 更小的体积:支持 tree-shaking,最小仅 10KB
- 更好的 TypeScript 支持:源码用 TypeScript 重写
- Composition API:更好的逻辑复用,更灵活的代码组织
- 新的内置组件:Teleport、Suspense 等
环境搭建
方式一:Vite(推荐)
# npm
npm create vite@latest my-vue3-app -- --template vue
# yarn
yarn create vite my-vue3-app --template vue
# pnpm
pnpm create vite my-vue3-app --template vue
# 进入项目
cd my-vue3-app
# 安装依赖
npm install
# 启动开发服务器
npm run dev
Vite 是 Vue 作者尤雨溪开发的下一代前端构建工具,具有:
- 极速的冷启动
- 即时的热更新
- 真正的按需编译
方式二:Vue CLI
# 确保 Vue CLI 版本 >= 4.5
npm install -g @vue/cli@4.5.15
# 创建项目
vue create my-vue3-app
# 选择 Vue 3 preset 或手动选择特性
方式三:CDN(学习用)
<!DOCTYPE html>
<html>
<head>
<title>Vue3 入门</title>
<script src="https://unpkg.com/vue@3.2.47/dist/vue.global.js"></script>
</head>
<body>
<div id="app">{{ message }}</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
message: "Hello Vue3!",
};
},
}).mount("#app");
</script>
</body>
</html>
应用创建
Vue2 vs Vue3 对比
// Vue2
import Vue from "vue";
import App from "./App.vue";
new Vue({
render: (h) => h(App),
}).$mount("#app");
// Vue3
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.mount("#app");
应用配置
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
const app = createApp(App);
// 全局属性(替代 Vue.prototype)
app.config.globalProperties.$api = api;
app.config.globalProperties.$filters = filters;
// 全局组件
app.component("GlobalButton", GlobalButton);
// 全局指令
app.directive("focus", {
mounted(el) {
el.focus();
},
});
// 使用插件
app.use(router);
app.use(store);
// 错误处理
app.config.errorHandler = (err, instance, info) => {
console.error("Global error:", err);
};
// 挂载
app.mount("#app");
多应用实例
Vue3 支持在同一页面创建多个应用实例:
const app1 = createApp(App1);
app1.mount("#app1");
const app2 = createApp(App2);
app2.mount("#app2");
// 两个应用完全独立
模板语法
Vue3 的模板语法与 Vue2 基本兼容,但有一些改进和新特性。
基础语法(与 Vue2 相同)
<template>
<!-- 插值 -->
<span>{{ message }}</span>
<!-- 属性绑定 -->
<div :id="dynamicId" :class="{ active: isActive }"></div>
<!-- 事件绑定 -->
<button @click="handleClick">点击</button>
<!-- 条件渲染 -->
<div v-if="seen">现在你看到我了</div>
<div v-else>否则看到这个</div>
<!-- 列表渲染 -->
<li v-for="item in items" :key="item.id">{{ item.text }}</li>
<!-- 双向绑定 -->
<input v-model="text" />
</template>
Vue3 新特性
1. 多根节点(Fragment)
<!-- Vue2:必须有单根节点 -->
<template>
<div>
<header>头部</header>
<main>内容</main>
<footer>底部</footer>
</div>
</template>
<!-- Vue3:支持多根节点 -->
<template>
<header>头部</header>
<main>内容</main>
<footer>底部</footer>
</template>
2. Teleport 传送门
<template>
<button @click="showModal = true">打开弹窗</button>
<!-- 将内容传送到 body 下 -->
<Teleport to="body">
<div v-if="showModal" class="modal">
<p>弹窗内容</p>
<button @click="showModal = false">关闭</button>
</div>
</Teleport>
</template>
3. Suspense(实验性)
<template>
<Suspense>
<!-- 异步组件加载完成后显示 -->
<template #default>
<AsyncComponent />
</template>
<!-- 加载中显示 -->
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
Options API(选项式 API)
如果你熟悉 Vue2,可以继续使用 Options API,Vue3 完全兼容:
<template>
<div>
<h1>{{ title }}</h1>
<p>计数:{{ count }}</p>
<p>双倍:{{ doubleCount }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script>
export default {
name: "Counter",
props: {
title: {
type: String,
default: "计数器",
},
},
data() {
return {
count: 0,
};
},
computed: {
doubleCount() {
return this.count * 2;
},
},
watch: {
count(newVal, oldVal) {
console.log(`count: ${oldVal} → ${newVal}`);
},
},
methods: {
increment() {
this.count++;
},
},
// 生命周期钩子
beforeCreate() {
console.log("beforeCreate");
},
created() {
console.log("created");
},
beforeMount() {
console.log("beforeMount");
},
mounted() {
console.log("mounted");
},
beforeUpdate() {
console.log("beforeUpdate");
},
updated() {
console.log("updated");
},
beforeUnmount() {
// Vue2 是 beforeDestroy
console.log("beforeUnmount");
},
unmounted() {
// Vue2 是 destroyed
console.log("unmounted");
},
};
</script>
生命周期变化
| Vue2 | Vue3 |
|---|---|
| beforeCreate | beforeCreate |
| created | created |
| beforeMount | beforeMount |
| mounted | mounted |
| beforeUpdate | beforeUpdate |
| updated | updated |
| beforeDestroy | beforeUnmount |
| destroyed | unmounted |
Composition API(组合式 API)
Composition API 是 Vue3 最重要的新特性,它提供了一种全新的组织组件逻辑的方式。
setup 函数
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script>
import { ref, reactive, computed, watch, onMounted } from "vue";
export default {
props: {
initialCount: {
type: Number,
default: 0,
},
},
setup(props, context) {
// props 是响应式的
console.log(props.initialCount);
// context 包含 attrs, slots, emit, expose
const { attrs, slots, emit, expose } = context;
// 响应式数据
const count = ref(props.initialCount);
const user = reactive({
name: "张三",
age: 25,
});
// 计算属性
const doubleCount = computed(() => count.value * 2);
// 方法
function increment() {
count.value++;
}
// 侦听器
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`);
});
// 生命周期
onMounted(() => {
console.log("组件已挂载");
});
// 返回给模板使用
return {
count,
user,
doubleCount,
increment,
};
},
};
</script>
<script setup> 语法糖
Vue 3.2+ 引入的语法糖,更简洁:
<template>
<div>
<p>{{ count }}</p>
<p>{{ doubleCount }}</p>
<button @click="increment">+1</button>
<ChildComponent :data="user" @update="handleUpdate" />
</div>
</template>
<script setup>
import { ref, reactive, computed, watch, onMounted } from "vue";
import ChildComponent from "./ChildComponent.vue";
// props
const props = defineProps({
initialCount: {
type: Number,
default: 0,
},
});
// emits
const emit = defineEmits(["change", "update"]);
// 响应式数据
const count = ref(props.initialCount);
const user = reactive({
name: "张三",
age: 25,
});
// 计算属性
const doubleCount = computed(() => count.value * 2);
// 方法
function increment() {
count.value++;
emit("change", count.value);
}
function handleUpdate(data) {
console.log(data);
}
// 侦听器
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`);
});
// 生命周期
onMounted(() => {
console.log("组件已挂载");
});
// 暴露给父组件(通过 ref 访问)
defineExpose({
count,
increment,
});
</script>
ref vs reactive
import { ref, reactive, toRefs } from "vue";
// ref:用于基本类型,也可用于对象
const count = ref(0);
const user = ref({ name: "张三" });
// 访问/修改需要 .value
console.log(count.value); // 0
count.value++;
// 模板中自动解包,不需要 .value
// <span>{{ count }}</span>
// reactive:用于对象/数组
const state = reactive({
count: 0,
user: { name: "张三" },
});
// 直接访问,不需要 .value
console.log(state.count); // 0
state.count++;
// 注意:解构会丢失响应式
const { count } = state; // count 不是响应式的!
// 使用 toRefs 保持响应式
const { count, user } = toRefs(state);
// 现在 count.value 是响应式的
选择建议:
- 基本类型:用
ref - 对象/数组:
ref或reactive都可以 - 需要整体替换:用
ref - 表单数据、状态对象:用
reactive
响应式工具函数
import {
ref,
reactive,
readonly, // 只读
isRef, // 是否是 ref
isReactive, // 是否是 reactive
isReadonly, // 是否是只读
isProxy, // 是否是代理
toRef, // 将对象的某个属性转为 ref
toRefs, // 将对象所有属性转为 refs
unref, // 获取 ref 的值
shallowRef, // 浅层 ref
shallowReactive, // 浅层 reactive
triggerRef, // 手动触发 ref 更新
customRef, // 自定义 ref
} from "vue";
// readonly:创建只读代理
const original = reactive({ count: 0 });
const copy = readonly(original);
copy.count++; // 警告!无法修改
// toRef:创建指向源对象属性的 ref
const state = reactive({ foo: 1, bar: 2 });
const fooRef = toRef(state, "foo");
fooRef.value++; // 会同步修改 state.foo
// toRefs:批量转换
const stateRefs = toRefs(state);
// { foo: Ref<1>, bar: Ref<2> }
// unref:如果是 ref 返回 value,否则返回原值
const val = unref(maybeRef); // 等同于 isRef(val) ? val.value : val
// shallowRef:只有 .value 是响应式的
const shallow = shallowRef({ nested: { count: 0 } });
shallow.value.nested.count++; // 不会触发更新
shallow.value = { nested: { count: 1 } }; // 会触发更新
计算属性与侦听器
computed
import { ref, computed } from "vue";
const count = ref(0);
// 只读计算属性
const doubleCount = computed(() => count.value * 2);
// 可写计算属性
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1;
},
});
plusOne.value = 10; // count.value 变成 9
watch
import { ref, reactive, watch, watchEffect } from "vue";
const count = ref(0);
const state = reactive({ name: "张三", age: 25 });
// 监听 ref
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`);
});
// 监听 reactive 的属性(需要使用 getter)
watch(
() => state.name,
(newVal, oldVal) => {
console.log(`name: ${oldVal} → ${newVal}`);
}
);
// 监听多个来源
watch([count, () => state.name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`count: ${oldCount} → ${newCount}`);
console.log(`name: ${oldName} → ${newName}`);
});
// 深度监听
watch(
() => state,
(newState) => {
console.log("state changed");
},
{ deep: true }
);
// 立即执行
watch(
count,
(val) => {
console.log(val);
},
{ immediate: true }
);
// 只执行一次
watch(
count,
(val) => {
console.log(val);
},
{ once: true }
);
watchEffect
自动追踪依赖,立即执行:
import { ref, watchEffect } from "vue";
const count = ref(0);
const name = ref("张三");
// 自动追踪所有用到的响应式数据
const stop = watchEffect(() => {
console.log(`count: ${count.value}, name: ${name.value}`);
});
// 立即执行一次,之后 count 或 name 变化都会触发
// 停止监听
stop();
// 清除副作用
watchEffect((onCleanup) => {
const timer = setInterval(() => {
console.log("tick");
}, 1000);
onCleanup(() => {
clearInterval(timer);
});
});
watchEffect vs watch
| 特性 | watch | watchEffect |
|---|---|---|
| 依赖追踪 | 显式指定 | 自动追踪 |
| 立即执行 | 需要配置 immediate | 默认立即执行 |
| 访问旧值 | 可以 | 不可以 |
| 适用场景 | 精确控制 | 副作用收集 |
生命周期钩子
Composition API 中的生命周期
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onActivated, // keep-alive 激活
onDeactivated, // keep-alive 停用
onErrorCaptured, // 捕获后代错误
} from "vue";
export default {
setup() {
onBeforeMount(() => {
console.log("即将挂载");
});
onMounted(() => {
console.log("已挂载");
// 可以访问 DOM
});
onBeforeUpdate(() => {
console.log("即将更新");
});
onUpdated(() => {
console.log("已更新");
});
onBeforeUnmount(() => {
console.log("即将卸载");
// 清理工作
});
onUnmounted(() => {
console.log("已卸载");
});
onErrorCaptured((err, instance, info) => {
console.error("捕获错误:", err);
return false; // 阻止错误继续传播
});
},
};
生命周期对比表
| Options API | Composition API |
|---|---|
| beforeCreate | setup() |
| created | setup() |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
| activated | onActivated |
| deactivated | onDeactivated |
| errorCaptured | onErrorCaptured |
实战:Todo List(Vue3 版)
<template>
<div class="todo-app">
<h1>我的待办</h1>
<!-- 输入 -->
<div class="input-area">
<input
v-model.trim="newTodo"
@keyup.enter="addTodo"
placeholder="添加新任务..."
/>
<button @click="addTodo" :disabled="!newTodo">添加</button>
</div>
<!-- 过滤 -->
<div class="filters">
<button
v-for="f in filterOptions"
:key="f.value"
:class="{ active: filter === f.value }"
@click="filter = f.value"
>
{{ f.label }}
</button>
</div>
<!-- 列表 -->
<TransitionGroup name="list" tag="ul" class="todo-list">
<li
v-for="todo in filteredTodos"
:key="todo.id"
:class="{ done: todo.done }"
>
<input type="checkbox" v-model="todo.done" />
<span>{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">删除</button>
</li>
</TransitionGroup>
<!-- 统计 -->
<div class="stats" v-if="todos.length">
<span>{{ remaining }} 项未完成</span>
<button v-if="completed" @click="clearCompleted">清除已完成</button>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from "vue";
// 状态
const newTodo = ref("");
const filter = ref("all");
const todos = ref([]);
// 过滤选项
const filterOptions = [
{ label: "全部", value: "all" },
{ label: "未完成", value: "active" },
{ label: "已完成", value: "completed" },
];
// 计算属性
const filteredTodos = computed(() => {
switch (filter.value) {
case "active":
return todos.value.filter((t) => !t.done);
case "completed":
return todos.value.filter((t) => t.done);
default:
return todos.value;
}
});
const remaining = computed(() => {
return todos.value.filter((t) => !t.done).length;
});
const completed = computed(() => {
return todos.value.filter((t) => t.done).length;
});
// 方法
function addTodo() {
if (!newTodo.value) return;
todos.value.push({
id: Date.now(),
text: newTodo.value,
done: false,
});
newTodo.value = "";
}
function removeTodo(id) {
const index = todos.value.findIndex((t) => t.id === id);
if (index > -1) {
todos.value.splice(index, 1);
}
}
function clearCompleted() {
todos.value = todos.value.filter((t) => !t.done);
}
// 持久化
watch(
todos,
(newTodos) => {
localStorage.setItem("vue3-todos", JSON.stringify(newTodos));
},
{ deep: true }
);
// 初始化
const saved = localStorage.getItem("vue3-todos");
if (saved) {
todos.value = JSON.parse(saved);
}
</script>
<style scoped>
.todo-app {
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
.done span {
text-decoration: line-through;
color: #999;
}
.filters button.active {
background: #42b983;
color: white;
}
/* 列表动画 */
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(-30px);
}
</style>
总结
本文介绍了 Vue3 的基础知识,包括:
- Vue3 vs Vue2:了解主要改进和升级理由
- 环境搭建:Vite 是推荐的构建工具
- 应用创建:createApp 代替 new Vue
- 模板语法:新增 Fragment、Teleport、Suspense
- Options API:保持 Vue2 的使用习惯
- Composition API:setup 函数和
<script setup> - 响应式系统:ref、reactive 及工具函数
- 计算属性与侦听器:computed、watch、watchEffect
- 生命周期:on 前缀的钩子函数
Vue3 的学习曲线并不陡峭,特别是如果你已经熟悉 Vue2。下一篇文章,我们将深入 Composition API,探索如何更好地组织和复用代码。
延伸阅读
- Vue3 官方文档:https://vuejs.org/
- Vite 官方文档:https://vitejs.dev/
- Vue3 迁移指南