时光如梭,一晃 2022 年已经过去 2/3,我们一起迎来 9 月。秋风送爽,丹桂漂亮,下面,我们一起回顾 8 月份我在 code.fun 完成的 Code Review 吧。
关于 Code Review 的介绍和经验,欢迎大家回顾前三篇,本文暂不重复:
- 在 Code.fun 做 Code Review
- 在 Code.fun 做 Code Review(二)
- 在 Code.fun 做 Code Review(三):聊聊 Promise 的错误处理、如何真正学到技术
(更多…)

时光如梭,一晃 2022 年已经过去 2/3,我们一起迎来 9 月。秋风送爽,丹桂漂亮,下面,我们一起回顾 8 月份我在 code.fun 完成的 Code Review 吧。
关于 Code Review 的介绍和经验,欢迎大家回顾前三篇,本文暂不重复:

嗯,不知不知觉这个系列写到第三篇,这一篇会改变一下写法,从一次 Code Review 出发,讲解几个技术点,然后分享一下技术学习的经验,以及 Code Review 的用途。希望对大家有帮助。
顺便推广下前两篇:
某天,我给一位同事做 code review,看到下面这段代码:
const err = new Error('错误信息');
return Promise.reject(err);
于是我就回复:error 最好直接 throw。
然后他回复我:如果改成 throw 的话,这个 catch 好像是捕获不到 throw 的。
这就很诡异了,这不符合 Promise 的设计;而 Promise 不是新生事物,它有很大的测试集可以保证行为符合预期,我觉得我们想遇到问题都很困难。于是我就跟这位同事连线,帮他分析问题。
连线之后,我发现他的真正问题并不是按照我的要求修改代码之后遇到的。由于基础不太牢靠,他先写了一段实验代码,想要验证我的要求,结果这段代码行为出现异常:

