嘿,朋友。咱们今天不聊那些枯燥的教科书定义,而是像两个老搭档坐在咖啡馆里,聊聊那些让我们又爱又恨的前端框架底层逻辑。你问为什么 Vue 的 v-model 那么丝滑,而 React 的 useState 却要求我们时刻警惕“引用相等”?这背后其实是两种完全不同的哲学在打架,或者说,在共舞。
很多初级甚至中级工程师,习惯了用 API,却忘了去摸一摸 API 底下的肌肉纹理。今天,我们就把 Vue 和 React 这两块招牌掀开,看看里面到底藏着什么秘密。我会尽量讲得透彻,哪怕你是刚入行的小白,也能听懂;如果你是老手,或许能从中找到一些重新审视代码的新视角。
第一部分:Vue 的响应式魔法——它是如何“偷看”你的数据的?
在 Vue 2 中,当你写下 data() { return { count: 0 } } 时,Vue 其实并没有直接把这个对象给你。它做了一个“狸猫换太子”的操作。
1. Object.defineProperty 的局限性
Vue 2 的核心是 Object.defineProperty。它会在数据初始化时,遍历所有属性,使用 defineProperty 来劫持 getter 和 setter。
想象一下,Vue 就像一个极其敏感的管家。当你读取 user.name 时(getter),它会悄悄记录:“哦,当前正在渲染的这个组件依赖了 user.name。” 当你修改 user.name = 'New Name' 时(setter),它会立刻跳出来:“有人改了数据!快通知那些依赖了这个数据的组件去重新渲染!”
// 模拟 Vue 2 的简易响应式核心
function defineReactive(obj, key, val) {
// 递归处理嵌套对象
observe(val);
const dep = new Dep(); // 依赖收集器
Object.defineProperty(obj, key, {
get() {
// 如果当前有依赖收集器在工作,就把这个依赖加进去
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
// 触发更新
dep.notify();
}
});
}
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
Dep.target = null; // 全局唯一的依赖收集目标
这里有个坑: 为什么 Vue 2 不能检测对象属性的添加或删除?因为 defineProperty 只能在初始化时定义属性。后来你加个 obj.newKey = 1,Vue 根本不知道,因为它没有对这个新键做 defineProperty。这就是为什么 Vue 提供了 Vue.set() 或者 this.$set 方法。
2. Proxy 的革命:Vue 3 的降维打击
到了 Vue 3,情况变了。ES6 引入了 Proxy,它像一个透明的中间人,拦截对象上发生的一切操作。
const handler = {
get(target, prop, receiver) {
console.log(`获取属性: ${prop}`);
track(target, prop); // 依赖收集
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`设置属性: ${prop} = ${value}`);
const oldValue = target[prop];
const result = Reflect.set(target, prop, value, receiver);
if (oldValue !== value) {
trigger(target, prop); // 触发更新
}
return result;
}
};
const proxy = new Proxy(originalData, handler);
Proxy 的优势显而易见:
- 无需递归初始化:只有当访问某个深层属性时,才会递归代理,性能更好。
- 支持数组索引修改和长度变化:
arr[0] = 1或arr.length = 0都能被完美拦截。 - 支持 Map, Set, WeakMap 等复杂类型。
对于小朋友来说,你可以这样理解:Object.defineProperty 像是给每个房间装了一个报警器,只有房间建好时才能装;而 Proxy 像是给整栋大楼装了智能监控系统,不管谁进出哪个房间,系统都知道。
第二部分:React 的不可变数据流——为什么我们要“复制”而不是“修改”?
如果说 Vue 是“命令式”的(我改数据,你帮我更新),那 React 就是“声明式”的(我告诉你要变成什么样,你自己去算)。
1. setState 的本质与批处理
在 React 中,你永远不会直接修改 state。
// ❌ 错误示范:直接修改
this.state.count++;
// ✅ 正确示范:通过 setState
this.setState({ count: this.state.count + 1 });
当你调用 setState 时,React 并没有立即更新 DOM。它做了什么?
- 它将新的 state 合并到一个 pending state 队列中。
- 标记当前组件为“脏”(dirty)。
- 在下一个事件循环(microtask)或批量更新周期中,React 会重新执行 render 函数,生成新的 Virtual DOM 树。
- 通过 Diff 算法,找出新旧 Virtual DOM 的差异。
- 将最小量的真实 DOM 更新应用到页面上。
关键点:不可变性(Immutability)。
// 假设我们要更新一个列表中的某一项
// ❌ 可变操作(React 无法感知)
items[0].name = "New Name";
setItems(items); // React 发现 items 的引用没变,可能跳过更新
// ✅ 不可变操作(React 能感知)
const newItem = { ...items[0], name: "New Name" };
const newItems = [newItem, ...items.slice(1)];
setItems(newItems); // 引用变了,React 知道数据变了
为什么要这么麻烦?因为如果允许直接修改,React 就失去了“纯函数”的特性。纯函数意味着:同样的输入,永远得到同样的输出,且没有副作用。这使得 React 的渲染过程变得可预测、可调试,也更容易实现高性能的并发特性(如 React 18 的 Concurrent Mode)。
2. 虚拟 DOM 与 Diff 算法
Virtual DOM 不是性能优化的银弹,而是一种抽象。它的核心价值在于解耦。
以前我们手动操作 DOM,就像是用螺丝刀一点点拧电路板。现在,我们用 JavaScript 对象描述界面,然后让 React 这个“编译器”来决定怎么最高效地更新真实 DOM。
Diff 算法的核心优化是 Key 属性。
// 如果没有 Key
{list.map(item => <div>{item.name}</div>)}
// 如果有 Key
{list.map(item => <div key={item.id}>{item.name}</div>)}
Key 帮助 React 识别哪些元素是稳定的,哪些是被移动或删除的。如果没有 Key,React 只能按位置比对,导致不必要的 DOM 重建和状态丢失(比如输入框的内容突然清空)。
第三部分:组件化开发的高级技巧——从“能用”到“好用”
掌握了底层原理,我们来看看在实际项目中,如何写出既优雅又高效的组件。
1. Vue 的组合式 API (Composition API) vs Options API
Vue 2 的 Options API 把 data, methods, computed 分开写。这在组件简单时很清晰,但逻辑复杂时,同一个功能的代码会被拆散在文件的不同角落。
Vue 3 的 Composition API 允许我们按逻辑关注点组织代码。
<script setup>
import { ref, computed, onMounted } from 'vue';
// 逻辑块 1:用户状态
const user = ref(null);
const isLoading = ref(false);
async function fetchUser(id) {
isLoading.value = true;
try {
user.value = await api.getUser(id);
} catch (e) {
console.error(e);
} finally {
isLoading.value = false;
}
}
// 逻辑块 2:分页状态
const page = ref(1);
const pageSize = ref(10);
const total = ref(0);
const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
onMounted(() => {
fetchUser(123);
});
</script>
这种写法非常适合大型项目,因为你可以把相关的逻辑封装成自定义 Hook(在 Vue 中叫 Composables),然后在多个组件间复用。
2. React 的 Hooks 与自定义 Hook
React 的 Hooks 理念与 Vue 的 Composition API 异曲同工。它们都旨在解决“逻辑复用”和“状态管理”的问题。
让我们写一个实用的自定义 Hook:useLocalStorage。这个 Hook 可以让组件的状态自动同步到浏览器的 LocalStorage。
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// 1. 初始化状态:先从 localStorage 读取,如果没有则使用初始值
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
// 2. 监听 storedValue 的变化,同步到 localStorage
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.log(error);
}
}, [key, storedValue]);
// 3. 返回状态值和设置函数
return [storedValue, setStoredValue];
}
// 使用示例
function Profile() {
const [name, setName] = useLocalStorage('profileName', 'Guest');
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<p>Hello, {name}!</p>
</div>
);
}
为什么这个 Hook 强大?
- 它封装了复杂的副作用逻辑(读写 localStorage)。
- 它利用了闭包和状态隔离,确保每个组件实例拥有独立的存储键。
- 它展示了 React Hooks 的核心思想:将 UI 逻辑与副作用逻辑分离。
3. 性能优化:避免不必要的重渲染
无论是 Vue 还是 React,性能优化的核心都是减少计算量和 DOM 操作。
Vue 的优化手段:
v-once: 用于静态内容,告诉 Vue 只渲染一次,后续忽略变化。computedvsmethods: 计算属性有缓存机制,只有依赖的数据变化时才会重新计算。v-memo(Vue 3.2+): 类似 React 的memo,用于跳过子树的渲染。
<!-- 只有当 items.length 变化时,才重新渲染列表 -->
<div v-memo="[items.length]">
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>
</div>
React 的优化手段:
React.memo: 高阶组件,对函数组件进行浅比较,props 不变则不重新渲染。useMemo: 缓存计算结果。useCallback: 缓存函数引用,防止因函数创建导致子组件不必要的重渲染。
// 父组件
const Parent = () => {
const [count, setCount] = useState(0);
// 每次 render 都会创建新的 handleClick 函数,导致 Child 重渲染
// const handleClick = () => setCount(c => c + 1);
// 使用 useCallback 缓存函数,只有依赖变化时才更新
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
return (
<>
<Child onClick={handleClick} />
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
</>
);
};
// 子组件
const Child = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click Me</button>;
});
第四部分:实战对比——同一个功能,两种思维
让我们做一个具体的例子:一个简单的待办事项列表(Todo List),包含添加、删除和过滤功能。
Vue 实现
<template>
<div class="todo-app">
<input v-model="newTodo" @keyup.enter="addTodo" placeholder="Add a todo..." />
<ul>
<li v-for="todo in filteredTodos" :key="todo.id">
<span :class="{ done: todo.done }">{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">Delete</button>
</li>
</ul>
<div>
<label>
<input type="radio" value="all" v-model="filter" /> All
</label>
<label>
<input type="radio" value="active" v-model="filter" /> Active
</label>
<label>
<input type="radio" value="completed" v-model="filter" /> Completed
</label>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const newTodo = ref('');
const todos = ref([
{ id: 1, text: 'Learn Vue', done: false },
{ id: 2, text: 'Build a project', done: true }
]);
const filter = ref('all');
const addTodo = () => {
if (newTodo.value.trim()) {
todos.value.push({
id: Date.now(),
text: newTodo.value,
done: false
});
newTodo.value = '';
}
};
const removeTodo = (id) => {
todos.value = todos.value.filter(t => t.id !== id);
};
const filteredTodos = computed(() => {
if (filter.value === 'active') return todos.value.filter(t => !t.done);
if (filter.value === 'completed') return todos.value.filter(t => t.done);
return todos.value;
});
</script>
Vue 的思维特点:
- 双向绑定:
v-model让表单输入和数据保持同步,非常直观。 - 响应式计算:
computed自动追踪依赖,filteredTodos会在todos或filter变化时自动更新。 - 模板语法:HTML 中嵌入逻辑,适合设计师或喜欢 HTML 的开发者。
React 实现
import React, { useState, useMemo } from 'react';
const TodoApp = () => {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', done: false },
{ id: 2, text: 'Build a project', done: true }
]);
const [newTodo, setNewTodo] = useState('');
const [filter, setFilter] = useState('all');
const addTodo = (e) => {
e.preventDefault();
if (newTodo.trim()) {
setTodos([...todos, { id: Date.now(), text: newTodo, done: false }]);
setNewTodo('');
}
};
const removeTodo = (id) => {
setTodos(todos.filter(t => t.id !== id));
};
const filteredTodos = useMemo(() => {
switch (filter) {
case 'active': return todos.filter(t => !t.done);
case 'completed': return todos.filter(t => t.done);
default: return todos;
}
}, [todos, filter]);
return (
<div className="todo-app">
<form onSubmit={addTodo}>
<input
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add a todo..."
/>
<button type="submit">Add</button>
</form>
<ul>
{filteredTodos.map(todo => (
<li key={todo.id} style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
<span>{todo.text}</span>
<button onClick={() => removeTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
<div>
{['all', 'active', 'completed'].map(f => (
<label key={f}>
<input
type="radio"
value={f}
checked={filter === f}
onChange={() => setFilter(f)}
/> {f.charAt(0).toUpperCase() + f.slice(1)}
</label>
))}
</div>
</div>
);
};
export default TodoApp;
React 的思维特点:
- 单向数据流:状态(State)驱动视图。输入框的值由
newTodo控制,变化通过onChange触发setNewTodo。 - 不可变更新:
setTodos([...todos, ...])创建新数组,确保 React 检测到变化。 - JSX 与逻辑融合:UI 和逻辑都在 JavaScript 中,灵活性更高,但也更复杂。
第五部分:给初学者的建议——如何跨越这两个框架的鸿沟?
如果你只会 Vue 或只会 React,建议你尝试学习另一个。这不仅能拓宽你的就业面,更能让你深刻理解前端工程的本质。
- 理解“状态”是核心:无论哪个框架,前端开发的核心都是管理状态。Vue 用响应式系统自动管理,React 用不可变数据和显式更新管理。
- 不要迷信框架:框架只是工具。原生 JavaScript 的 DOM 操作、事件委托、闭包、原型链,这些基础才是你的内功。
- 动手写 Diff 算法:试着手写一个简单的 Virtual DOM Diff 算法。你会惊讶地发现,现代框架的优化思路其实非常经典,很多思想源于 90 年代的浏览器实验。
- 关注生态:Vue 有 Pinia/Vuex,React 有 Redux/Zustand/Context。学习状态管理库的原理,比学习框架本身更有价值。
结语
前端世界变化很快,Vue 和 React 也在不断演进。Vue 3.5 引入了更强大的服务端组件支持,React 18 的并发特性也在改变我们编写异步逻辑的方式。但万变不离其宗:数据驱动视图,组件化构建复杂系统。
希望这篇文章能帮你拨开迷雾,看清 MVVM 和组件化的本质。记住,最好的代码不是最炫的代码,而是最能解决问题、最容易维护的代码。
如果你在实践中遇到任何问题,或者想深入探讨某个细节,随时回来找我。我们下期见!
