0%

一、mitt 是什么

mitt 是一个只有 200 字节的轻量级事件发射器(Event Emitter),由 Vue 作者尤雨溪编写。它提供了 onoffemit 三个核心方法,用于实现模块间的解耦通信。

阅读全文 »

一、小程序独有的性能模型

小程序的性能优化和 Web 完全不同,根本原因在于它的双线程架构

1
2
3
4
5
6
7
8
┌──────────────┐          setData          ┌──────────────┐
│ 渲染层 │ ◄─────── JSON ──────────► │ 逻辑层 │
│ WebView │ 序列化 │ JSCore/V8 │
│ (WXML+WXSS) │ │ (JS/TS) │
└──────────────┘ └──────────────┘
↑ ↑
DOM 操作 计算 + 网络请求
渲染更新 数据组装

Web 中 JS 和 DOM 在同一个线程,直接修改 DOM 即可。小程序中逻辑层和渲染层完全隔离,每次数据更新需要跨线程通信——JSON 序列化 + 传输 + 解析。这个通信成本是小程序性能问题的根源。

阅读全文 »

一、小程序是什么

小程序是一种无需下载安装即可使用的轻量级应用,运行在宿主 App 提供的运行时环境中。用户通过扫码、搜索、分享等方式即可打开,用完即走。

阅读全文 »

一、什么是微信小程序

微信小程序是一种运行在微信内的轻量级应用,无需下载安装,即点即用。它使用微信提供的原生开发方式,通过 WXML、WXSS、JavaScript/TypeScript 和 JSON 四种文件完成开发。

阅读全文 »

【面试速答版】

Q1: “Vue3 的模板编译相比 Vue2 做了哪些优化?”

Vue3 的编译器在编译阶段做了四个关键优化:① PatchFlag(动态标记)——编译时分析模板,在 VNode 上标记动态内容的类型(如动态 class、动态 text、动态 props),运行时 diff 只比较带标记的节点,跳过静态内容;② 静态提升——纯静态节点(没有绑定响应式数据的节点)被提升到 render 函数外部创建,每次渲染复用同一个 VNode,不需要重新创建;③ 事件缓存——模板中内联的事件处理函数在首次渲染时缓存,后续渲染复用,避免因事件引用变化导致子组件更新;④ Tree-shaking——只有用到的运行时 API 才会被打包(如 v-model 只在用到时才打包),减少了最终包体积。

Q2: “什么是 PatchFlag?它是如何提升性能的?”

PatchFlag 是编译器在 VNode 上打的二进制位掩码,标明这个节点哪些部分可能是动态的。例如 <div :class="cls">{{ msg }}</div> 编译后 PatchFlag = CLASS | TEXT = 1 | 2 = 3。运行时 diff 时检查 PatchFlag:如果带 TEXT 标记就只比较文本内容,如果带 CLASS 标记就只比较 class 属性,不带标记的节点直接跳过整个 diff 过程。这避免了 Vue2 中每个 VNode 都要全量比较所有属性的开销。

Q3: “Teleport、Fragments、Suspense 分别是什么?解决了什么问题?”

Fragments 支持模板有多个根节点,Vue2 中必须包裹一个 <div>,Vue3 不需要。Teleport 把组件的内容渲染到指定的 DOM 位置(如 <Teleport to="body">),常用于弹窗、通知栏——解决 CSS 层叠上下文和 overflow:hidden 裁剪问题。Suspense 管理异步依赖(顶层 await、异步组件)的加载状态,自动显示 fallback 内容直到异步操作完成。

【深入理解版】

1. 这个知识点要解决什么问题?

Vue 的模板(template)是一个声明式的描述——你写 <div>{{ count }}</div>,但浏览器只理解 DOM API(document.createElementtextContent 等)。模板编译器就在中间做翻译工作。但 Vue2 的编译器比较”老实”——编译后的 render 函数在执行时,所有 VNode 都是”盲比”的,运行时的 diff 不知道哪些节点是静态的、哪些是动态的,只能逐个属性比较。

Vue3 的编译器更”聪明”——它在编译阶段就分析出哪些部分是动态的(可能变化),哪些是静态的(永远不变),然后把这些信息标记在 VNode 上(PatchFlag),运行时只处理有标记的节点。这就是 Vue3 比 Vue2 渲染性能更好的根本原因。

2. 核心原理/执行过程

2.1 整体编译流程

1
2
3
4
5
6
7
8
9
template 字符串

