
2025年底,我在掘金上看到一个提问:"学了三年React,换到Vue项目组完全懵了,是不是要重新学?"
这个问题下面有237条回复,大部分都在说"正常,我也是这样"。
但这不正常。
如果你真的理解了React,切换到Vue应该只需要2-3天,而不是"重新学"。
问题出在哪?
你在背API,而不是理解系统。
我花了两周时间啃React和Supabase的源码,发现了一个让我震惊的事实:所谓的"框架差异",90%都是表面语法的差异。
底层系统?几乎一模一样。
就像你学会了开手动挡汽车,换到自动挡只需要适应一下操作方式,而不是重新学"怎么开车"。
今天这篇文章,我要用大白话+实战代码,彻底讲清楚:什么是系统思维,以及它如何让你在2026年的前端战场上碾压那些还在"背框架"的开发者。
你肯定经历过这个循环:
第1步: 打开B站,搜索"React useEffect教程"
第2步: 跟着敲代码,能跑起来了,感觉自己会了
第3步: 关掉视频,自己写项目,盯着空白屏幕20分钟
第4步: 又回到B站,搜索"React useEffect实战"
第5步: 继续循环...
这就是语法思维的陷阱:
// 你记住了这个写法
useEffect(() => {
fetchData()
}, [dependency])
// 但你不知道:
// ❌ 为什么要用useEffect?直接在组件里fetch不行吗?
// ❌ 为什么有时候它会执行两次?
// ❌ dependency数组到底是干嘛的?
// ❌ 什么时候该用useEffect,什么时候不该用?
问题核心:你学的是HOW(怎么写),而不是WHY(为什么这么设计)。
所以React 19一更新,引入了use()这个新API,你又懵了。
因为你不知道useEffect到底在解决什么问题。
语法思维 vs 系统思维:同样的问题,两种思路
语法思维的回答:
"用useEffect啊,然后在里面调用fetch,代码是这样的..."
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data))
}, [])
完了。他只会告诉你代码怎么写。
系统思维的回答:
"等等,我们先理清楚这个问题的本质:"
问题拆解:
┌─────────────────────────────────────┐
│ 1. 数据获取是【副作用】 │
│ → 不可预测(网络可能失败) │
│ │
│ 2. React组件渲染是【纯函数】 │
│ → 必须可预测(相同输入→相同输出)│
│ │
│ 3. 如果在渲染中直接fetch会怎样? │
│ → 每次渲染都fetch │
│ → 无限循环! │
│ │
│ 4. 所以需要一个机制: │
│ → 把副作用和渲染分离 │
│ → 控制副作用的执行时机 │
└─────────────────────────────────────┘
这就是useEffect存在的原因。
明白了吗?一个是"记住写法",一个是"理解设计"。
前者只能用React,后者能用所有框架。
看似不同的三种写法:
// React - Hooks模式
const [count, setCount] = useState(0)
setCount(count + 1)
// Vue - 响应式模式
const count = ref(0)
count.value++
// Svelte - 编译器模式
let count = 0
count++
小白会说:"完全不一样啊!语法差这么多!"
懂系统的人会说:"都是观察者模式,只是包装不同。"
观察者模式的核心(20行搞定):
// 手写一个超级简化的状态管理器
function createState(initialValue) {
let value = initialValue // 1. 存储值
const observers = [] // 2. 观察者列表
return {
// 获取当前值
get: () => value,
// 修改值 + 通知所有观察者
set: (newValue) => {
value = newValue // 3. 更新值
observers.forEach(fn => fn(value)) // 4. 通知订阅者
},
// 订阅变化
subscribe: (callback) => {
observers.push(callback) // 5. 添加观察者
}
}
}
// 使用示例
const count = createState(0)
count.subscribe((val) => {
console.log('UI更新了:', val) // 模拟UI更新
})
count.set(1) // 输出: UI更新了: 1
count.set(2) // 输出: UI更新了: 2
看懂了吗?
React的useState、Vue的ref、Svelte的响应式变量,本质都是这个模式的不同包装:
// React包装方式:用Hook隐藏观察者细节
const [count, setCount] = useState(0)
// 内部实现:
// - count是当前值
// - setCount触发观察者通知
// - React自动处理UI更新
// Vue包装方式:用Proxy实现自动追踪
const count = ref(0)
// 内部实现:
// - 用Proxy包装对象
// - 读取时收集依赖
// - 修改时触发更新
// Svelte包装方式:编译时生成观察者代码
let count = 0
count = count + 1
// 编译后:
// count = count + 1;
// $$invalidate('count', count) // 自动生成的通知代码
观察者模式 = 微信群通知
你(状态管理器)在群里发消息(setState):
"晚上聚餐地点改成海底捞了"
所有@你的人(观察者)都收到通知:
- 界面组件A: 更新地图显示
- 界面组件B: 更新餐厅详情
- 界面组件C: 重新计算路线
不同框架只是"通知方式"不同:
- React: 手动@所有人
- Vue: 自动@相关的人
- Svelte: 编译时就确定@谁
一旦你理解了这个系统,学习新框架的状态管理就是:
3个问题,10分钟搞定。
问:"什么是组件?"
99%的人会回答:"可复用的UI片段。"
错!
这是结果,不是目的。
组件的真正作用是:建立边界,隔离复杂度。
假设你在维护一个后台管理系统,代码写成这样(很常见):
// ❌ 所有逻辑混在一起的噩梦
function AdminDashboard() {
// 用户相关
const [user, setUser] = useState(null)
const [userLoading, setUserLoading] = useState(false)
const login = async (credentials) => { /* 50行代码 */ }
const logout = () => { /* 30行代码 */ }
// 订单相关
const [orders, setOrders] = useState([])
const [orderFilter, setOrderFilter] = useState('all')
const fetchOrders = async () => { /* 40行代码 */ }
const updateOrder = async (id, data) => { /* 35行代码 */ }
// 统计相关
const [stats, setStats] = useState({})
const calculateStats = () => { /* 60行代码 */ }
// 设置相关
const [settings, setSettings] = useState({})
const saveSettings = async () => { /* 45行代码 */ }
// ... 总共800行代码
// 现在老板说:订单模块要改需求
// 你敢动吗?改了订单会不会影响用户登录?
// 不知道!因为没有边界!
}
这就是没有边界的代价:改一个地方,牵扯整个系统。
// ✅ 用组件建立清晰的边界
function AdminDashboard() {
return (
<AuthBoundary> {/* 边界1: 认证逻辑 */}
<OrdersBoundary> {/* 边界2: 订单逻辑 */}
<StatsBoundary> {/* 边界3: 统计逻辑 */}
<SettingsBoundary> {/* 边界4: 设置逻辑 */}
<DashboardView />
</SettingsBoundary>
</StatsBoundary>
</OrdersBoundary>
</AuthBoundary>
)
}
// 每个边界内部:
// ✅ 只管自己的状态
// ✅ 只管自己的逻辑
// ✅ 只暴露必要的接口
// ✅ 修改时不影响其他边界
现在改订单模块?只需要打开OrdersBoundary,其他代码一行不动。
无边界的系统:
┌─────────────────────────────────────────┐
│ 所有逻辑混在一起 │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │用户 │ │订单 │ │统计 │ │设置 │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
│ ↓ ↓ ↓ ↓ │
│ 互相纠缠,牵一发动全身 │
└─────────────────────────────────────────┘
有边界的系统:
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 用户边界 │ │ 订单边界 │ │ 统计边界 │ │ 设置边界 │
├─────────┤ ├─────────┤ ├─────────┤ ├─────────┤
│ 状态 │ │ 状态 │ │ 状态 │ │ 状态 │
│ 逻辑 │ │ 逻辑 │ │ 逻辑 │ │ 逻辑 │
│ 接口 │ │ 接口 │ │ 接口 │ │ 接口 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
↓ ↓ ↓ ↓
独立修改 独立修改 独立修改 独立修改
不仅仅是React:
// Vue组件 - 同样的边界思想
<template>
<AuthBoundary>
<OrdersBoundary>
<DashboardView />
</OrdersBoundary>
</AuthBoundary>
</template>
// Web Components - 同样的边界思想
<auth-boundary>
<orders-boundary>
<dashboard-view></dashboard-view>
</orders-boundary>
</auth-boundary>
// 甚至Node.js模块 - 同样的边界思想
const authModule = require('./auth') // 边界
const ordersModule = require('./orders') // 边界
const statsModule = require('./stats') // 边界
一旦理解"边界"这个系统,你就知道:
"为什么React要用虚拟DOM?我直接操作真实DOM不行吗?"
语法层面的回答:"虚拟DOM快啊!"
错!虚拟DOM不一定更快。
真正的问题是:如何用最小的代价更新UI?
旧状态: [A, B, C, D, E]
新状态: [A, C, D, F, E]
问题: 怎样用最少的操作从旧状态变成新状态?
暴力方案: 删除所有,重新创建所有 (5次删除 + 5次创建 = 10次操作)
聪明方案: 删除B,添加F (1次删除 + 1次创建 = 2次操作)
这就是协调(Reconciliation)要解决的问题。
// React策略: 虚拟DOM差异对比
const oldTree = { type: 'ul', children: [
{ type: 'li', key: 'A', text: 'A' },
{ type: 'li', key: 'B', text: 'B' },
{ type: 'li', key: 'C', text: 'C' },
]}
const newTree = { type: 'ul', children: [
{ type: 'li', key: 'A', text: 'A' },
{ type: 'li', key: 'C', text: 'C' },
{ type: 'li', key: 'D', text: 'D' },
]}
// React会对比两棵树,找出差异
diff(oldTree, newTree)
// 结果: 删除B, 添加D
// Vue策略: 响应式依赖追踪(不需要diff)
const state = reactive({
items: ['A', 'B', 'C']
})
// Vue知道哪个组件依赖items
// items变化时,只重新渲染依赖的部分
// 跳过了diff步骤,更快!
// Svelte策略: 编译时优化
// 编译时就生成精确的更新代码
items = ['A', 'C', 'D'] // 源代码
// 编译后:
if (changed.includes('items')) {
// 直接生成更新代码,不需要运行时diff
element.children[1].remove() // 删除B
element.appendChild(createLi('D')) // 添加D
}
场景: 你要重新布置房间
React方式(虚拟DOM):
1. 拍一张当前房间的照片(旧虚拟DOM)
2. 在脑海里想象新布局(新虚拟DOM)
3. 对比两张"照片",列出变化清单:
- 沙发从左边移到右边
- 删除茶几
- 添加书架
4. 按清单执行搬运
Vue方式(响应式):
1. 每个家具都有"标签"
2. 你说"把沙发移到右边"
3. 只有沙发动,其他不动
4. 不需要对比整个房间
Svelte方式(编译优化):
1. 装修前就请设计师规划
2. 设计师给你精确的施工图:
"第3步:移动沙发到坐标(5,10)"
3. 按图施工,不需要临时决策
理解了这个系统,你就明白:
console.log(1)
setTimeout(() => console.log(2), 0)
console.log(3)
// 输出: 1, 3, 2
// 为什么setTimeout设置了0ms,还是最后执行???
背API的人:"JavaScript是单线程的,所以setTimeout会放到队列里..."(背书)
懂系统的人:"因为事件循环的执行顺序,让我画个图给你看。"
JavaScript运行机制:
┌─────────────────────────────────────────────────────┐
│ 调用栈(Call Stack) │
│ 同步代码在这里执行 │
│ ┌─────────────────────────────────────────────────┐ │
│ │ console.log(1) ← 正在执行 │ │
│ │ console.log(3) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
↓
调用栈清空后,事件循环开始工作
↓
┌────────────────────────┐ ┌──────────────────────┐
│ 微任务队列(Microtask) │ │ 宏任务队列(Task) │
│ 优先级: 高 │ │ 优先级: 低 │
├────────────────────────┤ ├──────────────────────┤
│ Promise.then() │ │ setTimeout() │
│ queueMicrotask() │ → │ setInterval() │
│ MutationObserver │ │ I/O操作 │
└────────────────────────┘ └──────────────────────┘
↓ ↓
先清空微任务队列 再执行一个宏任务
↓ ↓
循环往复,直到所有队列清空
console.log(1) // → 调用栈: 立即执行
setTimeout(() =>console.log(2), 0) // → 宏任务队列: 排队等待
Promise.resolve().then(() => { // → 微任务队列: 排队等待
console.log(4)
})
console.log(3) // → 调用栈: 立即执行
// 执行顺序分析:
// 第1轮:
// 调用栈: console.log(1) → 输出 1
// 调用栈: setTimeout → 回调放入【宏任务队列】
// 调用栈: Promise.then → 回调放入【微任务队列】
// 调用栈: console.log(3) → 输出 3
// 第2轮:
// 调用栈清空 → 事件循环检查
// 微任务队列有任务 → 执行 console.log(4) → 输出 4
// 微任务队列清空
// 第3轮:
// 宏任务队列有任务 → 执行 console.log(2) → 输出 2
// 最终输出: 1, 3, 4, 2
这个系统解释了React的很多"奇怪"行为:
function Counter() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
// 为什么count只增加了1,而不是3?
console.log(count) // 还是0???
}
return<div onClick={handleClick}>{count}</div>
}
// 原因: React利用事件循环批量更新
// 1. 三次setCount都放入【微任务队列】
// 2. 当前函数执行完毕(调用栈清空)
// 3. 事件循环处理微任务队列,React批量处理:
// - 合并成一次更新
// - count最终只+1
理解事件循环后,这些都不再神秘:
// async/await的执行顺序
asyncfunction foo() {
console.log('A')
awaitPromise.resolve()
console.log('B') // 这是微任务
}
foo()
console.log('C')
// 输出: A, C, B
// 因为await后面的代码进入微任务队列
// requestAnimationFrame的时机
requestAnimationFrame(() => {
console.log('动画帧') // 在重绘之前执行
})
setTimeout(() => {
console.log('宏任务') // 在事件循环的宏任务阶段
}, 0)
// 输出顺序取决于浏览器何时重绘
新手经常写出这样的Bug:
// ❌ 为什么所有按钮点击后都显示5?
for (var i = 0; i < 5; i++) {
const button = document.getElementById(`btn-${i}`)
button.onclick = function() {
alert(i) // 期望: 0,1,2,3,4 实际: 全是5
}
}
背API的答案:"因为var有变量提升,要用let。"
懂系统的答案:"因为闭包捕获的是变量的引用,不是值。理解闭包的本质后,var和let都能用。"
// 闭包 = 函数 + 词法作用域
function outer() {
let count = 0// 这个变量被"关"在闭包里
return {
increment() {
count++ // 通过闭包访问外部变量
return count
},
getCount() {
return count
}
}
}
const counter1 = outer()
const counter2 = outer()
counter1.increment() // 1
counter1.increment() // 2
counter2.increment() // 1 (独立的闭包!)
console.log(counter1.getCount()) // 2
console.log(counter2.getCount()) // 1
// 关键: count变量完全私有,外部无法访问
console.log(count) // ReferenceError: count is not defined
React Hooks的实现原理:
// 简化版useState的实现
function useState(initialValue) {
let state = initialValue // 闭包!
function setState(newValue) {
state = newValue
render() // 触发重新渲染
}
return [state, setState]
}
// 为什么Hooks必须在顶层调用?
// 因为React靠闭包顺序来识别是哪个Hook!
// Hooks调用顺序示意:
function MyComponent() {
const [name, setName] = useState('张三') // 第1个闭包
const [age, setAge] = useState(25) // 第2个闭包
const [city, setCity] = useState('北京') // 第3个闭包
// React内部:
// fiber.hooks = [
// { state: '张三', setState: ... }, // 通过闭包存储
// { state: 25, setState: ... },
// { state: '北京', setState: ... }
// ]
}
Redux的中间件:
// 中间件就是闭包套娃
const logger = store =>next =>action => {
console.log('dispatching', action)
let result = next(action) // 闭包访问next
console.log('next state', store.getState()) // 闭包访问store
return result
}
// 展开看就是:
function logger(store) { // 第1层闭包
returnfunction(next) { // 第2层闭包
returnfunction(action) { // 第3层闭包
// 这里可以访问store, next, action
// 全靠闭包!
}
}
}
// 方案1: 用let创建块级作用域(每次循环都是新的闭包)
for (let i = 0; i < 5; i++) {
button.onclick = function() {
alert(i) // ✅ 正确: 0,1,2,3,4
}
}
// 方案2: 手动创建闭包
for (var i = 0; i < 5; i++) {
button.onclick = (function(index) { // 立即执行函数
returnfunction() {
alert(index) // ✅ 正确: index被闭包捕获
}
})(i) // 传入当前i的值
}
// 方案3: 理解本质后的最优雅写法
for (var i = 0; i < 5; i++) {
((i) => { // 箭头函数创建新作用域
button.onclick = () => alert(i)
})(i)
}
理解闭包这个系统后:
前端开发中,90%的数据结构都是树。
不信?看看这些:
DOM结构:
html
├── head
│ ├── title
│ └── meta
└── body
├── header
│ └── nav
└── main
├── article
└── aside
React组件树:
<App>
├── <Header>
│ ├── <Logo>
│ └── <Nav>
│ └── <Link>
└── <Main>
├── <Sidebar>
└── <Content>
路由树:
/
├── /users
│ ├── /users/:id
│ └── /users/create
└── /posts
├── /posts/:id
└── /posts/create
状态树(Redux):
{
user: {
profile: {...},
settings: {...}
},
posts: {
byId: {...},
allIds: [...]
}
}
React的协调:
// React做的就是树的对比
function reconcile(oldTree, newTree) {
// 1. 深度优先遍历(DFS)
// 2. 逐节点对比
// 3. 收集差异
// 4. 批量更新DOM树
}
事件冒泡:
// 点击按钮 → 事件沿树向上传播
<div onClick={handleDiv}> ← 第3个触发
<button onClick={handleButton}> ← 第1个触发
点击
</button>
</div>
// 实际流程:
button → div → body → html → document
↑
从叶子节点向根节点遍历树
CSS选择器:
/* div > p.highlight = 在DOM树中查找 */
/* 1. 找到所有div节点 */
/* 2. 检查它们的直接子节点 */
/* 3. 筛选出class="highlight"的p元素 */
div > p.highlight {
color: red;
}
// 深度优先(DFS) - React用这个!
function dfs(node) {
console.log(node.value) // 先处理当前节点
node.children.forEach(child => {
dfs(child) // 递归处理子节点
})
}
// 执行顺序示意:
// A
// / \
// B C
// / \ /
// D E F
//
// DFS顺序: A → B → D → E → C → F
// 广度优先(BFS) - 某些算法用这个
function bfs(root) {
const queue = [root]
while (queue.length > 0) {
const node = queue.shift() // 取出队首
console.log(node.value)
queue.push(...node.children) // 子节点入队
}
}
// BFS顺序: A → B → C → D → E → F
// 虚拟DOM就是树结构
function createElement(type, props, ...children) {
return { type, props, children }
}
// 使用
const vdom = createElement('div', { id: 'app' },
createElement('h1', null, '标题'),
createElement('p', null, '内容')
)
// 结果:
// {
// type: 'div',
// props: { id: 'app' },
// children: [
// { type: 'h1', children: ['标题'] },
// { type: 'p', children: ['内容'] }
// ]
// }
// 渲染到真实DOM(树遍历)
function render(vnode) {
if (typeof vnode === 'string') {
returndocument.createTextNode(vnode)
}
const el = document.createElement(vnode.type)
// 设置属性
if (vnode.props) {
Object.entries(vnode.props).forEach(([key, value]) => {
el.setAttribute(key, value)
})
}
// 递归渲染子节点(树遍历!)
vnode.children.forEach(child => {
el.appendChild(render(child))
})
return el
}
理解树这个系统后:
这是函数式编程的核心思想,也是现代框架的设计基础。
什么是纯函数?
// ✅ 纯函数:相同输入永远返回相同输出
function add(a, b) {
return a + b
}
add(2, 3) // 5
add(2, 3) // 5 (100%确定)
// ❌ 副作用:结果不可预测
function fetchUser(id) {
return fetch(`/api/users/${id}`)
}
fetchUser(1) // 可能成功
fetchUser(1) // 可能失败(网络断了)
fetchUser(1) // 可能返回不同数据(用户更新了信息)
场景:React组件渲染
// ❌ 错误:在渲染中直接副作用
function UserProfile({ userId }) {
const user = fetch(`/api/users/${userId}`).then(r => r.json())
return <div>{user.name}</div>
}
// 问题:
// 1. 每次渲染都fetch → 无限请求!
// 2. fetch是异步的 → user是Promise,不是数据
// 3. 可能触发多次渲染 → 性能爆炸
React的解决方案:useEffect隔离副作用
// ✅ 正确:副作用和渲染分离
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
// 渲染逻辑(纯函数)
// 只管展示,不管获取
// 副作用(隔离)
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(data => setUser(data))
}, [userId]) // 只在userId变化时执行
if (!user) return<div>加载中...</div>
return<div>{user.name}</div>
}
// 好处:
// ✅ 渲染可预测(纯函数)
// ✅ 副作用可控(只在特定时机执行)
// ✅ 测试简单(可以分别测试渲染和副作用)
// Vue: onMounted
import { ref, onMounted } from'vue'
function useUser(userId) {
const user = ref(null)
onMounted(() => { // 副作用隔离
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(data => user.value = data)
})
return { user }
}
// Svelte: onMount
<script>
import { onMount } from'svelte'
let user
onMount(async () => { // 副作用隔离
const res = await fetch('/api/user')
user = await res.json()
})
</script>
// Solid.js: createEffect
import { createSignal, createEffect } from 'solid-js'
function UserProfile(props) {
const [user, setUser] = createSignal(null)
createEffect(() => { // 副作用隔离
fetch(`/api/users/${props.userId}`)
.then(r => r.json())
.then(data => setUser(data))
})
return <div>{user()?.name}</div>
}
同样的模式:
理解这个系统后:
看到这里,你可能会问:"道理我都懂,但怎么才能真正掌握系统思维?"
答案:刻意练习。
遇到新知识,问三次为什么:
例子:学习React.memo
// 第1次: 这是什么?
React.memo是一个高阶组件
// 第2次: 为什么需要它?
为了避免不必要的重新渲染
// 第3次: 为什么重新渲染是问题?为什么memo能解决?
因为渲染是有成本的(对比虚拟DOM、更新真实DOM)
memo通过浅比较props,如果props没变,跳过渲染
这利用了"缓存"和"记忆化"的系统思想
// 继续追问: 为什么是浅比较?深比较不行吗?
深比较也要遍历对象,成本高
框架做权衡:浅比较性能好,够用了
开发者如果需要深比较,自己写比较函数
// 现在你理解的不是API,而是系统!
不要只学一个框架,至少学三个:
第1遍: React - 学会怎么用
第2遍: Vue - 发现有些地方不一样,有些一样
第3遍: Svelte - 开始看出模式
模式:
- 都有组件概念
- 都要管理状态
- 都要处理副作用
- 都要协调更新
- 只是实现方式不同
系统思维建立!
实战对比表:
系统 | React | Vue | Svelte |
|---|---|---|---|
状态管理 | useState (Hook) | ref (Proxy) | $ (编译) |
副作用 | useEffect | watchEffect | $: (编译) |
协调 | 虚拟DOM | 响应式追踪 | 编译优化 |
理念 | 显式>隐式 | 平衡 | 编译时>运行时 |
想理解Redux?写个20行的mini-Redux:
// 完整可运行的mini-Redux
function createStore(reducer, initialState) {
let state = initialState
const listeners = []
return {
// 获取状态
getState: () => state,
// 派发action
dispatch: (action) => {
state = reducer(state, action) // 纯函数!
listeners.forEach(fn => fn()) // 通知观察者
},
// 订阅变化
subscribe: (listener) => {
listeners.push(listener)
return() => { // 返回取消订阅函数
const index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}
}
}
// 测试
const reducer = (state = 0, action) => {
if (action.type === 'INCREMENT') return state + 1
if (action.type === 'DECREMENT') return state - 1
return state
}
const store = createStore(reducer, 0)
store.subscribe(() => {
console.log('State changed:', store.getState())
})
store.dispatch({ type: 'INCREMENT' }) // State changed: 1
store.dispatch({ type: 'INCREMENT' }) // State changed: 2
store.dispatch({ type: 'DECREMENT' }) // State changed: 1
// 现在你理解了:
// - 单一数据源
// - 纯函数reducer
// - 观察者模式
// 这就是Redux的全部核心!
想理解useState?写个简化版:
// React的useState原理(超级简化版)
let state
let stateIndex = 0
const stateArray = []
function useState(initialValue) {
const currentIndex = stateIndex
// 初始化或获取已有状态
if (stateArray[currentIndex] === undefined) {
stateArray[currentIndex] = initialValue
}
const setState = (newValue) => {
stateArray[currentIndex] = newValue
render() // 触发重新渲染
}
stateIndex++ // 为下一个useState准备索引
return [stateArray[currentIndex], setState]
}
function Component() {
stateIndex = 0// 重置索引
const [count, setCount] = useState(0) // stateArray[0]
const [name, setName] = useState('张三') // stateArray[1]
console.log(count, name)
}
// 现在你理解了为什么Hooks不能在条件语句中:
// 因为它依赖调用顺序!
强制自己画出系统的流程:
useEffect的执行时机:
组件首次渲染
↓
执行JSX,生成虚拟DOM
↓
提交到真实DOM
↓
浏览器绘制页面 ← 用户看到界面
↓
执行useEffect回调 ← 在这里!
↓
(依赖项变化)
↓
清理函数执行(如果有)
↓
重新执行effect
这就是为什么叫"副作用":
它在主流程(渲染)之外!
费曼技巧:
小白问: "useEffect到底是干嘛的?"
差的回答:
"useEffect是React的Hook,用来处理副作用,
在组件渲染后执行,可以用依赖数组控制执行时机..."
→ 小白: ???
好的回答:
"你知道电饭煲吗?
按下煮饭键 = 渲染组件
米饭煮好 = 页面显示出来
然后'嘀嘀嘀'提示你 = useEffect执行
useEffect就是这个'提示音':
在主要工作(煮饭/渲染)完成后,
再做一些额外的事(提示/获取数据)。"
→ 小白: 懂了!
如果你能这样解释,说明你真的理解了系统。
2年前的我:
现在的我:
为什么?
// Solid.js的Signal
const [count, setCount] = createSignal(0)
// 我的思考过程(2秒):
// 1. Signal = Vue的ref = 观察者模式
// 2. 响应式基于Proxy? 不,基于getter/setter
// 3. 比React更细粒度? 对,避免了组件重新渲染
// 4. 好,已经懂了80%
// 然后快速验证我的猜想
console.log(count()) // getter - 果然!
setCount(1) // setter - 符合预期!
语法思维debug:
报错: Cannot read property 'map' of undefined
试错流程:
1. 加个 data && data.map? 不行
2. 加个 data?.map? 还不行
3. Google搜索错误信息
4. 试了10个Stack Overflow答案
5. 2小时过去了...
系统思维debug:
报错: Cannot read property 'map' of undefined
系统分析(30秒):
1. data是undefined → 数据未加载
2. 检查数据流系统:
- 异步请求发出了吗? ✓
- 请求成功了吗? ✗ 失败了
- 为什么失败? API地址错了
3. 修复API地址 → 问题解决
总耗时: 1分钟
语法思维做决策:
问题: 状态管理用什么?
拍脑袋:
"Redux很火,用Redux!"
"同事说Zustand好,用Zustand!"
"看这篇文章说Context够了,用Context!"
结果: 可能对,可能不对,全凭运气
系统思维做决策:
问题: 状态管理用什么?
系统分析:
1. 项目规模?
- 小项目(<10个组件) → Context够用
- 中项目(10-50个组件) → Zustand/Jotai
- 大项目(50+个组件) → Redux Toolkit
2. 更新频率?
- 低频(用户设置) → Context
- 高频(实时数据) → 需要优化的方案
3. 团队熟悉度?
- 新手团队 → Zustand(简单)
- 有Redux经验 → Redux Toolkit
4. 类型安全要求?
- 高要求 → Zustand + TypeScript
结论: 我们是中型项目+TypeScript → 选Zustand
2周后验证: 完美!
场景: 新项目要用Vue,但我只会React
我的做法:
1. 看了20个"Vue入门教程"
2. 背住了v-if、v-for、v-model...
3. 花了2个月"转型"
4. 写代码还是心虚,不知道为什么这么写
结果: 能干活,但一直觉得Vue"好陌生"
场景: 新项目要用Svelte,我从没用过
我的做法:
1. 花2小时看官方文档
2. 问自己: 它如何实现这7个系统?
- 状态? $ + 编译时追踪
- 组件? .svelte文件 = 边界
- 副作用? onMount + $:
- 协调? 编译时优化,无运行时
- 异步? 还是事件循环
- 上下文? 还是闭包
- 树? 还是组件树
3. 写了3个示例验证我的理解
4. 当天下午已经能正常开发
结果: 2小时上手,第2天就能给新人讲Svelte了
区别在哪?
我不再学"Svelte语法",而是学"Svelte如何实现那7个系统"。
框架换了,系统没换。
会背API的人:
会系统思维的人:
哪种人更有价值?
答案不言而喻。
选择一个你常用但不真正理解的API:
例如: useEffect、ref、v-model、$:...
执行三次"为什么":
1. 为什么这个API存在?
2. 为什么要这样设计?
3. 为什么这是最好的解决方案?
画出它的执行流程
写一个20行的简化实现
用最简单的话解释给朋友听
选择一个你不会的框架,但用系统思维学习:
第1步: 不要看教程!先问自己:
- 它如何管理状态?
- 它如何组织组件?
- 它如何处理副作用?
- 它如何优化更新?
第2步: 带着问题看文档
- 验证你的猜想
- 找到它独特的系统创新
第3步: 写3个demo
- 状态管理demo
- 副作用处理demo
- 性能优化demo
目标: 3天内上手,7天内能教别人
深入一个你感兴趣的系统:
选项1: 深入事件循环
- 手写Promise
- 理解async/await
- 掌握微任务/宏任务
选项2: 深入虚拟DOM
- 手写mini React
- 理解diff算法
- 优化协调性能
选项3: 深入响应式系统
- 手写mini Vue
- 理解Proxy原理
- 实现依赖追踪
结果: 成为这个领域的专家,能做技术分享
如果这篇文章让你有所收获,请做三件事:
记住:
语法会过时,框架会更新, 但系统是永恒的。 掌握系统, 你就掌握了所有框架。
2026年,我们不做"背框架"的码农,
我们做"懂系统"的工程师。
系统思维,从今天开始。
如果这篇文章对你有帮助,请点赞👍、分享🔄,让更多开发者看到!
关注《前端达人》,获取更多前端深度技术文章! 🚀