一、Vue 基础核心
1. 生命周期钩子
// 创建阶段
beforeCreate() // 实例初始化,数据观测前
created() // 数据观测完成,可访问 data,但未挂载 DOM
// 挂载阶段
beforeMount() // 模板编译完成,未渲染
mounted() // DOM 渲染完成,可操作 DOM
// 更新阶段
beforeUpdate() // 数据变化,DOM 更新前
updated() // DOM 更新完成
// 销毁阶段
beforeDestroy() // 实例销毁前,清理定时器、取消订阅
destroyed() // 实例销毁后
// 特殊(keep-alive)
activated() // 组件激活时
deactivated() // 组件失活时
// 面试题:mounted 和 created 区别?
// created: 能访问 data,不能操作 DOM
// mounted: DOM 已渲染,可操作 DOM、发请求
2. 组件通信方式
// 1. props / $emit (父子组件)
// 父组件
<Child :msg="message" @update="handleUpdate" />
// 子组件
props: ['msg'],
methods: {
sendToParent() {
this.$emit('update', '新数据')
}
}
// 2. $refs (父调子)
<Child ref="childComp" />
this.$refs.childComp.childMethod()
// 3. $parent / $children (不推荐)
// 4. provide / inject (祖先后代)
// 祖先
provide() {
return {
theme: 'dark',
user: this.user
}
}
// 后代
inject: ['theme', 'user']
// 5. eventBus (任意组件)
// bus.js
export const bus = new Vue()
// 发送
bus.$emit('event-name', data)
// 接收
bus.$on('event-name', data => {})
// 6. Vuex (全局状态管理)
// 7. $attrs / $listeners (跨级传递)
// 父传孙,中间组件用 v-bind="$attrs" v-on="$listeners"
// 8. slot (内容分发)
<!-- 子组件 Child.vue -->
<template>
<div>
<h3>子组件</h3>
<!-- 把 user 数据传给父组件 -->
<slot :user="userData"></slot>
</div>
</template>
<script>
export default {
data() {
return {
userData: { name: 'Alice', age: 25 }
}
}
}
</script>
<!-- 父组件 Parent.vue -->
<template>
<Child>
<!-- 接收子组件传来的数据,随便起名 slotProps -->
<template #default="slotProps">
<p>姓名:{{ slotProps.user.name }}</p>
<p>年龄:{{ slotProps.user.age }}</p>
</template>
</Child>
</template>
<script>
import Child from './Child.vue'
export default { components: { Child } }
</script>
3. 指令
<!-- 常用指令 -->
v-if <!-- 条件渲染(销毁/重建) -->
v-show <!-- 条件显示(display: none) -->
v-for <!-- 列表渲染,必须绑定 key -->
v-bind <!-- 动态绑定属性,简写 : -->
v-on <!-- 事件绑定,简写 @ -->
v-model <!-- 双向绑定 -->
v-html <!-- 插入 HTML(小心 XSS) -->
v-text <!-- 插入文本 -->
v-pre <!-- 跳过编译 -->
v-once <!-- 只渲染一次 -->
v-cloak <!-- 防止闪烁 -->
<!-- 面试题:v-if vs v-show -->
v-if: 条件不成立时不渲染,适合运行时很少改变
v-show: 始终渲染,切换 display,适合频繁切换
二、Vue 进阶核心
1. 响应式原理
// Vue 2 响应式 (Object.defineProperty)
function defineReactive(obj, key, val) {
// 依赖收集器(存有哪些地方用到了这个数据)
const dep = new Dep()
// 核心:拦截属性的读取和修改
Object.defineProperty(obj, key, {
get() {
// 读取时:记录谁在用这个数据
if (Dep.target) dep.add(Dep.target)
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
// 修改时:通知所有用到的地方更新
dep.notify()
}
}
})
}
// 缺点:
// - 无法检测对象属性的添加/删除 (需用 Vue.set/Vue.delete)
// - 无法直接监听数组变化 (需重写数组方法)
// Vue 3 响应式 (Proxy)
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
// 读取时收集依赖
track(target, key)
return target[key]
},
set(target, key, value) {
if (target[key] !== value) {
target[key] = value
// 修改时触发更新
trigger(target, key)
}
return true
},
deleteProperty(target, key) {
// 删除属性也能监听
delete target[key]
trigger(target, key)
return true
}
})
}
// 优点:
// - 可检测属性添加/删除
// - 可检测数组索引/length 变化
// - 性能更好
Vue 响应式核心是数据劫持 + 依赖收集 + 发布订阅
1.数据劫持
- Vue 2:用 `Object.defineProperty` 递归遍历对象属性,拦截 get/set
- Vue 3:用 `Proxy` 代理整个对象,拦截所有操作
2.依赖收集
- 模板编译时,会读取数据 → 触发 get
- 每个数据有一个 dep,记录哪些地方用到了它(watcher/effect)
3.发布订阅
- 数据修改时 → 触发 set/proxy
- 通知 dep 中记录的所有依赖 → 执行更新
2. 计算属性 vs 方法 vs 侦听器
// 计算属性 (computed)
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}
// 特点:有缓存,依赖不变不重新计算
// 方法 (methods)
methods: {
getFullName() {
return this.firstName + ' ' + this.lastName
}
}
// 特点:无缓存,每次调用都执行
// 侦听器 (watch)
watch: {
firstName(newVal, oldVal) {
this.fullName = newVal + ' ' + this.lastName
}
}
// 特点:适合异步/开销大的操作
// 面试题:computed 和 watch 区别?
// computed: 依赖多个数据,同步,有缓存
// watch: 监听单个数据变化,异步/复杂逻辑
3. nextTick 原理
// 为什么需要 nextTick?
// Vue 是异步更新 DOM 的,数据变化后 DOM 不会立即更新
methods: {
async updateData() {
this.message = '新消息'
console.log(this.$el.textContent) // 旧值 ❌
this.$nextTick(() => {
console.log(this.$el.textContent) // 新值 ✅
})
// 或 await 版本
await this.$nextTick()
console.log(this.$el.textContent) // 新值 ✅
}
}
// 原理:将回调放入微任务队列
// 优先 Promise.then > MutationObserver > setImmediate > setTimeout
三、Vue Router
1. 路由模式
// hash 模式
https://example.com/#/user/1
// 特点:兼容性好,无需服务器配置
// history 模式
https://example.com/user/1
// 特点:URL 美观,需服务器配置回退
// 面试题:两种模式区别?
// hash: 使用 URL hash 值,onhashchange 监听
// history: 使用 HTML5 History API,pushState/replaceState
2. 路由守卫
// 全局守卫
<!-- router/index.js -->
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isLogin) {
next('/login') // 跳转登录
} else {
next() // 放行
}
})
router.afterEach((to, from) => {
// 日志记录等
})
// 路由独享守卫
<!-- router/index.js -->
{
path: '/admin',
component: Admin,
beforeEnter: (to, from, next) => {}
}
// 组件内守卫
<!-- routeDemo.vue -->
beforeRouteEnter(to, from, next) {
// 不能访问 this
next(vm => { /* 可访问 vm */ })
},
beforeRouteUpdate(to, from, next) {},
beforeRouteLeave(to, from, next) {}
3. 路由传参
// 1. params (需配合 name)
// URL 示例
/user/123
// 路由配置
{ path: '/user/:id', name: 'user', component: User }
// 跳转
this.$router.push({ name: 'user', params: { id: 123 } })
// 获取
this.$route.params.id
// 2. query (URL 参数)
// URL 示例
/user?id=123
// 跳转
this.$router.push({ path: '/user', query: { id: 123 })
// 获取
this.$route.query.id
// 3. props 解耦
{ path: '/user/:id', component: User, props: true }
// 组件内用 props 接收
props: ['id']
四、Vuex
1. 核心概念
// store.js
export default new Vuex.Store({
// 状态
state: {
user: null,
count: 0
},
// 获取器(类似 computed)
getters: {
isLogin: state => !!state.user,
doubleCount: state => state.count * 2
},
// 同步修改(唯一修改 state 的方式)
mutations: {
SET_USER(state, user) {
state.user = user
},
INCREMENT(state) {
state.count++
}
},
// 异步操作
actions: {
async login({ commit }, userData) {
const user = await api.login(userData)
commit('SET_USER', user)
}
},
// 模块化
modules: {
cart: cartModule,
product: productModule
}
})
// 组件中使用
computed: {
user() {
return this.$store.state.user
},
...mapGetters(['isLogin', 'doubleCount'])
},
methods: {
...mapMutations(['INCREMENT']),
...mapActions(['login'])
}
2. 数据流
// 单向数据流
View (组件)
↓ dispatch
Action (异步)
↓ commit
Mutation (同步)
↓ mutate
State (状态)
↓ render
View (组件)
// 面试题:为什么 mutation 必须同步?
// 为了 devtools 能够追踪状态变化
五、Vue 3 新特性
1. Composition API
// Vue 2 (Options API)
export default {
data() {
return { count: 0 }
},
methods: {
increment() { this.count++ }
},
computed: {
double() { return this.count * 2 }
}
}
// Vue 3 (Composition API)
import { ref, computed, watch } from 'vue'
export default {
setup() {
// 响应式数据
const count = ref(0)
// 方法
const increment = () => count.value++
// 计算属性
const double = computed(() => count.value * 2)
// 侦听器
watch(count, (newVal, oldVal) => {
console.log('count变化:', newVal)
})
// 生命周期
onMounted(() => {
console.log('组件挂载')
})
return { count, increment, double }
}
}
2. 响应式 API
import { ref, reactive, toRefs } from 'vue'
// ref:处理基本类型
const count = ref(0)
count.value++ // 通过 .value 访问
// reactive:处理对象
const state = reactive({
name: 'Alice',
age: 25
})
state.name = 'Bob' // 直接访问
// toRefs:解构时保持响应式
const { name, age } = toRefs(state)
// 现在 name.value, age.value 是响应式的
3. 新特性
// 1. 多个根节点
<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template>
// 2. Teleport(传送门)
<teleport to="body">
<div class="modal">弹窗</div>
</teleport>
// 3. Suspense(异步组件)
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
六、常见面试题
1. v-for 为什么需要 key?
<!-- 错误 -->
<div v-for="item in list">{{ item }}</div>
<!-- 正确 -->
<div v-for="item in list" :key="item.id">{{ item }}</div>
原因:Vue 的 diff 算法通过 key 来识别节点,提高更新性能,避免渲染错误。
2. 组件中 data 为什么是函数?
// 错误
data: {
count: 0
}
// 正确
data() {
return {
count: 0
}
}
原因:保证每个组件实例有独立的数据,避免多个实例共享 data。
3. Vue 2 中改变数组无法触发视图更新?
// 不会触发更新
this.list[0] = '新值' // 索引赋值
this.list.length = 0 // 修改长度
// 会触发更新的方法
this.list.push('新值')
this.list.pop()
this.list.shift()
this.list.unshift()
this.list.splice()
this.list.sort()
this.list.reverse()
// 或使用 Vue.set
Vue.set(this.list, 0, '新值')
this.$set(this.list, 0, '新值')
4. 虚拟 DOM 是什么?
// 虚拟 DOM 是一个 JS 对象,描述真实 DOM
const vnode = {
tag: 'div',
props: { id: 'app', class: 'container' },
children: [
{ tag: 'h1', props: {}, children: ['标题'] }
]
}
// 优点:
// - 跨平台(渲染到 DOM、Native、小程序)
// - 批量更新,减少 DOM 操作
// - 抽象了渲染过程
5. keep-alive 的作用
<keep-alive>
<component :is="currentTab">
<!-- 被切换的组件会被缓存,不销毁 -->
</component>
</keep-alive>
作用:缓存组件状态,避免重复渲染。组件会多两个生命周期:activated、deactivated。