parse() — 词法/语法解析,生成 AST(抽象语法树)
↓ AST 是一棵描述模板结构的 JavaScript 对象树
transform() — 遍历 AST,添加 PatchFlag、静态提升等优化标记

generate() — 将带标记的 AST 生成 render 函数代码字符串

new Function('return ' + code) — 创建可执行的 render 函数

2.2 PatchFlag 详解

先从最简单的模板看起:

1
2
3
<template>
<div :class="cls">{{ message }}</div>
</template>

编译后生成的 render 函数(简化):

1
2
3
4
5
import { createVNode as _createVNode } from 'vue'

function render(_ctx, _cache) {
return _createVNode('div', { class: _ctx.cls }, _ctx.message, 3 /* TEXT | CLASS */)
}

最后一个参数 3 就是 PatchFlag。它是一个二进制位掩码,每一位代表一种动态类型:

1
2
3
4
5
6
7
8
9
// packages/shared/src/patchFlags.ts
export const enum PatchFlags {
TEXT = 1, // 二进制 001 → 动态文本内容
CLASS = 1 << 1, // 二进制 010 → 动态 class
STYLE = 1 << 2, // 二进制 100 → 动态 style
PROPS = 1 << 3, // 二进制 1000 → 动态属性(非 class/style)
FULL_PROPS = 1 << 4, // 动态键名(编译时不知道哪个属性变化)
// ...
}

PatchFlag = 1 | 2 = 3(二进制 011)表示这个节点既有动态文本(TEXT)又有动态 class(CLASS)。运行时 patch 时这样处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
function patchElement(oldVNode, newVNode) {
const patchFlag = newVNode.patchFlag

if (patchFlag & PatchFlag.CLASS) {
// 只比较 class(如果 CLASS 位是 1)
patchClass(el, newVNode.class)
}
if (patchFlag & PatchFlag.TEXT) {
// 只比较文本(如果 TEXT 位是 1)
patchText(el, newVNode.children)
}
// 没有标记的属性直接跳过,不比较
}

如果 PatchFlag = 0(静态节点),整个节点完全跳过 diff——Vue2 中即使完全静态的节点每次也要对比。

2.3 静态提升

静态提升是指:模板中完全静态的节点(没有绑定任何响应式数据)不放在 render 函数里重复创建,而是提升到外部只创建一次

1
2
3
4
5
6
<template>
<div>
<span>固定的标题</span> <!-- 静态 → 提升到外部 -->
<span>{{ dynamic }}</span> <!-- 动态 → 留在 render 里 -->
</div>
</template>

编译后:

1
2
3
4
5
6
7
8
9
// 静态提升到 render 函数外部
const _hoisted_1 = _createVNode('span', null, '固定的标题')

function render(_ctx, _cache) {
return _createVNode('div', null, [
_hoisted_1, // 复用同一个 VNode,不重新创建
_createVNode('span', null, _ctx.dynamic, 1 /* TEXT */)
])
}

每次组件重新渲染时,静态节点 _hoisted_1 都指向同一个 VNode 对象。这不仅节省了创建 VNode 的开销,而且 patch 时遇到 _hoisted_1(没有 PatchFlag)直接跳过。

2.4 事件缓存

1
2
3
<template>
<button @click="count++">+1</button>
</template>

Vue2 编译后:每次渲染都创建一个新的事件处理函数 ($event) => count++

Vue3 编译后:

1
2
3
4
5
function render(_ctx, _cache) {
return _createVNode('button', {
onClick: _cache[0] || (_cache[0] = ($event) => _ctx.count++)
}, '+1')
}

首次渲染:_cache[0] 是 undefined,执行右侧表达式创建函数并存到缓存。
后续渲染:_cache[0] 存在,直接返回缓存的函数。

为什么事件缓存有用? 如果父组件把内联函数作为 props 传给子组件,Vue2 中每次渲染都创建新函数 → 子组件收到的新 props(函数引用变了)→ 子组件重新渲染。Vue3 的事件缓存避免了这个问题。

3. 实际应用场景

场景1:通过编译器产物理解优化

可以用 Vue3 Template Explorer 查看模板编译后的代码。比较:

1
2
<!-- 全部静态 → PatchFlag = 0,且静态提升 -->
<span class="title">固定标题</span>
1
const _hoisted_1 = _createVNode('span', { class: 'title' }, '固定标题')
1
2
<!-- 部分动态 → PatchFlag 标记变化的部分 -->
<span :class="cls">{{ text }}</span>
1
_createVNode('span', { class: _ctx.cls }, _ctx.text, 3 /* TEXT, CLASS */)

