Vue 3 基础入门
1. Vue 3 简介
1.1 为什么选择 Vue 3
Vue 3 核心改进:
├── 性能提升 │ Proxy 响应式,Virtual DOM 重写,编译时优化
├── 更好的 TS 支持 │ 全部用 TS 重写,类型推导更准确
├── 组合式 API │ 逻辑复用更好,代码组织更灵活
├── 更小的体积 │ Tree-shaking 优化,核心 10KB 左右
├── 更强的扩展性 │ 自定义渲染器,依赖注入改进
└── 新增特性 │ Teleport、Suspense、Fragments1.2 搭建项目
bash
# 使用 npm
npm create vue@latest my-vue-app
# 使用 vite (推荐,更快)
npm create vite@latest my-vue-app -- --template vue
# 进入项目
cd my-vue-app
npm install
npm run dev1.3 项目结构
my-vue-app/
├── public/ # 静态资源
├── src/
│ ├── assets/ # 资源文件
│ ├── components/ # 组件
│ ├── views/ # 页面
│ ├── router/ # 路由
│ ├── stores/ # Pinia 状态管理
│ ├── composables/ # 组合式函数
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── index.html
├── vite.config.js
└── package.json1.4 入口文件
javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 安装插件
app.use(router)
app.use(pinia)
app.mount('#app')
// 全局配置
app.config.errorHandler = (err, instance, info) => {
console.error('全局错误:', err, info)
}2. 模板语法
2.1 插值
vue
<template>
<!-- 文本插值 -->
<span>{{ message }}</span>
<!-- 原始 HTML (慎用,XSS 风险) -->
<div v-html="rawHtml"></div>
<!-- 属性绑定 -->
<div :class="{ active: isActive }"></div>
<!-- JavaScript 表达式 -->
<span>{{ count + 1 }}</span>
<span>{{ ok ? 'YES' : 'NO' }}</span>
</template>
<script setup>
import { ref } from 'vue'
const message = ref('Hello Vue 3')
const rawHtml = ref('<strong>粗体文字</strong>')
const isActive = ref(true)
const count = ref(0)
const ok = ref(true)
</script>2.2 指令
vue
<template>
<!-- v-bind 绑定属性 -->
<img :src="imageUrl" :alt="title" />
<button :disabled="isDisabled">点击</button>
<!-- v-on 绑定事件 -->
<button @click="handleClick">点击</button>
<form @submit.prevent="handleSubmit">表单</form>
<div @click.stop="handleClick">阻止冒泡</div>
<!-- v-model 双向绑定 -->
<input v-model="text" />
<input v-model.number="age" type="number" />
<!-- v-if / v-show -->
<div v-if="show">条件渲染</div>
<div v-show="show">display 切换</div>
<!-- v-for 列表渲染 -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<!-- v-once 只渲染一次 -->
<span v-once>{{ message }}</span>
<!-- v-cloak 编译完成前隐藏 -->
<div v-cloak>{{ message }}</div>
</template>2.3 事件修饰符
vue
<template>
<!-- 阻止默认行为 -->
<form @submit.prevent="onSubmit">...</form>
<!-- 阻止冒泡 -->
<div @click.stop="onClick">...</div>
<!-- 只触发一次 -->
<button @click.once="doOnce">只执行一次</button>
<!-- 捕获模式 -->
<div @click.capture="handleCapture">捕获阶段触发</div>
<!-- 键盘事件 -->
<input @keyup.enter="submit" />
<input @keyup.ctrl.enter="submit" />
<!-- 鼠标事件修饰符 -->
<button @click.left="leftClick">左键</button>
<button @click.right="rightClick">右键</button>
<button @click.middle="middleClick">中键</button>
</template>3. 响应式基础
3.1 ref 和 reactive
javascript
import { ref, reactive, toRefs, toRef } from 'vue'
// ref: 基础类型响应式
const count = ref(0)
console.log(count.value) // 0
count.value++
// ref: 对象类型(自动深层响应式)
const obj = ref({ name: '张三', age: 25 })
obj.value.name = '李四'
// reactive: 对象/数组响应式(深层)
const state = reactive({
count: 0,
user: { name: '张三' },
items: [1, 2, 3]
})
state.count++
state.user.name = '李四'
state.items.push(4)
// toRefs: 解构响应式对象
const { count, name } = toRefs(state)
// 保持响应式连接
count.value = 100 // state.count 也会变
// toRef: 单个属性保持响应式
const age = toRef(state, 'age')3.2 computed 计算属性
javascript
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 只读计算属性
const fullName = computed(() => {
return firstName.value + lastName.value
})
// 可写计算属性
const fullNameWritable = computed({
get() {
return firstName.value + lastName.value
},
set(value) {
firstName.value = value.slice(0, 1)
lastName.value = value.slice(1)
}
})
// 访问
console.log(fullName.value) // '张三'
fullNameWritable.value = '李四'
console.log(firstName.value) // '李'3.3 watch 监听
javascript
import { ref, reactive, watch, watchEffect } from 'vue'
const count = ref(0)
const user = reactive({ name: '张三', age: 25 })
// 监听单个 ref
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变成 ${newVal}`)
})
// 监听多个
watch([count, () => user.name], ([newCount, newName], [oldCount, oldName]) => {
console.log('count 或 name 变化了')
})
// 监听 reactive 对象(需要用函数)
watch(() => user.age, (newAge, oldAge) => {
console.log(`年龄从 ${oldAge} 变成 ${newAge}`)
}, { immediate: true, deep: true })
// watchEffect: 副作用自动追踪
watchEffect(() => {
// 自动追踪 user.name 和 user.age
console.log(`${user.name} 的年龄是 ${user.age}`)
})
// 停止监听
const stop = watch(count, () => { ... })
stop() // 停止监听4. 组合式 API
4.1 setup 基础
vue
<script setup>
// setup 是编译器处理的语法糖
// 所有导入的组件、变量都会自动暴露给模板
import { ref, computed, onMounted } from 'vue'
import MyComponent from './MyComponent.vue'
const message = ref('Hello')
const count = ref(0)
const doubled = computed(() => count.value * 2)
// 方法
function increment() {
count.value++
}
// 生命周期钩子
onMounted(() => {
console.log('组件挂载完成')
})
</script>
<template>
<h1>{{ message }}</h1>
<button @click="increment">+1</button>
<p>count × 2 = {{ doubled }}</p>
<MyComponent />
</template>4.2 生命周期钩子
javascript
import {
onMounted,
onUpdated,
onUnmounted,
onBeforeMount,
onBeforeUpdate,
onBeforeUnmount,
onActivated,
onDeactivated,
onErrorCaptured
} from 'vue'
export default {
setup() {
// 创建阶段
onBeforeMount(() => {
console.log('即将挂载')
})
onMounted(() => {
console.log('已挂载')
})
// 更新阶段
onBeforeUpdate(() => {
console.log('即将更新')
})
onUpdated(() => {
console.log('已更新')
})
// 卸载阶段
onBeforeUnmount(() => {
console.log('即将卸载')
})
onUnmounted(() => {
console.log('已卸载')
})
// Keep-Alive 相关
onActivated(() => {
console.log('激活')
})
onDeactivated(() => {
console.log('停用')
})
// 错误捕获
onErrorCaptured((err, instance, info) => {
console.error('捕获到错误:', err, info)
})
}
}4.3 依赖注入
javascript
// 父组件 provide
import { provide, ref } from 'vue'
export default {
setup() {
const theme = ref('dark')
const updateTheme = (newTheme) => {
theme.value = newTheme
}
// 提供给所有后代
provide('theme', theme)
provide('updateTheme', updateTheme)
// 或者提供静态值
provide('appName', 'My Vue App')
}
}
// 子组件 inject
import { inject } from 'vue'
export default {
setup() {
// 注入
const theme = inject('theme')
const updateTheme = inject('updateTheme')
const appName = inject('appName', 'Default App') // 默认值
// 注入是响应式的
console.log(theme.value) // 'dark'
return { theme, updateTheme, appName }
}
}4.4 组合式函数 (Composables)
javascript
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = initialValue
}
return {
count,
double,
increment,
decrement,
reset
}
}
// 使用
import { useCounter } from '@/composables/useCounter'
export default {
setup() {
const { count, increment, decrement } = useCounter(10)
return { count, increment, decrement }
}
}4.5 常用工具函数
javascript
import {
// 判断类型
isRef, // 是否是 ref
isReactive, // 是否是 reactive
isProxy, // 是否是 proxy
isReadonly, // 是否是 readonly
// 转换
toRefs, // reactive 转 ref
toRef, // 单个属性转 ref
toValue, // ref/reactive 转原始值
// 高级
shallowRef, // 浅层响应式(.value 变化才触发)
shallowReactive, // 浅层 reactive
readonly, // 只读响应式
triggerRef, // 手动触发 shallowRef 更新
// nextTick
nextTick // 等待 DOM 更新
} from 'vue'
// 示例
import { shallowRef, triggerRef, nextTick } from 'vue'
const state = shallowRef({ count: 0 })
// 改变嵌套属性不会触发更新
state.value.count = 100 // 不会更新视图!
// 需要手动触发
state.value.count = 100
triggerRef(state)
// nextTick 等待更新
async function updateAndWait() {
count.value++
await nextTick()
console.log('DOM 已更新')
}5. 组件基础
5.1 组件通信
vue
<!-- Parent.vue -->
<template>
<!-- 父传子: props -->
<ChildComponent
:title="title"
:items="items"
@update="handleUpdate"
/>
<!-- 父传子: provide/inject -->
<MiddleComponent />
</template>
<script setup>
import { ref, provide } from 'vue'
import ChildComponent from './ChildComponent.vue'
const title = ref('标题')
const items = ref(['a', 'b', 'c'])
provide('sharedData', {
message: '共享数据'
})
function handleUpdate(newValue) {
console.log('子组件更新:', newValue)
}
</script>vue
<!-- Child.vue -->
<template>
<div>
<h2>{{ title }}</h2>
<ul>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
<!-- 子传父: emit -->
<button @click="notifyParent">通知父组件</button>
<!-- 访问父组件 -->
<button @click="handleClick">直接调用父组件方法</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
// 定义 props
const props = defineProps({
title: {
type: String,
required: true,
default: '默认标题'
},
items: {
type: Array,
default: () => []
}
})
// 定义 emit
const emit = defineEmits(['update', 'click'])
function notifyParent() {
emit('update', 'new value')
}
// 访问父组件方法
function handleClick() {
// 模板中用 $emit,脚本中用 context.emit
}
// 注入
const shared = inject('sharedData')
console.log(shared.message)
</script>5.2 v-model 在组件上
vue
<!-- Parent.vue -->
<template>
<!-- 默认: modelValue + update:modelValue -->
<CustomInput v-model="text" />
<!-- 多个 v-model -->
<CustomInput
v-model="text"
v-model:title="title"
/>
</template>
<script setup>
import CustomInput from './CustomInput.vue'
const text = ref('')
const title = ref('')
</script>vue
<!-- CustomInput.vue -->
<template>
<!-- 方式1: 普通 input -->
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
<!-- 方式2: 简写 -->
<input v-model="modelValue" />
<!-- 命名 v-model -->
<input :value="title" @input="$emit('update:title', $event.target.value)" />
</template>
<script setup>
defineProps(['modelValue', 'title'])
defineEmits(['update:modelValue', 'update:title'])
</script>5.3 插槽
vue
<!-- Parent.vue -->
<template>
<Layout>
<!-- 默认插槽 -->
<template #default>
<p>主要内容</p>
</template>
<!-- 命名插槽 -->
<template #header>
<h1>页面标题</h1>
</template>
<!-- 作用域插槽 -->
<template #footer="{ data }">
<p>页脚 - {{ data }}</p>
</template>
</Layout>
</template>vue
<!-- Layout.vue -->
<template>
<div class="layout">
<header>
<slot name="header">默认标题</slot>
</header>
<main>
<slot>默认内容</slot>
</main>
<footer>
<slot name="footer" :data="footerData">默认页脚</slot>
</footer>
</div>
</template>
<script setup>
const footerData = ref('版权信息')
</script>6. 常用特性
6.1 Teleport
vue
<!-- 将内容传送到任意 DOM 位置 -->
<template>
<button @click="showModal = true">打开弹窗</button>
<Teleport to="body">
<div v-if="showModal" class="modal">
<div class="modal-content">
<h2>弹窗内容</h2>
<button @click="showModal = false">关闭</button>
</div>
</div>
</Teleport>
<!-- 传送到指定 ID -->
<Teleport to="#modal-container">
<div>另一个弹窗</div>
</Teleport>
<!-- 禁用 (调试用) -->
<Teleport to="body" :disabled="true">
<div>临时禁用</div>
</Teleport>
</template>
<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>6.2 Suspense
vue
<!-- 异步组件加载状态 -->
<template>
<Suspense>
<!-- 主要内容: 异步组件 -->
<template #default>
<AsyncUserProfile />
</template>
<!-- 加载中的显示 -->
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>
<script setup>
// 异步组件
const UserProfile = defineAsyncComponent(() =>
import('./UserProfile.vue')
)
// 或者在 setup 中直接 await
import { ref, defineAsyncComponent, onMounted } from 'vue'
const UserProfile = defineAsyncComponent({
loader: () => import('./UserProfile.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000
})
</script>6.3 片段 (Fragments)
vue
<!-- Vue 3 组件可以有多个根元素 -->
<template>
<div class="header">头部</div>
<div class="main">主体</div>
<div class="footer">底部</div>
</template>
<!-- 带有特性的片段 -->
<template>
<!-- attributes 自动应用到内部第一个元素 -->
<h1>多根组件</h1>
<article>文章内容</article>
<!-- 等同于 -->
<div v--bind="$attrs">attributes 自动应用到这里</div>
</template>
<script setup>
// 禁用 inheritAttrs
defineOptions({
inheritAttrs: false
})
</script>7. 面试要点
7.1 Vue 2 vs Vue 3 响应式对比
Vue 2: Object.defineProperty
- 无法监听新增/删除属性 (需用 Vue.set/delete)
- 无法监听数组索引变化 (需用 $set)
- 需要遍历所有属性,性能差
Vue 3: Proxy
- 深层响应式,自动追踪
- 新增/删除属性自动响应
- 数组索引变化自动响应
- 性能更好7.2 ref vs reactive 怎么选
ref:
- 适合基本类型
- 适合解构赋值后保持响应式
- 模板中使用自动解包 (.value 可省略)
- 返回可变对象 { value: xxx }
reactive:
- 适合对象/数组
- 不适合解构(会丢失响应式)
- 返回 Proxy,性能略好
最佳实践:
- 基本类型用 ref
- 对象/数组用 reactive
- 解构用 toRefs + ref
- 保持清晰,不要混用7.3 computed vs watch 区别
computed:
- 用于派生计算
- 自动追踪依赖
- 结果会被缓存
- 同步返回
- 适合派生状态
watch:
- 用于副作用
- 手动指定监听目标
- 不缓存
- 可以是异步
- 适合执行副作用(请求、DOM 操作)7.4 组合式 API 优势
1. 更好的逻辑复用
- mixins 的缺陷: 属性来源不清、命名冲突
- composables: 显式导入、清晰的 API
2. 更好的代码组织
- 相关逻辑放一起(而非按 options 分散)
- 更适合大型项目
3. 更好的 TypeScript 支持
- 类型推导更准确
- IDE 支持更好