
我想问你一个问题,认真想一想再回答:
你上次写 useEffect,你知道为什么要在里面 return 一个函数吗?
不是「大概知道」,不是「好像是清理用的」,而是真的能说清楚——不写会发生什么,什么情况下会出问题,出了问题会报什么错。
如果你能说清楚,这篇文章你可以快速扫一眼。
如果你的第一反应是「这个我用过,但让我解释……等我想想」——那我们聊聊。
这不是你的问题,是一种新的学习方式带来的副作用
先说清楚:这不是在批评谁。
我们这一代学前端的人,赶上了一个神奇的时间窗口——AI 工具爆发的时候,刚好是很多人入门的时候。用 Claude、Cursor、Copilot 写代码,比查文档快十倍,比翻 StackOverflow 快二十倍。
这本来是一件好事。
但它悄悄带来了一个副作用:你开始习惯拿到能跑的代码,而不是习惯理解代码为什么能跑。
这两件事,差距比你想象的大。
先来看一个你肯定写过的代码
做一个简单的需求:页面加载完,调接口拿数据,显示出来。
AI 给的代码大概是这样:
import { useState, useEffect } from'react'functionUserList() {const [users, setUsers] = useState([]) useEffect(() => { fetch('/api/users') .then(res => res.json()) .then(data => setUsers(data)) }, [])return (<ul> {users.map(user => <likey={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,它会给我正确答案——但我不会经历发现这个边界的过程。下次遇到类似的多段合并场景,我还是不知道要注意这一点。
我花了三十分钟,最后写出来的代码:
functionmergeRanges(existing, newRange) {const [newStart, newEnd] = newRangelet start = newStartlet end = newEndconst 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 之前先想,和直接把问题扔给 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,自己做一遍。
不是为了证明什么,是为了确认:你的思考能力,还在。
夜雨聆风