场景2:Teleport 弹窗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- Modal.vue -->
<script setup>
defineProps({ visible: Boolean })
</script>

<template>
<Teleport to="body">
<Transition name="fade">
<div v-if="visible" class="modal-overlay">
<div class="modal-content">
<slot />
</div>
</div>
</Transition>
</Teleport>
</template>

Teleport 的 to="body" 告诉编译器:把这个 div 渲染到 body 下,但它在组件树中的逻辑位置不变(事件冒泡按组件树走)。

场景3:Suspense 异步组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
import { defineAsyncComponent } from 'vue'

const UserProfile = defineAsyncComponent(() => import('./UserProfile.vue'))
</script>

<template>
<Suspense>
<template #default>
<UserProfile :user-id="id" />
</template>
<template #fallback>
<LoadingSkeleton />
</template>
</Suspense>
</template>

如果 UserProfile 组件中有顶层的 await 操作(如 const user = await fetchUser()),Suspense 会自动等待它完成后再渲染 default 内容,期间显示 fallback。

4. 常见误区 & 实际项目中的坑

误区1:认为 <script setup> 只是语法糖,对编译优化没影响

<script setup> 不仅仅是语法糖——它让编译器能更准确地分析模板引用的变量来源。在 Options API + setup() 返回对象的写法中,编译器保守地认为所有绑定都可能是动态的。<script setup> 中模板引用直接映射到 import 变量,编译器可以更精确地判断哪些绑定是静态的。

误区2:使用了动态属性名 :[dynamicAttr] 导致优化降级

1
<div :[attr]="value">

当模板中出现动态键名时,编译器在编译时不知道具体是哪个属性变化,PatchFlag 会降级为 FULL_PROPS(完整 diff),性能下降。应尽量避免动态键名。

坑:v-if 和 v-for 优先级变化

Vue2 中 v-for 优先级高于 v-if,Vue3 中 v-if 优先级高于 v-for。迁移代码时要注意:

1
2
3
4
5
<!-- Vue2: v-for 先执行,v-if 在每次迭代中判断 -->
<li v-for="item in list" v-if="item.visible">{{ item.name }}</li>

<!-- Vue3: v-if 先执行,此时 item 未定义,会报错 -->
<!-- 需要改为 computed 过滤 -->

5. 与相关知识的关联 & 对比

编译特性Vue2Vue3
PatchFlag有,精确标记动态类型
静态提升
事件缓存
Tree-shaking不支持支持
根节点限制单根多根(Fragment)

6. 现代最佳实践(2024-2025)

  1. **保持模板”静态最大化”**——模板中的辅助函数调用会阻止静态提升,把计算逻辑放到 script 中。
  2. **避免动态属性名 :[attr]**——会导致 FULL_PROPS 降级。
  3. 使用 defineAsyncComponent + Suspense 做按需加载
  4. 配合 Vite 使用——Vite 开发模式下只编译当前页面用到的模板,HMR 更快。

7. 常见疑问解答

Q:PatchFlag 的位掩码设计为什么选择二进制位?

A:因为一个节点可能同时有多种动态类型(如同时有动态 class 和动态 style)。位掩码(1 | 2 | 4 = 7)可以用一个整数表示多种状态的组合,运行时用按位与(patchFlag & CLASS)快速判断某个类型是否是动态的。如果用数组或对象,判断和组合的开销都更大。

Q:Block Tree 是什么?它与 PatchFlag 的关系是什么?

A:Block Tree 是 Vue3 的另一个优化。组件渲染时,_openBlock() + _createBlock() 把所有带 PatchFlag 的动态节点扁平化收集到一个 dynamicChildren 数组中。patch 时直接遍历这个数组,而不是递归遍历整棵 VNode 树。Block Tree 和 PatchFlag 是配合使用的——PatchFlag 标记”哪些节点是动态的”,Block Tree 把这些节点收集为”快查表”。

关联知识点索引

  • diff 算法与虚拟DOM.md — PatchFlag 如何在运行时 diff 中被消费
  • 响应式原理.md — 响应式变化如何触发组件重新渲染

【面试速答版】

Q1: “Vue3 中父组件如何向子组件传递数据?子组件如何触发父组件的事件?”

