前端代码评审清单
· 阅读需 7 分钟
代码评审最怕两种情况:只看风格,不看行为;或者问题太散,最后谁都记不住。
我自己做前端 review 时,会优先看四件事:
- 有没有行为回归
- 状态流是不是清晰
- 边界条件有没有处理
- 命名和拆分是否真的降低理解成本
样式和格式当然也看,但通常不会放在最前面。真正影响线上质量的,还是行为和边界。
1. 行为回归检查
❌ 容易出问题的改动
// 修改前:点击按钮触发提交
function SubmitButton({ onSubmit }) {
return <button onClick={onSubmit}>提交</button>;
}
// 修改后:加了防抖,但改变了原有行为
function SubmitButton({ onSubmit }) {
const debouncedSubmit = useMemo(
() => debounce(onSubmit, 500),
[] // ❌ 依赖丢失,onSubmit 变化时不会更新
);
return <button onClick={debouncedSubmit}>提交</button>;
}
问题:
- 原来立即执行,现在有 500ms 延迟
- 依赖数组为空,导致闭包陷阱
- 用户快速点击时,只有最后一次生效
✅ 正确的做法
// 方案 1:明确告知这是行为变更,补充测试
function SubmitButton({ onSubmit }) {
const debouncedSubmit = useMemo(
() => debounce(onSubmit, 500),
[onSubmit] // ✅ 依赖正确
);
useEffect(() => {
return () => debouncedSubmit.cancel(); // ✅ 清理防抖
}, [debouncedSubmit]);
return <button onClick={debouncedSubmit}>提交</button>;
}
// 方案 2:不改原有逻辑,在外层控制
function FormContainer() {
const handleSubmit = useCallback(
debounce((data) => {
submitToServer(data);
}, 500),
[]
);
return <SubmitButton onSubmit={handleSubmit} />;
}
Review 要问的问题:
- 这个改动会影响现有交互吗?
- 有没有对应的测试用例?
- 边界情况(快速点击、组件卸载)处理了吗?
2. 状态流清晰度
❌ 状态流混乱
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [isEditing, setIsEditing] = useState(false);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
// ❌ 状态更新分散,难以追踪
const handleSave = async () => {
setLoading(true);
setError(null);
try {
const result = await saveUser(user);
setUser(result);
setIsEditing(false);
setHasUnsavedChanges(false);
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
};
// ❌ 状态可能不一致:loading=true 但 error 还有值
}
问题:
- 5 个独立状态,组合爆炸(2^5 = 32 种状态)
- 容易出现不合法状态:
loading=true+error="xxx" - 状态更新散落各处,维护困难
✅ 用状态机或 reducer 收敛
// 方案 1:状态机(推荐)
const STATES = {
IDLE: 'idle',
LOADING: 'loading',
EDITING: 'editing',
SAVING: 'saving',
ERROR: 'error'
};
function UserProfile() {
const [state, setState] = useState({
status: STATES.IDLE,
user: null,
error: null,
unsavedData: null
});
const handleSave = async () => {
setState(s => ({ ...s, status: STATES.SAVING, error: null }));
try {
const result = await saveUser(state.unsavedData);
setState({
status: STATES.IDLE,
user: result,
error: null,
unsavedData: null
});
} catch (e) {
setState(s => ({
...s,
status: STATES.ERROR,
error: e.message
}));
}
};
// ✅ 状态清晰,不会出现 loading + error 的矛盾状态
const isLoading = state.status === STATES.LOADING || state.status === STATES.SAVING;
const canEdit = state.status === STATES.IDLE || state.status === STATES.EDITING;
}
// 方案 2:useReducer
function reducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, status: 'loading', error: null };
case 'FETCH_SUCCESS':
return { status: 'idle', user: action.payload, error: null };
case 'FETCH_ERROR':
return { ...state, status: 'error', error: action.error };
case 'START_EDIT':
return { ...state, status: 'editing', unsavedData: state.user };
default:
return state;
}
}
function UserProfile() {
const [state, dispatch] = useReducer(reducer, {
status: 'idle',
user: null,
error: null,
unsavedData: null
});
// ✅ 所有状态变更都经过 reducer,易于追踪
}
Review 要问的问题:
- 这些状态会同时为真吗?
- 有没有不合法的状态组合?
- 状态更新逻辑是否集中?
3. 边界条件处理
❌ 缺少边界处理
function UserList({ users }) {
return (
<ul>
{users.map(user => ( // ❌ users 可能是 null/undefined
<li key={user.id}>{user.name}</li> // ❌ user.name 可能不存在
))}
</ul>
);
}
function SearchInput({ onSearch }) {
const [query, setQuery] = useState('');
const handleSearch = () => {
onSearch(query.trim()); // ❌ onSearch 可能是 undefined
};
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)} // ❌ e.target.value 空格处理?
/>
);
}
✅ 完善的边界处理
function UserList({ users = [] }) { // ✅ 默认值
if (!users.length) {
return <EmptyState />; // ✅ 空状态
}
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name || '匿名用户'} {/* ✅ 兜底文案 */}
</li>
))}
</ul>
);
}
function SearchInput({ onSearch = () => {} }) { // ✅ 默认空函数
const [query, setQuery] = useState('');
const handleSearch = () => {
const trimmed = query.trim();
if (!trimmed) { // ✅ 空值校验
return;
}
if (trimmed.length > 100) { // ✅ 长度限制
showError('搜索词不能超过100字');
return;
}
onSearch(trimmed);
};
const handleChange = (e) => {
const value = e.target.value;
// ✅ 防止粘贴大量空格
if (value.length - value.trim().length > 10) {
setQuery(value.trim());
return;
}
setQuery(value);
};
return (
<input
value={query}
onChange={handleChange}
maxLength={100} // ✅ 前端限制
/>
);
}
常见边界条件清单
// ✅ 数据边界
- 空数组:[]
- null/undefined
- 空字符串:""
- 超长文本:>1000字符
- 特殊字符:emoji、HTML标签
// ✅ 交互边界
- 快速连续点击
- 网络慢/失败
- 组件卸载时还有异步操作
- 权限不足
- 登录过期
// ✅ 时间边界
- 请求超时
- Token 过期
- 倒计时结束
- 定时器清理
Review 要问的问题:
- 数据为空时会怎样?
- 用户快速操作会有问题吗?
- 网络失败的错误提示在哪?
4. 命名和拆分
❌ 难以理解的命名
// ❌ 函数名不表意
function handle() {
// 处理什么?
}
function process(data) {
// 处理成什么?
}
// ❌ 布尔值命名不清晰
const flag = true; // 什么标志?
const status = false; // status 不应该是布尔值
const data = []; // 什么数据?
// ❌ 缩写过度
function calcUsrCntByDpt(d) {
// 谁能看懂?
}
✅ 清晰的命名
// ✅ 函数名清晰表达意图
function handleSubmitButtonClick() {
// 一看就知道是提交按钮点击
}
function transformUserDataToTableRows(users) {
// 输入输出都很明确
}
// ✅ 布尔值用 is/has/should 开头
const isLoading = true;
const hasError = false;
const shouldShowModal = true;
// ✅ 数组/对象名称体现内容
const userList = [];
const userIdToNameMap = {};
const pendingOrderIds = new Set();
// ✅ 不过度缩写
function calculateUserCountByDepartment(department) {
// 多几个字符,但清晰很多
}
❌ 函数过长,职责不清
function UserProfile() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [comments, setComments] = useState([]);
useEffect(() => {
// ❌ 一个 useEffect 做了太多事
fetch('/api/user')
.then(res => res.json())
.then(data => {
setUser(data);
return fetch(`/api/posts?userId=${data.id}`);
})
.then(res => res.json())
.then(posts => {
setPosts(posts);
return Promise.all(
posts.map(p => fetch(`/api/comments?postId=${p.id}`))
);
})
.then(responses => Promise.all(responses.map(r => r.json())))
.then(comments => setComments(comments.flat()));
}, []);
// ❌ 渲染逻辑和数据处理混在一起
return (
<div>
<h1>{user?.name}</h1>
{posts.map(post => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.content.substring(0, 100)}...</p>
{comments
.filter(c => c.postId === post.id)
.map(comment => (
<div key={comment.id}>{comment.text}</div>
))}
</div>
))}
</div>
);
}
✅ 职责清晰的拆分
// ✅ 数据获取逻辑抽离
function useUserProfile(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser(userId).then(setUser).finally(() => setLoading(false));
}, [userId]);
return { user, loading };
}
function useUserPosts(userId) {
const [posts, setPosts] = useState([]);
useEffect(() => {
if (!userId) return;
fetchPosts(userId).then(setPosts);
}, [userId]);
return posts;
}
// ✅ 组件拆分,职责单一
function PostItem({ post, comments }) {
const postComments = comments.filter(c => c.postId === post.id);
return (
<article>
<h2>{post.title}</h2>
<p>{post.content.substring(0, 100)}...</p>
<CommentList comments={postComments} />
</article>
);
}
function UserProfile({ userId }) {
const { user, loading } = useUserProfile(userId);
const posts = useUserPosts(user?.id);
const comments = useComments(posts);
if (loading) return <Spinner />;
if (!user) return <NotFound />;
return (
<div>
<UserHeader user={user} />
<PostList posts={posts} comments={comments} />
</div>
);
}
Review 要问的问题:
- 这个函数名能让人知道它做什么吗?
- 这个函数是否做了多件事?
- 能否拆成更小的、可复用的部分?
快速 Review 检查表
打印出来,贴在显示器旁边:
行为检查
- 改动是否影响现有功能?
- 有对应的测试用例吗?
- 异步操作有取消/清理吗?
状态检查
- 状态是否有不合法的组合?
- 状态更新逻辑是否集中?
- 派生状态能否用计算属性替代?
边界检查
- 数据为空时会怎样?
- 网络失败有错误提示吗?
- 用户快速操作会出问题吗?
- 有长度/大小限制吗?
命名检查
- 函数名能表达意图吗?
- 布尔值有 is/has 前缀吗?
- 有过度缩写吗?
- 函数是否只做一件事?
总结
代码评审的核心不是挑刺,而是降低线上风险。
优先级:行为 > 边界 > 状态 > 命名 > 格式
记住三个问题:
- 这段代码在什么情况下会出错?
- 三个月后的自己能看懂吗?
- 出了问题能快速定位吗?
格式问题交给 ESLint 和 Prettier,把精力放在真正影响质量的地方。