他希望能够通过 .catch(err => console.log(err)) 捕获并记录错误,但是从控制台的输出来看,错误却是 Uncaught(未捕获)。那么问题出在哪儿呢?
作为曾经讲解过 Promise 的我当然是一眼就看出来问题所在,但是这位同学却琢磨不透。于是我就把问题发到研发群里,果然又问倒好几位同学。
现在我请问各位读者老爷,你们知道么?或者换个方向,如果想正常使用 .catch() 捕获到错误,应该怎么修改呢?
这里我们必须回到 Promise 的规范,才能了解上面截图的问题所在(关于详细规范,请参阅 MDN Promise,这里只摘我们需要的部分):
new Promise(function (resolve, reject) {}) 会创建一个 Promise 实例,其中的参数应该是一个函数(通常是异步函数),接受两个参数:resolve 和 reject。当异步操作成功之后,应调用 resolve(result) 并传递结果;当异步操作失败,应调用 reject(reason) 并传递错误信息。pending,fulfilled,rejected,起始状态是 pending,变更后,就固定下来,不会再次变更。rejected 状态。fulfilled 的 Promise 实例会转入 .then() 处理;rejected 会转入 .catch() 处理。我们再回头看上面的截图,这里的问题在于,错误是在 setTimeout(异步函数)的回调函数里抛出的,抛出时当前 Promise 所在的执行栈已经结束了,回调函数是全新开启的执行栈,所以 Promise 无法捕获到它里面的异常;而它也没有主动调用 reject(err) 传出错误,所以就变成了 Uncaught(未捕获)。
既然说到执行栈,我们就顺便补充一下执行栈的知识吧。形如以下代码:
function a() {
b();
}
function b() {
c();
}
function c() {
d();
}
function d() {
// do something
}
a();
当函数 a 执行的时候,运行时会开启一个新的执行栈,并且把 a 入栈接下来发现 a 调用了 b,就又会把 b 入栈;然后发现 c……运行时会重复这个过程。当一个函数执行完毕,比如 d,运行时就会把它移出栈,然后继续执行 c 的剩下部分。
对于 JavaScript 而言,错误是可以冒泡的。即在 d 抛出的异常,如果没有被捕获,它就会一直上浮,直到回到全局。所以我们可以在栈内的任何一个环节捕获它;但如果跨栈,那就无法捕获。
异步函数的回调函数会由 Event Loop 开启新栈执行,与所以异步函数自身处于不同的执行栈,所以错误不会被异步函数捕获,自然也不会改变 Promise 的状态。
所以,要正确捕获错误的话,就需要想办法捕获异步函数的回调函数的错误,即:
function yy() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 方案1
reject(new Error('错误信息'));
// 方案2,实际中很少这么写,这里只是用来演示。但要注意,捕获必须放在回调栈,才能捕获到发生的错误。
try {
throw new Error('错误信息');
} catch (e) {
reject(e);
}
}, 1000);
// 实际场景中,更多是这样的,err 作为参数传给回调函数
fs.writeFile(path, content, 'utf8', (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
}
对于上述问题,我们只要掌握 Promise 规范、函数执行栈、Event Loop,就很容易判断。但是如果这三个概念有一个不太清楚,就容易犯迷糊。所以,很多时候,要避免出问题、要保证软件正常工作,我们必须清楚了解每个技术的规范、定义。
比如 Promise、原型链、闭包等等,它们都不是自然界的产物;而是在长期软件开发实践中,由开发者总结设计出来,用于解决特定问题的发明。他们都有严格的定义、功用、优缺点,等等。我们日常开发,应该把这些东西当成知识储备起来,针对需求做排列组合,给出解决方案。
有些同学相反,他们会记下来一些技术的用法,然后反推这些技术的特性。如果对一个技术不熟悉,他们会尝试做类似实际场景的实验,然后再想办法搬到实际场景中。这样做,覆盖常见场景可能没问题,遇到陌生的领域就容易踩坑。
所以,我要强调,对于新技术,大家不熟悉又要尽快用在实际开发中,临时做点实验记住些经验用法当然可以。但不应满足于此,要找时间把技术规范补起来,把完整的设计至少读个几遍。一方面纠正自己的错误实践,另一方面,也可以扩大你对这个技术的应用面。
这里也不得不提 Code Review 的一个重要作用:
传承知识。Code Reviewe 是非常好的查缺补漏机会,可以针对性补强开发者的知识盲区,纠正不良习惯。
通过 Code Review,我发现了一位同事在 Promise 和函数执行栈方面存在知识盲区,然后我借机帮他补齐了这方面的知识。接下来,我把这个问题分享到技术研讨群,还有几位同学也不是很清楚,也趁机补齐了。于是,我厂再出现这个问题的概率,就降低了。换言之,我厂技术群体的下限,就拔高了。
思否上有同学问:什么样的技术 leader 是称职的? 我的答案第一条就是:
给团队的技术兜底。通过工具、规范、流程,保证无论开发水平如何,都能尽快提交符合要求、满足规范、质量过硬的代码。
具体一点:要重视每一次 Code Review,找到问题,解决问题,补全大家的知识点,提升团队下限。
上次的 Code Review 分享获得了意料之外的欢迎,希望这次同样能帮助到大家——我已经把上面的问题加入我的面试题库了,哈哈。
我现在在 code.fun 工作,我们的产品会把设计稿自动转换成代码,期望大大提升前端的开发效率,如果你对这个产品感兴趣,欢迎联系我,或直接试用。我们公司提供远程工作岗位,有兴趣的同学可以联系我;朋友的公司 API7 也在招聘前端,有兴趣可以找我内推。
如果诸位读者老爷对软件质量管理、软件开发效率、Code Review 有什么想问的、想说的,敬请在评论区与我互动。

这周周会上,有同事说:
以前他的 PR 被 approved 就直接合;现在他会等上一两天,期待我来 review。晚上耍手机时如果突然一阵爆响就说明我 reviewing,他就爬起来好好看我的评论然后作出修改,学到很多东西。
被承认当然很爽、很骄傲。于是我想起这个系列,该再更一篇了。本文选材自七月的 Code Review 经验,趁热先分享一波。更早的 Code Review 过两天再往前翻翻看能否整理出来。
关于 Code Review 的介绍和经验,欢迎大家回顾:在 Code.fun 做 Code Review,本文我就不重复了。
<script src="...">
在这个需求中,我们要使用火山引擎统计用户行为,某同事就直接在模版里添加了 <script src="..."> 导入初始化 JS。这段 JS 非常之短,只是帮火山引擎初始化执行环境,但是为了保证执行状态,它既不能 async 也不能 defer。
这样做有几个问题:
<script> 会阻塞后面的 HTML 渲染和 JS 执行;正确的做法是把它直接塞到现在 JS 里,只增加几十个字节而已。
mounted 钩子添加的事件处理函数没有清理;直接使用 window.onblur
这位同学犯了两个错误:
mounted 钩子里添加了事件处理函数,但是并没有在 destroyed 钩子里清理。这可能导致内存泄漏,也可能导致奇怪的表现。window 上,如果其它库、其它代码使用了同样的事件,就会导致冲突。
这里这位同学犯了两个错误:
event 是名词,不应该用做函数名。函数名应该是动词、动宾短语、动副短语,表达一个动作。event 是个非常非常通用的单词,用作全局函数太容易和其它全局对象起冲突或起混淆,应该加长。结合上下文,合适的写法是:triggerHuoshanEvent()。
JSON.parse()
我们需要记录一个小数据:sourceType,这个小数据被封装在一堆很大的 JSON 里面。这位同学直接拿 JSON.parse() 解码,然后打点。这样做当然可行,但是效率上非常之浪费。
大家千万不要小看 JSON.parse(),它其实有很多细节:
所以正确的写法,应该使用字符串匹配或者正则。字符串操作都是 O(1),而且会在匹配到结果后立即返回,所以性能优秀非常多。
(其实吧,这位同学的正则也有些一言难尽,不过今天先不展开。)

使用 TypeScript 之后,我们需要声明函数的返回值类型。对于一些比较通用的返回值,比如截图中的 mongoose lean(),就要进行类型声明转换,后面的代码才知道取到的值是什么类型。这里大家要记住,无论是浏览器还是 node,TS 都要经历编译之后,变成 JS 再被执行。
所以 TS 阶段的类型转换不会出现在最终执行的代码里,也不会影响效率。但是截图中属于不同的情况,它会留下一个 .then() ,并增加一个微任务,不是我们想要的结果。

TypeScript 提供 enum 关键字,用来声明枚举值,可以大大提升开发效率,降低出错概率。枚举的值可以手动指定,也可以省略,省略后 TS 会自动帮我们从 0 开始递增。上面就是典型的使用场景,列举若干设计软件,最后是 Other(其它),可以做成单选多选放在表单里。
这段代码的问题在于,设计软件很多,我们列出来的肯定有疏漏;即使没有,将来也会发布更多设计软件。比如有天,我们自己做了设计软件,叫 design.fun,那么该放到哪里呢?放到 Other 前面,它的值就会跟 Other 冲突;放到后面,又会使得数据看起来很奇怪。
此时只要给 Other 一个大点的值,拉开差距,就可以解决这个问题:
export enum DesignSoftware {
Photoshop,
XD,
JsDesign,
Other = 100, // 再大点也行,不过 100 应该够我们用了
}

这个其实是 Vue 官方的风格建议。原因很简单,一个单词,无论作为组件名称还是根节点样式名,都太容易和其它组件重复,也会影响 IDE 里快速跳转的效率,所以尽量多写几个单词,反正编译过程可以自动优化。
时间过的真快,距离上次分享 Code Review 经验已经过去四个月了,我在 Code.fun 也是半年多工龄的老员工了。经过半年多的努力,我厂的产品完整了很多,但也很难称得上完善。希望这次分享的内容对大家有用,也希望大家多来试用我们的产品。
如果诸位读者老爷对软件质量管理、软件开发效率、Code Review 有什么想问的、想说的,敬请在评论区与我互动。
我现在在 code.fun 工作,我们的产品会把设计稿自动转换成代码,期望大大提升前端的开发效率,如果你对这个产品感兴趣,欢迎联系我,或直接试用。