
我想问你一个问题,认真想一想再回答:
你上次写 useEffect,你知道为什么要在里面 return 一个函数吗?
不是「大概知道」,不是「好像是清理用的」,而是真的能说清楚——不写会发生什么,什么情况下会出问题,出了问题会报什么错。
如果你能说清楚,这篇文章你可以快速扫一眼。
如果你的第一反应是「这个我用过,但让我解释……等我想想」——那我们聊聊。
先说清楚:这不是在批评谁。
我们这一代学前端的人,赶上了一个神奇的时间窗口——AI 工具爆发的时候,刚好是很多人入门的时候。用 Claude、Cursor、Copilot 写代码,比查文档快十倍,比翻 StackOverflow 快二十倍。
这本来是一件好事。
但它悄悄带来了一个副作用:你开始习惯拿到能跑的代码,而不是习惯理解代码为什么能跑。
这两件事,差距比你想象的大。
做一个简单的需求:页面加载完,调接口拿数据,显示出来。
AI 给的代码大概是这样:
import { useState, useEffect } from'react'
function UserList() {
const [users, setUsers] = useState([])
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => setUsers(data))
}, [])
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
)
}
代码跑通了。页面有数据了。你提交了。
但我问你几个问题——
useEffect 后面那个空数组 [] 是干嘛的?如果不传,会有什么后果?如果里面传了一个变量,行为又有什么不同?
useState([]) 为什么初始值是空数组,而不是 null 或者 undefined?
setUsers(data) 调用之后,页面是立刻重新渲染,还是等一会儿?
这些问题,你能流畅回答几个?
我不是要考你,而是想说明一件事:如果你只是把 AI 的代码复制过来改改,这几个问题你很可能回答不上来。因为 AI 给你的是结果,不是过程。它不会告诉你「我为什么要写这个 []」,它只会给你一份能跑的代码。
useEffect 这件事,值得认真说一次很多初学者把 useEffect 理解成「页面加载完执行一次的地方」,然后就这么用了。
这个理解没有大错,但只说对了一小部分。
真正的理解是这样的:
React 的渲染模型
用户操作 / 数据变化
│
▼
React 重新渲染组件(函数重新执行)
│
▼
更新 DOM
│
▼
执行 useEffect(在 DOM 更新之后)
│
├─ 依赖数组 [] → 只在挂载时执行一次
├─ 依赖数组 [count] → count 变化时重新执行
└─ 没有依赖数组 → 每次渲染后都执行
这个流程,是 React 运转的核心逻辑。
如果你理解这个图,你就能回答为什么 [] 意味着只执行一次——因为没有任何依赖会变化,所以不会重新触发。
如果你不理解这个流程,当某天接口被重复调用了,或者数据更新不符合预期,你会不知道从哪里入手。
AI 给了你能跑的代码,但没有给你这张图。
我说一件我自己的事。
前段时间我在做一个音频功能,需要记录用户听过哪些时间段。后端存的是一个区间数组:
[[0, 19], [29, 65], [80, 120]]
每次用户新听了一段,我要把这段合并进去,去掉重叠,存一份干净的结果。
我当时的第一反应——是打开 AI 对话框,准备把这个需求粘进去。
然后我停了下来。
我拿出纸,画了一条数线:
已有: [0──19] [29──────65] [80──────120]
新来: [15──────35]
分析每个已有区间:
[0,19] 和 [15,35]: 19 > 15,重叠 → 合并 → [0,35]
[29,65] 和 [15,35]: 35 > 29,重叠 → 继续合并 → [0,65] ← 这里容易漏掉!
[80,120] 和 [15,65]: 65 < 80,不重叠 → 保留
结果: [0──────65] [80──────120]
手推的时候发现一个我一开始没想到的边界:新区间不只是和一个已有区间重叠,可能和多个区间都有重叠,要一路合并下去。
如果直接用 AI,它会给我正确答案——但我不会经历发现这个边界的过程。下次遇到类似的多段合并场景,我还是不知道要注意这一点。
我花了三十分钟,最后写出来的代码:
function mergeRanges(existing, newRange) {
const [newStart, newEnd] = newRange
let start = newStart
let end = newEnd
const others = []
for (const [s, e] of existing) {
// 完全不重叠:新区间在左边,或者在右边
if (e < start || s > end) {
others.push([s, e])
} else {
// 重叠:扩展边界
start = Math.min(start, s)
end = Math.max(end, e)
}
}
others.push([start, end])
return others.sort((a, b) => a[0] - b[0])
}
测试时还是漏了一个 edge case,改了一次。
但这段代码我能解释每一行。不是因为我记住了,是因为我想出来的。
async/await,另一个「会用但不懂」的重灾区问你一个问题:下面这段代码,console.log 的输出顺序是什么?
console.log('1')
setTimeout(() => {
console.log('2')
}, 0)
Promise.resolve().then(() => {
console.log('3')
})
console.log('4')
如果你的第一反应是「1 4 2 3」或者「1 2 3 4」,说明你对 JavaScript 的执行机制还没有真正理解。
正确答案是 1 → 4 → 3 → 2。
为什么?因为 JS 的事件循环分三层:
JavaScript 执行顺序
同步代码(主线程)
│ 最先执行
▼
微任务队列(Microtask)
│ Promise.then / queueMicrotask 在这里
│ 同步代码执行完立刻清空
▼
宏任务队列(Macrotask)
│ setTimeout / setInterval / DOM 事件 在这里
│ 每次事件循环取一个执行
▼
(循环回去继续)
所以:console.log('1') 和 console.log('4') 是同步代码,先跑。Promise.then 是微任务,同步结束后立刻跑,所以 3 在 2 前面。setTimeout 是宏任务,最后跑。
这张图理解了,你就能看懂为什么 async/await 的等待不会卡死页面,为什么某些异步操作的结果拿到得比你预期的晚,为什么接口请求的顺序偶尔会乱。
用 AI 写 async 代码很容易。但这些问题出现的时候,AI 不会告诉你为什么。
我打个比方。
你去学炒菜。老师给你写好了菜谱:几勺盐、几勺酱油、火候多大、翻炒几下。你照着做,菜端上桌,不难吃。
这是 AI 给代码的方式。
但如果你只会照菜谱,某天菜谱里没有这道菜——你就不会做了。更麻烦的是,某天菜太咸了,你不知道是盐放多了还是酱油太多,因为你从来没有真正理解「咸度是怎么来的」。
真正的厨师能做到:给我几样食材,我告诉你能做什么菜;这道菜少了某个调料,我知道用什么替代;出了问题,我能找到问题在哪一步。
这种能力,不是照着菜谱做出来的。是经历了很多次「这道菜为什么不对」之后磨出来的。
写代码也一样。
能用 useEffect 发请求,不等于理解了 React 的渲染流程。能用 async/await 等待数据,不等于理解了 JS 的事件循环。能用 AI 生成一个完整页面,不等于理解了状态和副作用是怎么协作的。
差距在调试的时候才会暴露。那个时候 AI 给你的不是答案,是几个猜测——真正能判断哪个猜测是对的,需要你自己的基础理解。
我们团队有个刚入职的同学,能用 AI 很快写出一个功能完整的 React 表单:输入验证、提交、错误提示、加载状态全有。
有一天这个表单出 bug 了:某个输入框的内容,在提交之后不会清空。
他把 bug 描述给 AI,AI 给了三个可能的原因,他逐一试,花了一个小时,没找到。
我看了一眼代码,发现问题:他在 onSubmit 里直接 setFormData({}) 清空了 state,但在同一个函数里紧接着又读了 formData 的值——而在 React 里,state 更新不是立即生效的,同一次渲染里你读到的还是旧值。
这个 bug,如果他理解 React 的 state 更新机制,两秒钟就能看出来。因为他没有这个理解,AI 给他的几个猜测他也分辨不了哪个是正确方向。
代码是 AI 写的,但出了问题,你得自己懂。
这就是现实。
我观察过身边几个技术强的同事,他们不是不用 AI——他们每天都在用,用得比一般人还多。
但区别在于,他们在打开 AI 之前,会先在草稿上写几行:
写完再去问。
这样问出来的问题质量完全不一样——AI 给的方案,他们能立刻判断对不对、有没有考虑某个边界、用了什么原理。遇到 AI 答错了,他们也能发现。
用 AI 之前先想,和直接把问题扔给 AI,两种用法,两种成长速度。
如果你现在刚开始学前端,我特别想说这几件事:
先搞懂 useState 真正在做什么,再用 AI 帮你写状态逻辑。
它不是一个「存变量的地方」,它触发的是整个组件的重新渲染。你改了 state,React 会重新执行你的整个组件函数,重新生成 JSX,再去更新 DOM 里真正需要改的地方。理解这个,很多奇怪的 bug 就能看懂了。
先理解 useEffect 是什么时机执行,再用它调接口。
它不是「页面加载的地方」,它是「渲染完成之后的副作用执行器」。依赖数组控制的是「什么情况下重新执行」,而不是「执行几次」。
先弄清楚 Promise 是什么,再用 async/await。
async/await 只是 Promise 的语法糖,让异步代码看起来像同步代码。理解了 Promise 的状态(pending、fulfilled、rejected),你才能真正看懂 .catch() 在做什么,.finally() 什么时候会跑。
这些不是要你死记硬背,而是要你真正用过、踩过坑、想明白。
这个过程,AI 可以帮你,但不能替你经历。
有两种「会用」:
第一种:你能让代码跑起来。
第二种:你知道它为什么能跑起来,以及——当它跑不起来的时候,你知道为什么。
第一种,现在的 AI 工具已经能给任何人了。
第二种,只能靠你自己积累。
两种「会用」的差距
AI 能帮你到这里
│
▼
┌─────────────────┐
│ 代码能运行 │
└─────────────────┘
│
这段路要自己走
│
▼
┌─────────────────┐
│ 理解为什么能运行 │
└─────────────────┘
│
▼
┌─────────────────┐
│ 出问题能找到原因 │
└─────────────────┘
│
▼
┌─────────────────┐
│ 能设计出新的方案 │
└─────────────────┘
第二、三、四层,是你真正的技术能力。AI 帮你到第一层,剩下的在你自己身上。
那道区间合并题,我手推了三十分钟,最后写出来的代码,我现在还记得思路。
如果当时问了 AI,代码早就提交了,我现在什么都不记得。
AI 让你跑得快,但如果你一直跑在别人给你铺的路上,你自己找路的能力会慢慢退化。
这不是 AI 的错,是我们用它的方式的问题。
很多人担心 AI 会替代程序员。我觉得更现实的风险是另一个:AI 把「写出能跑的代码」这件事变简单了,但调试、架构、判断——这些事还是得靠人。能做这些事的人,价值只会越来越高;只会让 AI 跑代码的人,才真的会被替代。
从现在开始,每隔一段时间,给自己一道题,关掉 AI,自己做一遍。
不是为了证明什么,是为了确认:你的思考能力,还在。