面试官提问环节

面试官:说说你对 Vue 响应式的理解?

Vue 的响应式系统是其核心特性之一,我主要从以下几个方面来理解:

Vue2 的响应式实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 核心是通过 Object.defineProperty 来劫持对象的属性
function defineReactive(obj, key, val) {
// 每个属性对应一个 dep 用来收集依赖
const dep = new Dep()

Object.defineProperty(obj, key, {
get() {
// 收集依赖
if (Dep.target) {
dep.depend()
}
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
// 通知更新
dep.notify()
}
})
}

// 实际项目中遇到的问题:
1. 对数组的变化监听需要特殊处理
2. 新增、删除属性需要使用 Vue.set/delete
3. 嵌套对象需要递归遍历

Vue3 的响应式优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 使用 Proxy 来代理整个对象
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 收集依赖
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
// 触发更新
trigger(target, key)
return result
}
})
}

// 优势:
1. 可以监听数组变化
2. 可以监听对象属性的添加和删除
3. 支持 MapSetWeakMapWeakSet
4. 性能更好,不需要递归遍历

面试官:项目中遇到过哪些性能问题?如何解决的?

在之前提到的虚拟表格项目中,我遇到了以下性能问题:

1. 大数据渲染问题

1
2
3
4
5
6
问题:10万条数据渲染,导致页面卡顿
解决方案:
1. 实现虚拟滚动,只渲染可视区域数据
2. 使用 transform 代替 top 定位
3. 使用 RAF 优化滚动事件
4. 实现缓冲区机制提升滚动体验

2. 频繁更新问题

1
2
3
4
5
6
问题:实时数据频繁更新导致性能问题
解决方案:
1. 使用 Web Worker 处理数据计算
2. 实现增量更新机制
3. 使用 Object.freeze 冻结不变数据
4. 优化更新频率,合并多次更新

3. 内存泄漏问题

1
2
3
4
5
6
问题:长时间运行后内存占用过高
解决方案:
1. 及时清理不可见区域的 DOM
2. 解绑事件监听器
3. 清理定时器和订阅
4. 使用 WeakMap/WeakSet 存储引用

面试官:你是如何设计一个组件的?

以虚拟表格组件为例,我的设计思路是:

1. 接口设计

1
2
3
4
5
6
7
8
9
1. 保持简单直观:
- 必要的 props:data、columns、height
- 可选的功能 props:selectable、sortable、expandable
- 统一的事件命名:onSort、onSelect、onExpand

2. 兼顾灵活性:
- 支持自定义列模板
- 支持自定义排序逻辑
- 支持自定义展开行内容

2. 性能考虑

1
2
3
4
5
6
7
8
9
1. 渲染性能:
- 虚拟滚动
- 函数式组件
- 合理的更新粒度

2. 内存优化:
- 缓存计算结果
- 及时清理资源
- 优化数据结构

3. 可维护性

1
2
3
4
5
6
7
8
9
1. 代码组织:
- 单一职责
- 逻辑分层
- 清晰的注释

2. 扩展性:
- 插件机制
- 钩子函数
- 预留扩展接口

这些都是我在实际项目中总结的经验,每个方案都经过了实践验证。

面试官:说说你对 Vue 生命周期的理解?

Vue 的生命周期是组件从创建到销毁的整个过程,我主要从以下几个方面来理解:

Vue2 生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1. 创建阶段:
beforeCreate: 实例创建前,data/methods 都不可用
created: 实例创建后,可访问 data/methods,但未挂载 DOM

2. 挂载阶段:
beforeMount: 模板编译完成,但未挂载到 DOM
mounted: DOM 挂载完成,可以访问 DOM 元素

3. 更新阶段:
beforeUpdate: 数据更新,但 DOM 未更新
updated: DOM 更新完成

4. 销毁阶段:
beforeDestroy: 实例销毁前,可以清理事件、定时器等
destroyed: 实例销毁后,所有指令解绑,子实例销毁

实际应用:
- created: 发起数据请求,初始化数据
- mounted: 操作 DOM,初始化第三方库
- beforeDestroy: 清理定时器、取消订阅

Vue3 生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { onMounted, onBeforeMount, onBeforeUnmount } from 'vue'

setup() {
// 创建阶段
onBeforeMount(() => {
// 组件挂载前
})

onMounted(() => {
// 组件挂载后
})

// 更新阶段
onBeforeUpdate(() => {
// 组件更新前
})

onUpdated(() => {
// 组件更新后
})

// 销毁阶段
onBeforeUnmount(() => {
// 组件卸载前
})

onUnmounted(() => {
// 组件卸载后
})
}

面试官:Vue2 和 Vue3 的区别?

主要从以下几个方面来说:

1. 响应式系统

1
2
3
4
5
6
7
8
9
10
// Vue2:Object.defineProperty
1. 需要递归遍历对象
2. 不能监听数组索引和长度变化
3. 不能监听对象属性的添加和删除

// Vue3:Proxy
1. 可以监听整个对象
2. 可以监听数组变化
3. 支持 MapSet 等数据结构
4. 性能更好,不需要递归

2. 组合式 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Vue2:Options API
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
},
computed: {
doubleCount() {
return this.count * 2
}
}
}

// Vue3:Composition API
import { ref, computed } from 'vue'

export default {
setup() {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)

function increment() {
count.value++
}

return {
count,
doubleCount,
increment
}
}
}

优势:
1. 更好的代码组织
2. 更好的逻辑复用
3. 更好的类型推导

3. 性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
1. 编译优化:
- 静态节点提升
- Patch Flag 标记
- 块级树结构

2. 按需编译:
- Tree-shaking 支持
- 更小的打包体积

3. 新特性:
- Fragment
- Teleport
- Suspense

面试官:Vue 中如何实现组件通信?

我通常使用以下几种方式:

1. Props/Emit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 父组件
<template>
<child-component
:message="message"
@update="handleUpdate"
/>
</template>

// 子组件
<template>
<div @click="handleClick">{{ message }}</div>
</template>

<script>
export default {
props: ['message'],
methods: {
handleClick() {
this.$emit('update', 'new value')
}
}
}
</script>

适用场景:父子组件通信

2. Provide/Inject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 父组件
export default {
provide() {
return {
theme: this.theme,
updateTheme: this.updateTheme
}
}
}

// 子组件
export default {
inject: ['theme', 'updateTheme']
}

适用场景:跨层级组件通信

3. Vuex/Pinia

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Vuex
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
}
})

// Pinia
const useStore = defineStore('main', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++
}
}
})

适用场景:全局状态管理

4. EventBus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Vue2
const bus = new Vue()
// 发送事件
bus.$emit('update', data)
// 监听事件
bus.$on('update', data => {})

// Vue3
const emitter = mitt()
// 发送事件
emitter.emit('update', data)
// 监听事件
emitter.on('update', data => {})

适用场景:非父子组件通信
注意事项:需要及时销毁监听器

这些都是我在实际项目中经常使用的通信方式,选择哪种方式主要取决于:

  1. 组件间的关系(父子、兄弟、跨层级)
  2. 通信的频率和复杂度
  3. 是否需要状态管理
  4. 是否需要调试工具支持