父传子用 props,子传父用 emit。在 Vue3 的 <script setup> 中,用 defineProps 声明 props,用 defineEmits 声明自定义事件,两者都不需要 import:

1
2
3
4
5
6
7
8
9
10
11
<!-- 父 -->
<Child :title="pageTitle" @update="handleUpdate" />

<!-- 子 -->
<script setup>
const props = defineProps({ title: String })
const emit = defineEmits(['update'])

// 触发父组件事件
emit('update', newValue)
</script>

与 Vue2 的区别:Vue3 推荐 defineProps + defineEmits(在 script setup 中可用),替代了 Vue2 的 props 选项和 this.$emit

Q2: “Vue3 的 v-model 有什么变化?如何实现多个双向绑定?”

Vue3 支持多个 v-model,每个绑定对应一对 prop + update:prop 事件。默认的 v-model 对应 modelValue prop 和 update:modelValue 事件。多个绑定写法:v-model:title="title" v-model:content="content"。Vue2 的 .sync 修饰符在 Vue3 中废弃,统一用多 v-model 替代。

Q3: “provide/inject 的原理是什么?适合什么场景?”

祖先组件通过 provide(key, value) 提供数据,后代组件通过 inject(key) 注入。适合跨多层组件传递数据变化不频繁的场景,如主题(light/dark)、当前用户信息、语言设置。注意:默认不是响应式的——如果用 provide('count', ref(0)) 传一个 ref,后代才能感知变化。适合场景:深层传递、不需要额外引入状态管理库。

【深入理解版】

1. 这个知识点要解决什么问题?

组件化应用由父子组件嵌套构成,数据不可避免地需要跨越组件边界流动。主要场景:

  • 父子之间:父组件传递配置给子组件,子组件反馈操作结果
  • 祖孙之间:爷爷组件的数据,孙子组件需要用到,但中间两层”不关心”这个数据
  • 兄弟之间:两个同级组件需要共享同一份状态(如购物车数据)

Vue3 提供了多种通信机制,不同场景选不同方式。选错了就会导致代码冗余或性能问题。

2. 核心原理/执行过程

2.1 props 的单向数据流

props 是 Vue 最基本的通信方式——数据从父组件流向子组件,子组件不能直接修改 props:

1
2
3
4
5
6
7
<!-- 父组件 -->
<script setup>
const list = ref(['A', 'B', 'C'])
</script>
<template>
<Child :items="list" />
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 子组件 -->
<script setup>
// defineProps 声明接收的 props,不需要 import
// 参数可以传数组或对象(对象可以加类型/默认值校验)
const props = defineProps({
items: {
type: Array,
required: true,
default: () => [],
}
})

// ❌ 不能直接修改 props
// props.items.push('D') // 开发环境下会警告
</script>

单项数据流:数据从父到子单向流动。如果子组件需要修改数据,通过 emit 事件通知父组件去改。这种模式的好处是数据变化路径清晰,便于追踪 bug。

2.2 emit 事件

子组件通过 defineEmits 声明事件,然后调用 emit 触发:

1
2
3
4
5
6
7
8
<script setup>
const emit = defineEmits(['update', 'delete'])

function handleClick() {
emit('update', { id: 1, title: '新标题' })
// 第一个参数是事件名,后面的参数是传给父组件的载荷
}
</script>
1
2
<!-- 父组件 -->
<Child @update="(payload) => handleUpdate(payload)" />

Vue3 vs Vue2 的 emit 差异:Vue2 中 $emit 直接挂载在组件实例上(this.$emit),Vue3 中 defineEmits 是编译时宏,在 <script setup> 中不需要 import。而且 defineEmits 可以做事件名校验——声明了的事件才能触发,没声明的事件触发时会在开发环境下警告。

2.3 多 v-model 替代 .sync

Vue3 的 v-model 本质是语法糖——v-model:title="title" 等价于:

1
<Child :title="title" @update:title="val => title = val" />

所以 Vue3 支持多个 v-model 绑定

1
2
3
4
5
<Child
v-model:title="pageTitle"
v-model:content="pageContent"
v-model:author="pageAuthor"
/>

子组件中:

1
2
3
4
5
6
7
8
<script setup>
const props = defineProps(['title', 'content', 'author'])
const emit = defineEmits(['update:title', 'update:content', 'update:author'])
</script>
<template>
<input :value="title" @input="emit('update:title', $event.target.value)" />
<input :value="content" @input="emit('update:content', $event.target.value)" />
</template>

