jsernews 1.0.1

<~>

ts 98 days ago.

原文:https://blog.safia.rocks/post/167943601717/node-module-deep-dive-console

你好!是的,又是一个星期一。这意味着现在是时候让我回头看看好的 GitHub.com,以便深入了解另一个 Node 模块。如果你刚来这里,在过去几周里我一直在阅读 Node 代码库。我的目标是更多地了解隐藏在 Node 标准库下的内容,并学习一些新的代码模式。

这周,我打算深入了解 console 模块。在开始之前,我们应给予 console 一些当之无愧的感谢,这是迄今为止最可靠的调试工具!如果你还不太熟悉,console 暴露了了一组打印到标准输出和标准错误的方法。 通常像这样使用:

> console.log("Just a standard message going to standard out.");
Just a standard message going to standard out.
undefined
> console.error("Error: Something terrible has happened and this is going to stderr.");
Error: Something terrible has happened and this is going to stderr.
undefined
> console.info("This is also informative and is going to standard out.");
This is also informative and is going to standard out.
undefined

相当有用,是吧?好的!让我们深入代码。按照惯例,这里是我将在本文中阅读的 console 模块版本的固定链接。在这之前,我已经阅读过一些类似代码,并学到了一些东西:从底部开始阅读 Node 模块代码更有助于理解。从本质上讲,这给了我一个机会来找出模块暴露的 API 是什么,并更有效地阅读相关模块。在 console 模块下,exports 看起来像这样:

module.exports = new Console(process.stdout, process.stderr);
module.exports.Console = Console;

console 模块默认导出一个 Console 对象的​实例,但该模块还会导出该对象本身的定义,以便用户可以在其代码中实例化自己的实例。Console 对象在其原型上定义了几个函数。我已经知道一些函数,比如 logdebug,但是有一些对我来说是新的,比如 timecount。这些暴露出来的 API 背后的关键支持函数是 write 函数,它定义如下:

