1. 背景
我们知道 TypeScript 的命令行工具是可以 watch 的,进程启动后,终端是这样的,
$ tsc index.ts --watch[00:00:00 AM] Starting compilation in watch mode...
index.ts 文件变更后,如果没有错误,终端会追加这样一行,
[00:01:00 AM] Found 0 errors. Watching for file changes.
如果编译过程中有错误,终端也会追加错误信息,
index.ts:1:1 - error TS2304: Cannot find name 'x'.1 x~[00:02:00 AM] Found 1 error. Watching for file changes.
这是 TypeScript tsc watch 的全过程。
除此之外,TypeScript 还支持 API 接口的方式对文件进行 watch。
const ts = require('typescript');// 要监听的 ts 文件const watchFilePath = ...;// 诊断信息、watch 状态,会根据这两个回调函数反馈出来const reportDiagnostic = (...args) => ...;const reportWatchStatus = (...args) => ...;const rootFiles = [watchFilePath];const options = {};const host = ts.createWatchCompilerHost(rootFiles,options,ts.sys,ts.createEmitAndSemanticDiagnosticsBuilderProgram,reportDiagnostic,reportWatchStatus,);const watchProgram = ts.createWatchProgram(host);
完整的代码可参考这里 github: debug-typescript。
以上代码,会监听指定 ts 文件的变更,然后从接口而不是命令行中,取到编译相关的信息。
这一切是怎么完成的呢?
要追踪整条链路,我们还得从 Node.js 标准库 fs.watchFile 开始。
2. fs.watchFile
fs.watchFile 是 Node.js 标准库中的方法,
具体用法如下,
const fs = require('fs');fs.watchFile(fileName, // 要监听的文件路径{ // (可选)persistent: true, // 程序执行完后,当前进程是否挂住,默认为 true(挂住)interval: 250, // 每个多久检测一次,默认值 5000 ms},fileChanged, // 文件变更时的回调);
值得注意的是,persistent 不论为 true 还是 false,fs.watchFile 执行完后,都会执行下一行语句,关键在于整个程序执行完后是否挂住。
了解了 fs.watchFile 的用法之后,TypeScript 文件监听的实现,也就容易跟踪了。
3. 调试
(1)source map 问题
根据 github: debug-typescript 的说明,
我们选择 debug watch 可对 watch 文件的主程序进行调试。
VSCode 中直接按 F5,断点会停在第一行,
我们来看这里,
const typescript = path.join(root, './lib/typescript.js');
此处引用了 TypeScript 源码仓库中 lib/ 文件夹的 typescript.js 文件,
线上地址在这里,github: TypeScipt/lib/typescript.js
并没有像以前的文章中提到的那样,引用了本地 TypeScript 的编译结果 built/local/typescript.js。
这是有原因的,
因为本地的编译结果 built/local/typescript.js 相关的 source map 有问题。built/local/typescript.js.map 内容如下,
{"version": 3,"file": "typescript.js","sources": ["typescriptServices.out.js"],"sourceRoot": "","mappings": ";;;;;;;;;;;;;;;","preExistingComment": "typescriptServices.js.map"}
其中 mappings 只包含了一些分号,因此 VSCode 调试器无法根据编译产物定位到源码中。
VSCode 调试器要么会卡住,要么会提前终止掉。
手动将 built/local/typescript.js.map 文件删掉就可以调试了,
与直接引用 lib/typescript.js 的效果是一样的。
关于 TypeScript 本地编译,可参考 github: debug-typescript,
$ node node_modules/.bin/gulp LKG
(2)调用栈
好了我们言归正传,我们可以在第 7 行打个断点,
然后单步调试进去,跑到被 require 的文件中,
这正是 lib/typescript.js 文件。
下一步由于我们已经知道了 Node.js watch 文件变更的方法,
所以就可以简化调试过程了。
直接在 lib/typescript.js 中搜索 fs.watchFile。
找到了 6 个结果,只有一个结果不在注释中,
位于 fsWatchFileWorker lib/typescript.js#L5334 函数中,
function fsWatchFileWorker(fileName, callback, pollingInterval) {_fs.watchFile(fileName, { persistent: true, interval: pollingInterval || 250 }, fileChanged);...return {close: function () { return _fs.unwatchFile(fileName, fileChanged); }};function fileChanged(curr, prev) {...callback(fileName, eventKind);}}
我们看到它执行了 watchFile 操作,
_fs.watchFile(fileName, { persistent: true, interval: pollingInterval || 250 }, fileChanged);
每 250 ms 检测一下文件变更,如有变更就回调 fileChanged。
在这一行打个断点,看看能否得到调用栈信息。
watch fileName 的值,是待编译源文件的绝对地址,
调用栈最底部,正是 debug-watch/index.js 中调用 ts.createWatchProgram 的位置,
(3)监听文件变更
我们接着在 fileChanged lib/typescript.js#L5342 回调函数中打个断点,
然后按 F5 期望程序先跑完,再去修改源文件,看看能否触发 fileChanged。
结果,出乎意料的是,程序又来到了 watchFile 的断点处,fileName 改成了其他的值,
/Users/.../Microsoft/TypeScript/node_modules/_@types_browserify@12.0.36@@types/browserify/index.d.ts

这些文件是 TypeScript 预加载的文件,我们在之前的文章中,也曾遇到过。
可见 TypeScript 对这些预加载文件,也会进行监听。
把 watchFile 断点去掉,然后按 F5 执行,发现会直接进入 fileChanged 中,
其中,fileName 的值来自闭包中,
/Users/.../Microsoft/node_modules/@types"

这里暂时略过不去追究,按 F5 继续执行,我们发现调试器挂住了,进程并没有结束,
VSCode 调试器底下的状态栏仍然是橙色的。
现在我们去修改一下待编译的源文件 debug/index.ts 并保存。
程序果然来到了 fileChanged lib/typescript.js#L5342 的断点中。
fileName 的值正是刚才修改 debug/index.ts 文件的绝对地址,
/Users/.../Microsoft/TypeScript/debug/index.ts
(4)回调数据
回到主程序 debug-watch/index.js 中,
在 reportDiagnostic 和 reportWatchStatus 中打个断点,
然后按 F5 继续执行,就可以看到监听到文件变更后,TypeScript 的调用链路了。
args 中包含了这些内容,
TypeScript 检测到了文件变更,正在进行增量编译(incremental compilation)
File change detected. Starting incremental compilation...
这就又给我们带来了疑问,TypeScript 到底是怎样进行增量编译的呢?
这些探索留给后续的文章吧。
4. 后记
增量编译看起来是一种很神秘的技术,这也是写文本的原因之一,
从 watch 角度来跟踪增量编译,算是一个不错的切入点。
本文主要介绍了 Node.js fs.watchFile 库函数,以及 TypeScript 监听文件变更的具体细节。
其中遇到了 lib/typescript.js 的 source map 文件,导致我们只能调试 js 代码了。
js 代码(编译产物)的回调函数,看起来会比较烧脑一些,
幸运的是代码没有被压缩和混淆,不然真是遇到麻烦了。
