包阅导读总结
1. `TypeScript`、`性能问题`、`代码风格`、`语言特性`、`模块方案`
2. 本文主要探讨了 TS 源码中 `checker.ts` 文件的相关情况,包括其代码风格和语言特性,如低配版命名参数、大量使用 `const enum` 等,还分析了 ESM/CJS 的性能问题、模块方案的不足等,指出 JS 特性限制了 TS 的实现,虽 compiler pipeline 架构优秀,但仍存在诸多问题。
3.
– 五万行的 `checker.ts` 文件
– 类型系统全部逻辑写在一个文件里,相当暴力
– 低配版命名参数以减少内存开销和性能损失
– 大量使用 `const enum` ,虽有争议但提升了性能
– ESM/CJS 的性能问题
– 导出过多成员会有处理开销
– 缺少 `private export` 特性,通过 `@internal` 注解变相实现
– 代码风格和语言特性
– 大量使用 `var` 而非 `let` 和 `const`
– 往 `String.prototype` 注入东西
– 无类编程,推崇组合编程
– 未用表驱动,因其无法被优化且慢
– 基本没有 `try-catch`
– 文件和模块相关
– 文件多导致查找困难,TS 曾尝试 namespace 特性但未成功
– 认为 ESM 和 CJS 性能不行,JS 特性限制了 TS 实现
思维导图:
文章地址:https://mp.weixin.qq.com/s/EdFTaOTHhIyNKjkXFBQ4Mw
文章来源:mp.weixin.qq.com
作者:赖博志@腾讯文档
发布时间:2024/8/27 23:47
语言:中文
总字数:3808字
预计阅读时间:16分钟
评分:88分
标签:TypeScript,编码实践,性能优化,JavaScript,模块系统
以下为原文内容
本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com
👉目录
0五万行 all-in-one 的 checker.ts
1 低配版 named parameters
2 能用 number 尽量 number
3无限制使用 const enum
4 ESM/CJS 的性能问题:尤其是 export 导出特别多的时候
5ESM 没有 private 导出
6TS 甚至大量使用 var,而不是用 let 和 const
7往 String.prototype.xxx 上注入东西
8无类编程,推崇组合编程
9 怎么没有用「表驱动」这种所谓的常用「前端设计模式」?
10基本没有 try-catch
11文件多才是大问题 —— 可惜了半成品的 ts namespace
12 最后来个暴论:JS 已经严重影响 TS 的演进了
《如意、悟净、悟能——操作系统界的“黑神话”特性》「鹅厂程序员面对面」今晚7:30直播精彩继续,预约观看有机会抢鹅厂周边好礼!
大名鼎鼎的checker.ts
这个文件我很久以前就知道了, 在 GitHub 上直接打不开:GitHub – microsfot/GitHub: ./src/compiler/checker.ts
好,腾讯云 Cloud Studio 启动:
腾讯云云端开发环境 Cloud Studio (https://ide.cloud.tencent.com/)是基于浏览器的在线IDE,为开发者提供了一个稳定的云端工作站。用户在使用 Cloud Studio 时无需安装,随时随地打开浏览器就能使用。其功能包含代码高亮、自动补全、Git 集成、终端等 IDE 的基础功能,同时支持实时调试、插件扩展等,并内置多种前后端与AI开发模板,可以帮助开发者快速完成各种应用的开发、编译与部署工作。
这个文件很暴力,类型系统全部逻辑 5 万行 all-in-one file ,是 TS 源码维护者不会写代码吗?显然并不是,我翻了一些资料和读了下其中的实现,稍微震撼了一下,将相关思考细节记录在本文。
众所周知,JS 各种规范都推荐你用一个对象来传递多个参数,然后在函数里解构 —— 多数时候这没什么,但是在 TS compiler 里,任何浪费都会被极限放大,因此他们用了这种低配版用注释的方式来表示 named parameters (这行还是 anders 老爷子写的,C# 之父 《编程领域的传奇!C#、TypeScript 之父!全世界最顶尖的程序员之一》。
(https://cloud.tencent.com/developer/article/1751937)
何为 named parameter 呢?其实就是带名字标签的函数,调用的时候可以指定标签来传参数,这个在其他语言里是基操,比如 moonbit or swift 里的标签函数:
fn add(~left: Int, ~right: Int) -> Int {
return left + right;
}
add(left: 1, right: 44);
add(right: 44, left: 1);
add(1, 2);
为什么 TS 需要 named parameter 特性:在 TS这种高频调用场景里通过解构 options 对象的方式传参会导致大量无谓的内存开销 —— 这通常会导致 type checking 过程中的内存峰值而造成频繁 gc & mem_copy 更重要的是字面量 key 的顺序还会影响 v8 的 inline caches 优化,写的不好可能会对函数调用 feedback 造成严重负面反馈进而影响 TurboFan 的进一步优化最后造成非常大的性能损失 …
当 V8 函数调用的 feedback slot 从 SMI 变成 Any 时,TurboFan codegen 的汇编将会慢三倍。
比如 switch、比如 const enum、比如各种 enum bitmap flags 等等设计,原因是 object 和 string 的开销太大了,而小一点的整数在 v8 里甚至是无开销的。(如果 SMI tagged pointer 指针自身数值不算开销的话)
const enum 有个特性可以直接 inline 枚举值到函数里变成立即数,能享受极致优化:
但目前社区对于 const enum 的主流意见是不推荐使用
,而且 ts 的部分维护者也认为这个是 mistake:
但是这说法其实相当尴尬:是的虽然这是 mistake 我们不推荐使用,但我们 TS 源码里全都是 const enum 到处飞 … (800+ 个 const enum,没这个特性估计 tsc 要慢不少)
当 export
导出太多成员的情况下,V8 内部处理这类对象会将其变成 Slow Properties
字典模式,在多数时候这没啥,但如果遇到某高频模块内的常量被引用大几百万次的情况下,此时 export.xxxxx
的点读查询开销就不能忽视了,尤其是当 export
上有几百个导出的时候,此时点读开销不可忽视,比如:
const constant = require(`./constant`);
module.export = function getXXConfig() {
return constant.xxx + constant.bbb;
}
而 checker.ts 则是将所有东西 all in one,就没这问题了,全都在函数作用域内,查询时间是 O(1) 。
有种 export
是只想在项目内无限制使用,但是又不期望导出能被外部的 npm 看到 —— 也就是 esm 没有提供 private export 这种特性:
import D from '@tencent/xxx/a/b/c/d';
而 TS 又恰恰要这种特性,那么它们怎么实现的呢?通过/** @internal */
注解,比如:
标记为@internal
的东西在生成 d.ts 的时候会被抹去,变相实现外部无法 import 而 TS 仓库内随便 import 。
又比如,有部分函数为了性能全用 var,愣是没用 const / let 这些,你看 TS 怎么写的:
具体见:https://github.com/microsoft/TypeScript/issues/52924
大意是 TS 的场景下,v8 这类 js runtime 的 TDZ 检查甚至会相当影响运行性能。。。毕竟五万行呢。。。(production build 会比 dev build 要快不少的原因之一)
这类操作在普通 JS/TS项目里是一定会被鄙视的,但一个静态类型语言怎么没办法自己拓展基础类型来使用呢?(这在 swift / Go 之类的语言里基于 string / int 来搞出一个新的类型出来是基操。。。。)
checker.ts
几万行核心逻辑几乎没有 class 和继承,完全通过函数组合的方式来架构代码,整体看着像是有 rust impl
关键字的 TS 那样:
代码里大部分函数都是上面这种风格,第一个参数是「核心接口」其他参数则是对应的参数,当然,组合优于继承也算是近年来业界达成的共识了。
当然比起架构,我更愿意相信 TS 是考虑到 class 继承可能存在潜在的性能问题导致的:
比如 V8 引擎下的 A extends B
场景,B 上面有个方法 fn,当 A.fn();
B.fn();
都调用了之后,如果 A 和 B 的 shapes
不一样,此时 fn 调用 feedback slot 会从 monomorphic 的变成 polymorphic 的,当继承三个以上的时候就会变成 megamorphic 了,这会影响引擎 ICs 的优化效果,导致性能下降。
源码里很多这种根据 ast node kind 去走不同逻辑,然后这些逻辑都写成 if else if else 或者 switch 语句 —— 为何不使用一个 Record<Kind, Fn>
的方式去表驱动呢?
原因很简单:表驱动无法被 v8 这类 runtime 静态分析优化,而且表驱动这类写法会慢个几十倍对于基础设施来说这是不可接受的。(无贬义,JS 的表驱动写法看场景,高频调用还是别了吧,写 event selector 之类的倒是一类比较合适的场景)。
从语言特性的层面来说,TS 真的缺一个满血版模式匹配 + enum adt 了,但目前 TS 原则上是不会再合入新的 runtime 特性了 —— 这就很难受了,又不能表驱动,又不能模式匹配,最后代码很 C style 了,而且要写非常多的 x is X 谓语 。
与 Go 有类似的想法,checker.ts 里通过返回值 + 往 context.xxx 上写东西的方式来指示异常,一方面是为了性能,另外一方面我甚至可以合理怀疑为是没有 checked exception 导致只能这样才能 type checked … (当然 anders 老爷子应该是 uncheck 党,参考 C# 的设计)
如果有接触过大型 JS/TS 项目的同学肯定知道,文件一多就不知道东西在哪了,找个 import 你甚至要垮十几个文件 。
—— 从这也可看到,东西为什么要 import 才让用呢?能否有 moonbit、rust 那样好用的模块系统呢?⬅️ 但这依然涉及 runtime 改造,现阶段 TS 就别想了,当然 tc39 也不会再考虑这类特性就是了,等一个 TypeScript Pro Max 吧。
关于 namespace:有接触过 Go Rust C++ 的同学应该都有了解了,是用来管理包及语言符号的特性,是业内比较通用的解决方案。
在 ESM 落地之前,TS 有尝试去做满血版的 namespace 特性,但是由于重新确定了不做运行时的想法,因此这个特性在成熟之前就放弃迭代而全面转向 ESM 了,至今 TS 源码里还大量使用 namespace 或者用 ESM 模拟出 namesapce 特性:
不得不说,TS如果继续死磕 JS/tc39 而放弃做 runtime feature,恐怕现在已经是最终形态了 … 以后不会有更进一步的演进了,因为目前 TS类型系统已经相当完善了,甚至部分能力其他语言都没有,比如 Union Types 以及领先各大友商的控制流分析技术(然而,2024 了 TS还没有满血版 ADT + 模式匹配,因为这属于 runtime 特性,不是简单擦掉类型就能搞定的)
当然,近期 tc39 虽然也提了不少新东西但是没有静态类型系统就显得这些特性相当鸡肋以至于它们看起来就像是 ts39
一样,比如备受关注的 Record & Tuple 已经到 Stage 2
了,但懂得都懂这特性一看就知道明显就是给 TS设计的,给 JS用这个特性跟到处传 void*
一样没什么区别,因为这东西是运行时强类型的,也就是访问 one_record.x
如果真的没有定义 x
那么会直接抛出 error 的而不是返回 undefined
。
此外这东西太猛了,几乎就是一个 C 语言版的 匿名 struct
定义对象+内存结构的方案了,我估计各大浏览器估计都不太想搞这个 —— 这个要大改引擎的 JS 对象模型了,如果真能实装我很期待它的性能表现。
总之,就目前 TS 源码仓库来看,JS 自身的语言特性已经极其限制 TS 对其自身的实现了,但是 TS 又承诺不再做新的 runtime 特性,只做类型系统,这就相当拧巴了,尤其是体现在 TS 源码里。
EOF
checker.ts 已经搞出几万行文件以及大量 if-else 超高复杂度的控制流了,还自己手写 named para 注释、甚至不用 const / let / class 。。。而且从代码里处处可见 TS 相当鄙视 esm 和 cjs 这些 module 方案,觉得性能不行,然后搞出来一个半成品的 namespace 模块方案 … 总之包含大量非主流实践,有些甚至可以形容为龌龊。
总之由于 JS 特性太少了,导致源码实现相当拧巴,虽然如此但 TS 整体的 compiler pipeline 架构设计却相当漂亮和简洁,尤其是 transfomers 和 anders 老爷子主推的 LSP 所带来的 IDE 革命,有机会后续单开一篇谈谈这个。
📢📢欢迎加入腾讯云开发者社群,享前沿资讯、大咖干货,找兴趣搭子,交同城好友,更有鹅厂招聘机会、限量周边好礼等你来~
(长按图片立即扫码)