目录
周五下午四点,你在 PR 里留了一条评论:
这里每次都
JSON.parse,性能太差了,建议用useMemo。
十分钟后,作者回了一句:
数据量很小。
对话结束。代码没改,你有点不爽,作者也觉得你没事找事。
把同一件事换一种说法:
第 42 行在每次 render 时都会重新
JSON.parse(raw)。我担心的是raw较大、列表又频繁重渲染时,解析会落在主线程上;如果raw只在进入页面时变一次,这个担心就不成立。能否确认raw的来源和更新频率?如果它确实每次都变,我建议在数据层解析一次,而不是在组件里 memo。
还是同一个技术判断,还是可能被拒绝。但这一次,作者知道你在担心什么、需要回答什么,也知道拒绝你的条件是什么。
两条评论的差别不在语气,而在信息结构:第一条只交付了结论,第二条交付了一个可验证的判断和一个具体的请求。代码评审里的沟通问题,大多不是「怎么说才显得礼貌」,而是「一句话里到底装了什么」。
一、评审沟通难,不是因为人难相处
先承认一个事实:大多数评审冲突里,双方都不是坏人。
reviewer 看到一处可能出问题的地方,花了三十秒写下一句提醒;作者花了两天写这个 PR,被打断后看到一句「这里有问题」,本能地进入防守。两个人都觉得自己在讲道理,但两个人拿到的信息根本不一样。
这背后有四个结构性失真,和性格关系不大:
| 失真 | 具体表现 | 作者接收到的东西 |
|---|---|---|
| 异步 | 没有语气、表情和即时澄清,文字会被读成最坏的语气 | 「他在否定我」 |
| 公开 | 评论有观众:其他 reviewer、组长、路过的同事 | 「我被当众指出问题了」 |
| 压缩 | 一条评论常常只有一句,省掉了观察、证据和请求 | 「他说我不行,但没说哪里不行」 |
| 只看见 diff | diff 显示改了什么,不显示为什么改、放弃了什么 | 「他根本不知道我考虑过什么」 |
再加一层:代码是作者的作品。被指出问题时,人很难把「这段代码有问题」和「我这个人有问题」完全分开。这不是玻璃心,是投入之后的正常反应。评审沟通的设计目标,不是要求作者「别玻璃心」,而是让评论携带足够的信息,使作者不必靠猜来补全你的意思。
二、先分级:四级评论,四种语法
很多来回其实只卡在一个问题上:这条评论到底要不要改?
reviewer 用「建议」的语气说了一件必须改的事,作者就按「可选」处理;reviewer 用「必须」的语气说了一件个人偏好,作者只好照做,心里不服。语气和后果错位,讨论就会从技术问题滑向态度问题。
一个实用的做法是给评论标级别。级别不是礼貌程度,而是「不采纳会发生什么」:
| 级别 | 它回答的问题 | 不采纳的后果 | 必须携带 | 示例 |
|---|---|---|---|---|
blocking | 这段代码能不能合入? | 正确性、安全、数据丢失、破坏公开契约 | 触发条件、复现步骤或明确的影响面 | 「并发写同一个 key 时,后到的请求会覆盖先到的结果;这里有复现脚本。」 |
question | 我需要先知道什么? | 无法判断,讨论暂停 | 一个具体、能被回答的问题 | 「这个字段在 SSR 之后还会变吗?」 |
suggestion | 有没有更合适的写法? | 可以合入,但维护成本更高 | 收益、代价、适用条件 | 「拆成两个函数后,校验逻辑可以单独测;代价是多一层调用。」 |
nit | 这是不是纯风格偏好? | 不影响合入 | 明确标注 nit,并说明不阻塞 | 「nit: 这个变量叫 data 有点泛,叫 orders 更清楚。」 |
四条规则让这张表真正起作用:
blocking必须能被证伪。 「这里以后会出问题」不是blocking,因为它没有触发条件;「当items为空数组时这里返回undefined,下游.map会抛错」才是。nit不阻塞合入。 如果一条nit卡住了 PR,说明级别标错了,或者评审流程把「作者必须全部照做」当成了默认规则。question不是二等评论。 很多 blocking 是在回答完一个 question 之后才被确认的。先问,再判断,比先下结论更省时间。- 一条评论只装一件事。 把「命名」「空值」「性能」塞进同一段话,作者只能挑一个回,剩下的会被静默忽略。
如果团队用 GitHub、GitLab 这类工具,级别可以直接写进评论开头:blocking:、question:、suggestion:、nit:。它不解决所有问题,但能立刻消掉「这条到底要不要改」的来回。Conventional Comments 这类约定做的就是这件事,规则很简单,贵在团队统一。
三、把断言改成可证伪的提问
分级解决「要不要改」,接下来解决「凭什么改」。
一条低效的评论通常长这样:
这里性能太差了,应该用
useMemo。
它有结论,没有观察,没有触发条件,也没有给作者反驳的入口。作者只能回「数据量很小」或者默默照做,两种情况都没有产生可复用的判断。
把评论拆成四段,信息量会完全不同:
观察:我看到了什么(文件、行号、具体行为)影响:在什么条件下,会导致什么后果假设:我为什么认为会这样(明确标成假设,允许被推翻)请求:我希望你确认或做什么(一个具体动作)套进前面的例子:
观察:第 42 行在每次 render 时都会重新 JSON.parse(raw)。影响:如果 raw 较大、列表又频繁重渲染,解析会落在主线程上,滚动时可能出现掉帧。假设:我猜 raw 在父组件里每次都是新字符串,所以 memo 也救不了;如果 raw 只在进入页面时变一次,这个担心不成立。请求:能否确认 raw 的来源和更新频率?如果确实每次都变,我建议在数据层解析一次。四段里最容易被跳过的是「假设」。跳过它,评论就变成了判决;写出来,它才是一个可以被作者用事实推翻的模型。好的评审评论不是证明自己对了,而是把自己的判断变成可检验的。
3.1 事实用证据,偏好用让渡
评审里最耗时的争论,往往是把偏好说成了事实。
- 「这个函数太长了」是偏好,除非你能指出它导致了什么具体问题:难以测试、修改时容易漏改、两个职责被绑在一起。
- 「应该用
Map而不是对象」是偏好,除非你能给出键的类型、插入顺序或原型链污染的具体条件。 - 「这个命名不好」是偏好,除非团队有命名规范,或者这个名字已经在别处指向了另一个概念。
对偏好类意见,一个有效的句式是「我倾向于 X,因为 Y;如果你选 Z 是因为 W,那也可以,能否在注释或 PR 描述里记一下原因」。它同时做了三件事:表明立场、给出理由、把决定权留给最了解上下文的人。
对事实类意见,反过来:不要用形容词,用数字、复现步骤和日志。把「这里会卡」换成「输入 5 万条时,这段同步解析在低端机上大约需要几百毫秒」,把「可能空指针」换成「当 user 为 null 时,第 18 行会抛 TypeError」。
3.2 改写对照表
下面这些句子在评审里出现频率很高,问题也很稳定:
| 原评论 | 问题 | 改写 |
|---|---|---|
| 「这里性能太差了」 | 没有触发条件,不可证伪 | 「每次 render 都解析;如果 raw 大于 100KB 且频繁重渲染,主线程会有可感知的卡顿。能否确认更新频率?」 |
| 「这个函数太长了」 | 没有指认哪一段多余 | 「这个函数同时做了解析、校验和写库;如果校验要单独测试,建议把第 40–70 行抽成 validateOrder。」 |
| 「为什么不用 X?」 | 反问句,容易被读成质问 | 「我倾向于用 X,因为……;你选 Y 是因为……吗?如果是,能否记一下原因?」 |
| 「这样写不对」 | 没有给失败场景 | 「当 items 为空数组时,这里返回 undefined,下游 .map 会抛错。能否补一个空数组用例?」 |
| 「显然应该……」 | 「显然」在否定对方的判断力 | 「我的理解是……,可能漏了上下文;如果是这样,请纠正我。」 |
| 「你总是把逻辑写在组件里」 | 针对人,且无法执行 | 「这个组件里的解析逻辑能否下沉到数据层?这样测试时不必渲染组件。」 |
改写不是把话说软,而是把可执行的部分补上。语气可以直,信息必须全。
四、作者侧:回应、拒绝与记录
评审沟通的另一半在作者这边。作者最容易犯的两个错误,一个是把每条意见都当成必须执行的指令,另一个是把每条意见都当成对自己的否定。两者都会让讨论失焦。
一个更稳的处理顺序是:先分类,再用事实回应,最后记录决定。
4.1 先分类,再决定花多少力气
收到一批评论后,先扫一遍级别,不要从第一条开始逐条回:
blocking:优先处理,因为它决定能不能合入;有疑问就要求对方给触发条件。question:用事实回答,答完再判断要不要改。suggestion:评估收益和代价,可以接受,也可以拒绝,但要说清理由。nit:批量处理或明确说不改,不要让它占用讨论的主线程。
分类的另一个作用,是防止「评论数量」变成压力。一个 20 条 nit 的 PR 不一定比一个 2 条 blocking 的 PR 问题更多,但前者看起来更吓人。
4.2 回应模板:确认、事实、方案、决定
确认:我理解你的担心是 ___。事实:这里的实际情况是 ___(日志、代码路径、数据来源)。方案:我倾向于 ___,因为 ___;另一个选项是 ___,代价是 ___。决定:这条我 ___(改 / 不改 / 需要你确认 ___)。一个完整的例子:
确认:你担心的是每次 render 都重新解析 raw。事实:raw 来自 SSR,客户端不会重新赋值;组件只在挂载时渲染一次,列表滚动走的是虚拟列表,不会触发父组件重渲染。方案:这种情况下 memo 的收益很小,我倾向于保持现状;如果你担心未来 raw 会变成动态的,我们可以加一条注释,说明这个假设。决定:这条我先不改,等确认 raw 的更新路径后再定。这段回应没有一句「你说得不对」,但把技术判断讲清楚了。作者不需要为了显得配合而改代码,reviewer 也能从事实里更新自己的模型。
4.3 拒绝的语法:给出「什么条件下我会改」
拒绝不是对抗,含糊的拒绝才是。有效的拒绝长这样:
这条我不同意,因为 X。如果 Y 成立,我会改。
例如:
这条我倾向于不改:这个
catch之所以吞掉错误,是因为上层会统一重试,重复上报会产生告警风暴。如果上层不再重试,这里就应该重新抛出。
它把「不」变成了一个有条件的判断。对方如果不同意,只需要攻击条件 Y,而不是猜测你的态度。
反过来,最消耗团队的拒绝是这两种:
- 沉默式拒绝:不改,也不说为什么,等 PR 被搁置。
- 服从式拒绝:照改,但在别处抱怨「评审就是走形式」。代码变了,判断没有变,下一次还会发生。
4.4 不要把「改了多少条」当成服从度
评审的目标不是让作者把每条意见都执行一遍,而是让代码在合入前经过一次独立检查。作者保留判断权,reviewer 保留提出问题的责任,这两件事不冲突。
如果团队把「reviewer 提了多少条、作者改了多少条」当成绩效信号,很快就会出现两种反噬:reviewer 为了显得认真而凑评论,作者为了显得配合而机械照改。两种行为都会让评审失去信息量。
五、reviewer 侧:让意见被接受,而不是被服从
站在 reviewer 的角度,有几件事比「说得客气」更能提高采纳率。
5.1 先读 PR 描述,再读 diff
很多评论之所以跑偏,是因为 reviewer 不知道这个 PR 要解决什么问题。作者可能已经明确说了「这次只做重命名,行为不变」,reviewer 却在评论里要求顺手重构。先读描述和目标,再判断实现,能消掉大量无效评论。
如果 PR 描述写不清目标,那本身就是一条 blocking:不是代码有问题,而是这次改动无法被验证。
5.2 一次只提影响最大的几条
一次评审给出 20 条评论,作者的注意力会被平均分配,真正重要的两条反而被淹没。一个实用的限制是:一次最多提 3 条 blocking,其余写成 suggestion 或留到后续 PR。
这不是放过问题,而是承认评审的带宽有限。把「必须现在改」和「可以以后改」分开,作者才知道先做什么。
5.3 明确说出「哪些地方不用改」
只指出问题,作者会误以为整个 PR 都需要重写。花三十秒写下「数据层这部分我看过了,没问题;我重点担心的是组件里的解析和错误处理」,能显著降低作者的防御感,也能让讨论聚焦在真正的分歧上。
这不是鼓励表演式表扬。它传递的是评审的边界:我看了什么、跳过了什么、哪些判断我暂时没有依据。
5.4 对初级和高级,给的东西不一样
- 对刚入门的作者:给方向和一个具体例子,不要直接重写。直接重写会让作者失去自己动手建立判断的机会。
- 对熟悉的作者:给判断依据,不要给指令。「这里可能有问题,因为……」比「改成 X」更有价值,因为对方有能力自己选方案。
- 对跨模块的改动:先问「你希望我重点看哪一块」,再分配注意力。reviewer 不必假装自己懂所有上下文。
5.5 「我们」有边界
把「你这里错了」换成「我们可以看看这里」,确实能降低对抗,但用过头会让责任变模糊:出了问题,没人知道是谁的决定。
一个简单的区分:讨论方案时用「我们」,确认决定时用「我」和「你」。 「我们一起看看这段并发逻辑」是邀请;「我同意这个方案,你来改」是决定。两者都需要。
六、异步评审的节奏:什么时候该停下来开会
代码评审默认是异步的,但异步有成本:作者在等,reviewer 在切换上下文,讨论在文字里来回。
6.1 diff 越大,发现率越低
业界常引用的评审研究建议,单次评审控制在 200–400 行左右,超过这个规模,缺陷发现率会下降。数字不必当成硬指标,但方向很清楚:大 PR 的评审质量会肉眼可见地变差。
对应的做法不是「让 reviewer 更努力」,而是拆 PR:
- 纯重命名、格式化、依赖升级,单独一个 PR;
- 行为改动和重构分开;
- 一个 PR 只解决一个问题,描述里写清「不包含什么」。
拆 PR 的成本在作者侧,收益在 reviewer 侧和未来的 bisect 上。多数团队低估了后者的价值。
6.2 两种评审,不要混着用
| 模式 | 适合什么 | 产出 |
|---|---|---|
| 同步走查 | 架构调整、跨模块改动、方向不确定 | 一个决定,以及决定背后的理由 |
| 异步评论 | 局部实现、命名、测试补充、文档 | 一组可执行的修改项 |
把架构问题塞进异步评论,通常会来回三轮以上;把命名问题拉去开会,则是浪费所有人的时间。先判断属于哪一种,再选媒介。
6.3 三个升级信号
出现下面任一情况,就应该从文字切换到语音或当面,十五分钟往往能解决三轮评论:
- 同一个问题来回超过三轮,双方都在重复自己的话;
- 出现明显的误解,比如 reviewer 以为作者漏了某个分支,作者以为 reviewer 要求重写;
- 涉及跨团队或架构决策,需要更多人一起拍板。
升级之后,把结论写回 PR。不要让它只存在于会议里,否则下一个看 PR 的人还要重新问一遍。
6.4 把评论变成决策记录
代码评审的一个隐藏产出是决策记录:为什么选这个方案、放弃了什么、谁同意了。没有它,三个月后同样的讨论会再来一次。
不需要长篇大论,PR 描述里加一小段就够:
决策:在数据层解析 raw,组件只接收已解析的 orders。理由:raw 只在进入页面时变化;放在组件里会让解析和渲染耦合,测试时需要渲染组件。替代方案:useMemo(未采用,因为父组件不会重渲染,收益接近零)。拍板:@alice(前端 owner),2026-11-06。这段记录的价值不在于「留痕」,而在于它把一次口头讨论变成了可检索的资产。下次有人问「为什么不用 memo」,答案在 PR 里,而不是在某个人的记忆里。
七、把重复意见沉淀成规则
如果同一条评论在不同 PR 里出现了三次,第四次就不该再写评论。
原因很简单:重复评论说明问题不在某一段代码,而在团队的默认约定或工具链。继续评论,只会让 reviewer 变成人肉 linter,让作者觉得自己总在同一个小坑里摔倒。
三类沉淀方式,按成本从低到高:
7.1 自动化:交给 lint 和类型检查
能在提交前自动发现的问题,不要留到评审:
// .eslintrc.json(片段){ "rules": { "no-floating-promises": "error", "consistent-type-imports": "error", "@typescript-eslint/no-misused-promises": "error" }}// tsconfig.json(片段){ "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true }}规则一旦生效,评审里就不用再解释「为什么 await 不能漏」。reviewer 的注意力可以留给工具判断不了的东西:命名是否表达了意图、抽象是否多了一层、这段逻辑是否应该存在。
7.2 模板:把期待写进 PR 和 issue
如果团队反复要求「PR 要写测试说明」「要写回滚方案」,把它写进 PR 模板,而不是每次口头提醒。模板不是官僚流程,它把隐性的期待变成显性的检查项。
7.3 文档:把约定写进 CONTRIBUTING
评论级别的含义、命名规范、目录结构、错误处理约定,写进 CONTRIBUTING.md 或团队 wiki。评论里附一个链接,比重复解释十遍更省时间。
一个最小的约定可以只有四行:
blocking: 不改不能合入,必须给出触发条件。question: 需要信息才能判断,请用事实回答。suggestion: 可以合入,但建议考虑;作者可以拒绝并说明理由。nit: 纯风格,不阻塞合入。八、两份检查清单
发评论之前
- 我知道这个 PR 要解决什么问题吗?
- 这条是
blocking还是suggestion?我标注了吗? - 我给了触发条件、影响或复现步骤,而不只是形容词?
- 我是在问问题,还是在发指令?
- 这条意见是否已经出现过三次?该不该写进 lint 或文档?
- 我有没有指出哪些地方不用改?
回应之前
- 我把意见按级别分类了吗?
- 我用事实回应了吗,还是只在说「我觉得」?
- 我拒绝时,有没有给出「什么条件下我会改」?
- 这个决定需要记录吗?谁来拍板?
- 我有没有把「被评审」当成对能力的评价?
九、结语:评审的产出不是一句「批准」
代码评审最常见的误解,是把它当成一道门:reviewer 守着,作者想办法通过。门开了,流程结束,代码合入。
但一次好的评审留下的东西不止于此。它留下的是一段共同理解:这段代码为什么这样写、哪些取舍是有意的、哪些地方下次可以做得不同。作者关掉 PR 之后,遇到同类问题时能自己做出判断,而不是再问一次「这里要不要 memo」。
所以,评论的质量不看它多礼貌,而看它是否让下一次不再需要这条评论。把事实、偏好和决策分开说,是朝这个方向走的一小步:事实可以被检验,偏好可以被讨论,决策可以被记录。三者混在一起,才会变成「你错了」和「你不懂」的来回。