Vue2 的 .sync 修饰符(:title.sync="title")做的事情完全一样,Vue3 统一为 v-model:propName,语法更一致。

2.4 provide/inject 的跨层级注入

当数据需要从爷爷传给孙子,中间隔了两层,用 props 就要经过两层不必要的传递:

1
2
3
4
5
6
7
<!-- 爷爷组件 -->
<script setup>
import { ref, provide } from 'vue'
const theme = ref('dark')
provide('theme', theme) // 提供数据
provide('setTheme', (val) => theme.value = val) // 提供修改方法
</script>
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 孙子组件(跳过中间的父/子层级) -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme', 'light') // 第二个参数是默认值
const setTheme = inject('setTheme')
</script>
<template>
<div :class="theme">
当前主题: {{ theme }}
<button @click="setTheme('light')">切换为亮色</button>
</div>
</template>

provide(key, value) 把数据注入到后代组件中。inject(key, defaultValue) 从祖先组件获取数据,如果找不到对应的 provide,使用默认值。

默认不是响应式的——如果你 provide('theme', 'dark') 传了一个字符串,后代拿到的是固定值,后续祖先修改这个值后代不会感知。需要传 refreactive 对象才能保持响应式。

3. 实际应用场景

场景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
<!-- TodoList.vue -->
<script setup>
import { ref } from 'vue'
import TodoItem from './TodoItem.vue'

const todos = ref([
{ id: 1, text: '学习 Vue3', done: false },
{ id: 2, text: '复习响应式', done: false },
])

function toggleTodo(id) {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.done = !todo.done
}
</script>

<template>
<TodoItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
@toggle="toggleTodo"
/>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- TodoItem.vue -->
<script setup>
const props = defineProps({
todo: { type: Object, required: true }
})
const emit = defineEmits(['toggle'])
</script>

<template>
<li>
<input type="checkbox" :checked="todo.done" @change="emit('toggle', todo.id)" />
<span :class="{ done: todo.done }">{{ todo.text }}</span>
</li>
</template>

场景2:多 v-model 实现表单组件

1
2
3
4
5
6
7
8
9
10
<!-- UserForm.vue(父组件) -->
<script setup>
const name = ref('')
const email = ref('')
</script>

<template>
<FormInput v-model:value="name" label="姓名" />
<FormInput v-model:value="email" label="邮箱" type="email" />
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- FormInput.vue(子组件) -->
<script setup>
const props = defineProps({
value: String,
label: String,
type: { type: String, default: 'text' },
})
const emit = defineEmits(['update:value'])
</script>

<template>
<div>
<label>{{ label }}</label>
<input :type="type" :value="value" @input="emit('update:value', $event.target.value)" />
</div>
</template>

4. 常见误区 & 实际项目中的坑

误区1:在子组件中直接修改 props

1
2
3
4
<script setup>
const props = defineProps(['count'])
props.count++ // ❌ 开发环境警告:意外修改 props
</script>

正确做法:emit 事件通知父组件修改。

误区2:认为 provide/inject 默认是响应式的

1
2
3
4
5
6
7
8
// ❌ 传基础类型,后代感知不到变化
provide('theme', 'dark')
const theme = inject('theme')
theme = 'light' // 祖先的值没变,后代也不会更新

// ✅ 传 ref 或 reactive
const theme = ref('dark')
provide('theme', theme)

坑:v-model 默认 prop 名称变化

Vue2 的 v-model="count" 默认对应 value prop 和 input 事件。Vue3 改为 modelValue prop 和 update:modelValue 事件。迁移旧组件时要注意。

5. 与相关知识的关联 & 对比

通信方式方向适用场景
props父→子直接父子
emit子→父子组件反馈操作
v-model父↔子双向绑定
provide/inject祖先→后代跨层级、低频数据
slots父→子(模板)定制渲染
defineExpose / ref父→子直接调用子组件方法
差异点Vue2Vue3
默认 v-modelvalue + inputmodelValue + update:modelValue
.sync 修饰符存在废弃(多 v-model 替代)
EventBus$on/$off(内置)移除(推荐 mitt)
$listeners独立对象合并到 $attrs
defineExpose默认全部暴露需显式声明