function write(ignoreErrors, stream, string, errorhandler, groupIndent) {

我通读了函数的主体,并找出了每个参数是用来做什么的。

  • ignoreErrors:该参数是一个布尔值,用于定义是否忽略写入输出流时发生的错误。请注意,默认值为 true
  • stream:此参数定义函数应将信息写入的流对象。
  • string:该参数定义了应该隐藏的字符串。
  • errorhandler:在写入流时遇到错误时执行的回调。
  • groupIndent:该参数定义了每条新行的缩进数量。例如,这在打印堆栈跟踪时特别有用,因为跟踪通常是缩进的。

我发现确定每个参数所做的工作特别有用,因为它使阅读函数体更容易。前面几行检查每个换行符后面的 string 是否需要缩进,并检查 string 中是否有换行符。如果定义了 groupIndent 并且 string 中包含换行符,则换行符将被替换为适当的缩进。

if (groupIndent.length !== 0) {
  if (string.indexOf('\n') !== -1) {
    string = string.replace(/\n/g, `\n${groupIndent}`);
  }
  string = groupIndent + string;
}
string += '\n';

代码接下来的一部分处理上述的 ignoreErrors 参数。如果 ignoreErrorstrue,则该字符串将直接发送到流而不进行任何错误处理。

if (!ignoreErrors) return stream.write(string);

另一方面,如果我们想要处理错误,函数会执行一个try-catch子句。

try {
  // Add and later remove a noop error handler to catch synchronous errors.
  stream.once('error', noop);

  stream.write(string, errorhandler);
} catch (e) {
  // console is a debugging utility, so it swallowing errors is not desirable
  // even in edge cases such as low stack space.
  if (e.message === 'Maximum call stack size exceeded')
    throw e;
  // Sorry, there’s no proper way to pass along the error here.
} finally {
  stream.removeListener('error', noop);
}

我发现 stream.once('error', noop); 语句相当有趣,并决定做一些挖掘来弄清楚它是什么。我最终发现了这个 Pull Request相应的问题。看来这个语句被添加来处理标准输出和标准错误不可用的情况。这个函数不会抛出一个错误,而应该默默地失败。但如果一旦流写入时发生错误,函数应该使用 errorHandler 处理错误。

ts 97 days ago. link 1 point

console API 公开的大部分方法都利用了 write 函数。例如,经常使用的 log 函数看起来像这样。

Console.prototype.log = function log(...args) {
  write(this._ignoreErrors,
        this._stdout,
        util.format.apply(null, args),
        this._stdoutErrorHandler,
        this[kGroupIndent]);
};

warn 函数看起来有点像这样。

Console.prototype.warn = function warn(...args) {
  write(this._ignoreErrors,
        this._stderr,
        util.format.apply(null, args),
        this._stderrErrorHandler,
        this[kGroupIndent]);
};

对于我来说,console API 里有一些方法比较新鲜。例如,timetimeEnd 方法用来测量代码当中两点之间的时间差。例如,我们可以测试执行两条语句之间经过了多少时间,如下所示。

> console.time("testing-time");
undefined
> console.timeEnd("testing-time");
testing-time: 42570.609ms

time 函数将一个键值对添加到 Console 对象的 _times 属性中,它定义了 label 和由 process.hrtime 获取的当前时间戳之间的关系。

Console.prototype.time = function time(label = 'default') {
  // Coerces everything other than Symbol to a string
  label = `${label}`;
  this._times.set(label, process.hrtime());
};

timeEnd 取回 time 函数存储的开始时间戳,并计算自该时间以来已经过去的时间量。

Console.prototype.timeEnd = function timeEnd(label = 'default') {
  // Coerces everything other than Symbol to a string
  label = `${label}`;
  const time = this._times.get(label);
  if (!time) {
    process.emitWarning(`No such label '${label}' for console.timeEnd()`);
    return;
  }
  const duration = process.hrtime(time);
  const ms = duration[0] * 1000 + duration[1] / 1e6;
  this.log('%s: %sms', label, ms.toFixed(3));
  this._times.delete(label);
};

结合使用 timetimeEnd 函数可以很好地对代码片段进行性能测试。

阅读代码库时引起我注意的另一组函数是 countcountReset 函数。这些函数用于维护给定的特定 label 的计数。

> console.count("red-fish");
red-fish: 1
undefined
> console.count("blue-fish");
blue-fish: 1
undefined
> console.count("red-fish");
red-fish: 2
undefined
> console.count("blue-fish");
blue-fish: 2
undefined
> console.count("red-fish");
red-fish: 3
undefined

count 函数增加或重置为特定 label 定义的计数器,该计数器存储在 Console 对象的 kCounts 属性中。

Console.prototype.count = function count(label = 'default') {
  // Ensures that label is a string, and only things that can be
  // coerced to strings. e.g. Symbol is not allowed
  label = `${label}`;
  const counts = this[kCounts];
  let count = counts.get(label);
  if (count === undefined)
    count = 1;
  else
    count++;
  counts.set(label, count);
  this.log(`${label}: ${count}`);
};

resetCount 函数重置特定 label 的计数。

Console.prototype.countReset = function countReset(label = 'default') {
  const counts = this[kCounts];
  counts.delete(`${label}`);
};

countReset 函数上面写有一个有趣的注释。

// Not yet defined by the https://console.spec.whatwg.org, but
// proposed to be added and currently implemented by Edge. Having
// the ability to reset counters is important to help prevent
// the counter from being a memory leak.

如上所述,console API的规范没有明确定义重置标签计数的函数。考虑到标准定义了与 time 函数关联的 timeEnd 函数的规范,我认为这很有趣。无论如何,这个标准是一个活的标准,所以有足够的时间来增加它。

就是这样!Console 对象并不像其他一些函数那样复杂,但我在阅读代码时发现了一些新的用途。