6. 现代最佳实践(2024-2025)

  1. 父子通信优先 props/emit + v-model,最显式、最符合单向数据流。
  2. 跨层级用 provide/inject 并传 ref/reactive 对象,保持响应式。
  3. 全局共享状态用 Pinia,不要自己用 provide/inject 实现全局状态管理——Pinia 帮你处理了状态隔离、响应式、DevTools 等。
  4. EventBus 场景用 mitt(仅 200 字节),Vue3 已移除 $on/$off
  5. 使用 TypeScript 时用 defineProps 的类型声明形式
1
2
3
4
5
6
7
8
9
<script setup lang="ts">
interface Props { title: string; count?: number }
const props = defineProps<Props>() // 类型安全

const emit = defineEmits<{
(e: 'update:title', val: string): void
(e: 'delete', id: number): void
}>()
</script>

7. 常见疑问解答

Q:provide/inject 和 Pinia 的适用边界在哪里?

A:provide/inject 适合跨 2-3 层但不会在多个无关页面之间共享的数据——如当前主题、当前用户信息。Pinia 适合跨页面、跨路由、多组件共享的全局状态——如购物车数据、用户设置。如果数据需要在完全不相关的两个页面之间共享,用 Pinia。如果数据只在”某个内部组件树”中传递,用 provide/inject。

Q:为什么 Vue3 要移除 $on/$off/$once?

A:$on/$off/$once 是 Vue2 实例上的事件发射器(EventBus)API。Vue 团队认为组件通信应该通过 props/emit、provide/inject、Pinia 等显式的机制,而不是”全局发布订阅”这种隐式方式。EventBus 在大型项目中会导致事件来源不明、命名冲突、调试困难的问题。如果确实需要跨组件的事件通信,用 mitt 库(200 字节)即可。

关联知识点索引

  • Vue3 核心变化与 Composition API.md — setup 中如何使用通信 API
  • Pinia.md — 全局状态管理与 provide/inject 的适用边界

【面试速答版】

Q1: “React 和 Vue 的核心设计理念有什么不同?”

React 是 UI 库,推崇”一切皆 JS”——JSX、函数式编程、状态变化触发整组件重渲染。Vue 是渐进式框架,推崇”声明式模板”——template + 指令、响应式系统自动追踪依赖精确更新、官方提供路由/状态管理一站式方案。两者都是组件化、声明式、基于 Virtual DOM,但 React 更灵活(少约束),Vue 更容易上手(模板直观)。React 的生态靠社区,Vue 的生态官方覆盖大部分。

Q2: “React 和 Vue 在渲染性能上有什么差异?”

Vue3 有编译时优化(PatchFlag + 静态提升 + Block Tree),只在动态节点处做 diff,开发者在大部分场景下不需要手动优化。React 是纯运行时 diff——setState 后整个组件重新渲染再 diff,需要开发者用 React.memo/useMemo/useCallback 手动控制更新范围。Benchmark 显示 Vue3 在挂载/更新上略有优势,但实际应用中差异可忽略(大列表、复杂交互场景中两者相当)。真正的性能瓶颈通常在”不合理的写法”而非框架本身。

Q3: “你在实际项目中用过两个框架吗?选型建议是什么?”

两个框架都能做出优秀的产品。选型主要看团队和业务:团队偏好函数式编程、需要大生态(React Native、Next.js)、大型 SPA 选 React。团队从传统 HTML/JS 转型、追求开发效率、需要官方一站式方案、国内项目选 Vue。新项目还可以考虑市场环境——国内招聘 Vue 岗位更多,国际大厂 React 更普遍。

【深入理解版】

1. 这个知识点要解决什么问题?

React 和 Vue 是 2024 年全球最流行的两个前端框架。面试中问”两者的区别”,本质是考察你对框架底层原理的理解深度——不是背诵 API 差异,而是理解它们各自的设计哲学和取舍。

先记住一句话:React 是”UI = f(state)”的纯粹主义者,Vue 是”让开发者写更少代码”的实用主义者。这个出发点导致了它们在 API 设计、模板语法、响应式机制、生态组织上的一系列差异。

2. 核心差异分析

2.1 响应式机制:手动触发 vs 自动追踪

这是两者最根本的技术差异。

React: 调用 setStateuseState 的 dispatch 后,整个组件重新执行 render 函数,生成新的 Virtual DOM 树,然后 diff。开发者需要显式告诉 React”数据变了”。

1
2
3
4
5
6
7
function Counter() {
const [count, setCount] = useState(0)
const [name, setName] = useState('')

// setCount 导致整个 Counter 组件重新执行(包括 name 相关的代码)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

Vue3: 修改响应式数据(count.value++)时,只有用到 count 的组件才会重新渲染。不需要手动调用像 setState 这样的”通知”函数,也不需要指定哪些数据变了——Proxy 自动追踪。

1
2
3
4
5
6
7
8
<script setup>
const count = ref(0)
const name = ref('')
// count.value++ 只触发用到 count 的组件更新,name 不受影响
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>

这个差异带来的实际影响:

场景React 需要Vue 需要
阻止子组件无关重渲染React.memo + useMemo/useCallback不需要(自动精确追踪)
缓存计算结果useMemo(手动声明依赖)computed(自动追踪依赖)
监听数据变化useEffect(手动声明依赖)watch/watchEffect(自动追踪)
闭包陷阱常见问题(stale closure)不存在(响应式引用)

Vue 的自动追踪依赖减少了开发者的心智负担,但代价是运行时维护了一个依赖收集系统(Proxy + WeakMap + Set),这也是 Vue 比 React 体积略大的原因之一。

2.2 模板语法:JSX vs 指令

React 用 JSX,Vue 用 template + 指令。这个差异影响了写代码的思维方式。

1
2
3
4
5
6
// React — 用 JS 控制逻辑
function List({ items, loading, empty }) {
if (loading) return <Spinner />
if (items.length === 0) return <Empty />
return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>
}
1
2
3
4
5
6
7
8
<!-- Vue — 用指令 -->
<template>
<Spinner v-if="loading" />
<Empty v-else-if="items.length === 0" />
<ul v-else>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</template>

差异的本质: React 把”控制逻辑”完全交给 JavaScript——if/else、map、三元运算符。Vue 把”视图逻辑”抽象为指令——v-ifv-forv-show。JSX 更灵活(你可以用任何 JS 语法),但需要开发者已经熟悉 JavaScript;模板更接近传统 HTML+CSS 的思维模式,对从 HTML 转前端的人更友好。

2.3 Composition API vs Hooks

Vue3 的 Composition API 和 React Hooks 是相似的解决方案,但实现不同

1
2
3
4
5
6
7
8
9
10
11
// Vue3 Composition API
const count = ref(0)
const doubled = computed(() => count.value * 2)
watch(count, (val) => console.log(val))
onMounted(() => fetchData())

// React Hooks
const [count, setCount] = useState(0)
const doubled = useMemo(() => count * 2, [count])
useEffect(() => { console.log(count) }, [count])
useEffect(() => { fetchData() }, [])
对比Vue3 Composition APIReact Hooks
响应式Proxy 自动追踪依赖显式声明依赖数组
缓存computed(自动)useMemo(手动)
副作用watch/watchEffect(自动追踪)useEffect(手动声明依赖)
执行时机setup 在组件创建时执行一次函数组件每次渲染都执行
钩子/条件使用可以放在 if 中不能放在条件/循环中
闭包陷阱不存在存在

关键差异:Vue 的 setup 只执行一次refcomputedwatch 在 setup 中就建立好响应式连接,之后数据变化时不需要重新执行 setup。React 的函数组件每次渲染都重新执行,hooks 靠调用顺序匹配上一次的状态——这就是为什么 React hooks 有调用顺序的限制,而 Vue 没有。

3. 实际场景对比

场景1:表单双向绑定

1
2
3
4
5
6
7
8
9
10
11
// React — 手动控制 value + onChange
function Form() {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
return (
<form>
<input value={name} onChange={e => setName(e.target.value)} />
<input value={email} onChange={e => setEmail(e.target.value)} />
</form>
)
}
1
2
3
4
5
6
7
8
9
<!-- Vue — v-model -->
<script setup>
const name = ref('')
const email = ref('')
</script>
<template>
<input v-model="name" />
<input v-model="email" />
</template>

Vue 的 v-model 更简洁,React 的 value + onChange 更显式(数据流方向清晰)。

场景2:逻辑复用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Vue3 — composable
export function useMouse() {
const x = ref(0)
const y = ref(0)
onMounted(() => window.addEventListener('mousemove', e => { x.value = e.x; y.value = e.y }))
onUnmounted(() => window.removeEventListener('mousemove', handler))
return { x, y }
}

// React — custom hook
export function useMouse() {
const [pos, setPos] = useState({ x: 0, y: 0 })
useEffect(() => {
const handler = (e) => setPos({ x: e.x, y: e.y })
window.addEventListener('mousemove', handler)
return () => window.removeEventListener('mousemove', handler)
}, [])
return pos
}

4. 常见误区 & 面试陷阱

误区 1:”Vue 的响应式比 React 的 setState ‘先进’。”

A:这不是”先进”的问题,而是”取舍”的问题。Vue 选择在运行时维护依赖收集系统来做到自动精确更新,代价是额外的运行时开销和更大的框架体积。React 选择信 VDOM diff——执行整个组件函数生成新 VNode,然后 diff。自动追踪不意味更高性能,只是把性能优化的责任从开发者转移到了框架。两者最终都要遍历和 diff,只是路径不同。

误区 2:”Vue 和 React 哪个快?”

A:Benchmark 显示 Vue3 在组件挂载/更新上略有优势(得益于编译优化),但差异通常在 10% 以内,用户无感知。实际应用中性能瓶颈永远在”不合理的写法”(大列表没有虚拟滚动、组件没有合理拆分、不必要的重渲染),而不是框架本身。选型时不应以性能为主要考量。

误区 3:”React 比 Vue 更适合大型项目。”

A:这取决于”大型”的定义。大型项目需要的不是特定的框架,而是架构约束——文件夹组织、状态管理模式、代码规范、TypeScript 类型系统。React + Redux Toolkit 有更强的数据流约束(纯函数 reducer、单向 dispatch),Vue + Pinia 更灵活但需要团队自发约定。两者都能支撑大型项目,TiScript(Vue 核心团队使用)、Shopify(React 为主)都证明了这一点。

5. 生态系统对比

领域React 生态Vue 生态
路由React Router 6Vue Router 4(官方)
状态管理Redux / Zustand / JotaiPinia(官方)
构建工具Vite / Next.jsVite / Nuxt 3(官方)
SSG/SSRNext.js / RemixNuxt 3(官方)
移动端React Nativeuni-app / Weex
测试Jest + React Testing LibraryVitest + @vue/test-utils
数据请求TanStack Query / SWRVue Query / Pinia + action
TypeScript原生支持良好

6. 选型建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
选 React:
├─ 需要 React Native 跨平台移动端
├─ 需要 Next.js 做 SSR/SSG(生态最成熟)
├─ 团队熟悉函数式编程、偏好少约束
├─ 大型复杂 SPA,需要灵活性和生态广度
└─ 面向国际市场的产品

选 Vue:
├─ 快速原型、中小型项目
├─ 团队从 HTML/CSS/JS 转型
├─ 需要官方一站式解决方案
├─ 面向国内市场的产品
└─ 渐进式引入(现有项目中逐步使用)

不应只看框架本身:
└─ 团队技术栈、招聘难度、现有代码库、业务需求 > 框架性能差异

7. 常见疑问解答

Q:Vue 和 React 的共同点是什么?

A:① 组件化——UI 由组件树构成;② 声明式——描述”UI 应该是什么样”,而不是”怎么操作 DOM”;③ Virtual DOM——用 JS 对象模拟真实 DOM,通过 diff 最小化 DOM 操作;④ 单向数据流——数据从父组件向子组件流动;⑤ 框架无关的共同理念:UI = f(state)。

Q:如果团队要从 Vue 迁移到 React(或反之),最大的挑战是什么?

A:最大的挑战不是新框架的 API,而是思维模式的转变。Vue → React:要习惯”一切皆 JS”(没有 v-if/v-for/v-model 了,都用 JS 控制)、习惯手动管理依赖(useEffect 要写依赖数组、useMemo 要声明依赖)、习惯处理闭包陷阱。React → Vue:要习惯模板语法(指令、插槽)、理解响应式的”自动”特性(什么时候该用 ref、什么时候该用 reactive)、习惯用 <script setup>

Q:两个框架都在往”编译时优化”方向发展,这代表什么趋势?

A:代表前端框架从”纯运行时”向”编译时 + 运行时”结合的方向演进。Vue3 的 PatchFlag 是编译时优化,React 也在探索 React Forget(自动记忆编译器,把 useMemo/useCallback 的负担转移给编译器)。未来的趋势是:框架承担更多的性能优化责任,开发者越来越少需要手动优化。

关联知识点索引

  • 所有 Vue3 文档(../Vue3/
  • 所有 React 文档(本目录)
  • Vue2 vs Vue3 对比.md — Vue 框架内部更新对比