var index = 0;
var outIndex = 0;
var currentChar;
var nextChar;
var hexHigh;
var hexLow;
var maxLength = s.length - 2;
// Flag to know ifsome hex chars have been decoded
var hasHex = false;
下一块代码是一个 while 循环,遍历字符串中的每个字符。如果字符是 +,并且函数设置为将 + 更改为空格,则会将转义的字符串中该字符的值设置为空格。
functionparse(qs, sep, eq, options) {
const obj = Object.create(null);
if (typeof qs !== 'string' || qs.length === 0) {
return obj;
}
var sepCodes = (!sep ? defSepCodes : charCodes(sep + ''));
var eqCodes = (!eq ? defEqCodes : charCodes(eq + ''));
const sepLen = sepCodes.length;
const eqLen = eqCodes.length;
var pairs = 1000;
if (options && typeof options.maxKeys === 'number') {
// -1 is used in place of a value like Infinity for meaning// "unlimited pairs" because of additional checks V8 (at least as of v5.4)// has to do when using variables that contain values like Infinity. Since// `pairs` is always decremented and checked explicitly for 0, -1 works// effectively the same as Infinity, while providing a significant// performance boost.
pairs = (options.maxKeys > 0 ? options.maxKeys : -1);
}
var decode = QueryString.unescape;
if (options && typeof options.decodeURIComponent === 'function') {
decode = options.decodeURIComponent;
}
const customDecode = (decode !== qsUnescape);
然后函数遍历查询字符串中的每个字符并获取该字符的字符代码。
var lastPos = 0;
var sepIdx = 0;
var eqIdx = 0;
var key = '';
varvalue = '';
var keyEncoded = customDecode;
var valEncoded = customDecode;
const plusChar = (customDecode ? '%20' : ' ');
var encodeCheck = 0;
for (var i = 0; i < qs.length; ++i) {
const code = qs.charCodeAt(i);
// Deal with any leftover keyor value data
if (lastPos < qs.length) {
if (eqIdx < eqLen)
key += qs.slice(lastPos);
elseif (sepIdx < sepLen)
value += qs.slice(lastPos);
} elseif (eqIdx === 0 && key.length === 0) {
// We ended on an empty substringreturn obj;
}
if (key.length > 0 && keyEncoded)
key = decodeStr(key, decode);
if (value.length > 0 && valEncoded)
value = decodeStr(value, decode);
if (obj[key] === undefined) {
obj[key] = value;
} else {
const curValue = obj[key];
// A simple Array-specific property check is enough here to
// distinguish from a string value andis faster and still safe since
// we are generating all of the values being assigned.
if (curValue.pop)
curValue[curValue.length] = value;
else
obj[key] = [curValue, value];
}
if (obj !== null && typeof obj === 'object') {
var keys = Object.keys(obj);
var len = keys.length;
var flast = len - 1;
var fields = '';
for (var i = 0; i < len; ++i) {
var k = keys[i];
var v = obj[k];
var ks = encode(stringifyPrimitive(k)) + eq;
functionqsEscape(str) {
if (typeof str !== 'string') {
if (typeof str === 'object')
str = String(str);
else
str += '';
}
var out = '';
var lastPos = 0;
for (var i = 0; i < str.length; ++i) {
var c = str.charCodeAt(i);
有一段时间了,我想要在 Node 生态系统中执行标准库和常用软件包的代码演练。我想现在是时候把这个意愿改变为行动,并且实际写出一篇文章。所以在这里,我的第一个带注释的代码演练。
我想先看一下Node标准库中最基本的模块之一:
querystring
。querystring
是一个允许用户提取 URL 的查询部分的值和从键值关联的对象构建查询的模块。这是一个快速的代码片段,显示了由querystring
暴露的四种不同的API函数,escape
,parse
,stringify
和unescape
。好的!让我们深入了解有趣的部分。我将查看
querystring
模块的代码作为我写这篇文章的标准。你可以在这里找到这个版本的副本。引起我注意的第一件事是47-64行的这段代码。
这是什么胡言乱语?我在整个代码库中搜索了
unhexTable
这个术语,以找出它在哪里使用。除了定义语句之外,搜索还返回了另外两个结果。它们出现在代码库的第86和91行。这里是包含这些引用的代码块。所有这些都发生在
unescapeBuffer
函数中。快速搜索后,我发现unescapeBuffer
函数由模块公开的unescape
函数调用(请参见第113行)。所以这里是发生在querystring
中的 unescape 动作。好的!那么,
unhexTable
的所有逻辑是什么?我开始通读unescapeBuffer
函数来弄清楚它在做什么。我从第67行开始。所以函数首先初始化一个传递给函数的字符串长度的 Buffer。此时,我可以深入了解
Buffer
类中的allocUnsafe
正在做什么,但是我将预留它为另一篇博客文章。之后,有几个语句会初始化为稍后将在函数中使用的不同变量。下一块代码是一个 while 循环,遍历字符串中的每个字符。如果字符是
+
,并且函数设置为将+
更改为空格,则会将转义的字符串中该字符的值设置为空格。第二组 if 语句检查迭代器是否处于以
%
开始的字符序列,这表示接下来的字符将代表十六进制代码。然后程序获取字符代码。接着程序使用该字符代码作为查找unhexTable
列表中的索引。如果查找返回的值为-1
,则该函数将输出字符串中的字符值设置为百分号。如果从unhexTable
中的查找返回的值大于-1
,则函数会将分隔字符解析为十六进制字符代码。让我们再深入一点这段代码。所以,如果第一个字符是有效的十六进制代码,它将使用下一个字符的字符代码作为
unhexTable
的查找索引。这个值是在hexLow
变量中。如果该变量等于-1
,则该值不会被解析为十六进制字符序列。如果不等于-1
,则该字符被解析为十六进制字符代码。该函数取十六进制代码的最高位(第二位)(hexHigh
)的值,将其乘以16并将其加到十六进制代码的值的第一位中。函数的最后一行让我困惑了一会儿。
如果我们在查询中检测到一个十六进制序列,则将输出字符串从
0
到outIndex
的切片,否则保持原样。这使我感到困惑,因为我认为outIndex
的值将等于程序结束时输出字符串的长度。 我本可以花时间弄清楚这个假设是否属实,但说实话,现在已经快到午夜了,而且我已经没有精力在深夜做这种荒唐举动了。所以我在代码库上运行git blame
,并试图找出哪些提交与这个特别的改动相关联。事实证明,这并没有太大的帮助。我期待着那里有一个孤立的提交,它描述了为什么那个特别的一行代码是这样的,但最近的变化是属于escape
函数的一个更大的重构的一部分。 我越看越确定这里不需要三元运算符,但我还没有找到一些可重现的证据。我研究的下一个函数是
parse
函数。函数的第一部分进行一些基本的设置。函数默认分析查询字符串中的 1000 个键值对,但用户可以在options
对象中传递maxKeys
值以更改此值。 该函数还使用我们前面介绍的unescape
函数,除非用户在选项对象中提供了不同的东西。然后函数遍历查询字符串中的每个字符并获取该字符的字符代码。
函数然后检查被检查的字符是否对应于键值分隔符(例如查询字符串中的”&“字符)并执行一些特殊的逻辑。它会检查“&”后面是否有“key=value”段,并尝试从中提取相应的键和值对(第304-347行)。
如果字符代码不与分隔符相对应,函数会检查它是否与“=”符号或其它的用于提取键值的分隔符相对应。
接下来,该函数检查字符是否为“+”符号。如果是这种情况,那么函数会生成一个空格分隔的字符串。如果字符是“%”,则该函数会正确解码后面的十六进制字符。
还需要在未处理的数据上完成剩余的检查。换句话说,函数要检查是否还有一个需要添加的键值对,或者该函数是否可以返回空数据。我认为这里包含了处理解析时可能出现的边界情况。
最后一组检查,它会检查是否需要对键或值进行解码(使用
unescape
函数),或者是否需要将特定键的值构造为数组。这就是
parse
函数!好的!我继续看看
querystring
模块暴露的另一个函数stringify
。stringify
函数首先初始化一些必需的变量。它默认使用escape
函数来编码值,用户可以在选项中提供自定义的编码函数。之后,该函数会迭代对象中的每个键值对。当它遍历每个键值对时,它会对键进行编码和串化。
接下来,它检查键值对中的值是否为数组。如果是,则它遍历数组中的每个元素,并向该字符串添加
ks=element
关联。如果不是,则该函数根据键值对构建ks=v
关联。这个函数对我来说很简单。在由API公开的最后一个函数中,
escape
。该函数遍历字符串中的每个字符并获取与该字符相对应的字符代码。如果字符代码小于
0x80
,表示所代表的字符是有效的ASCII字符(ASCII字符的十六进制代码范围为0
到0x7F
)。该函数然后通过在noEscape
表中查找来检查字符是否应该被转义。该表允许标点符号,数字或字符的字符不会被转义,并要求其他所有内容都被转义。然后它检查字符的位置是否大于lastPos
发现的位置(意味着已经超过了字符串的长度)并适当地对字符串进行分片。最后,如果字符确实需要转义,它将查找hexTable
中的字符代码并将其附加到输出字符串。下一个if语句会检查字符是否是多字节字符代码。多字节字符通常代表重音和非英文字母的字符。
在这种情况下,使用
hexTable
中的以下查找来计算输出字符串。好的!这里有很多事情要做,所以我开始研究它。
hexTable
在internal/querystring
支持模块中定义,并且像这样生成。所以输出结果是一个字符串数组,代表256个字符的十六进制字符代码。它看起来有点像
['%00', '%01', '%02',..., '%FD', '%FE', '%FF']
。所以,上面的查询语句。语句
c >> 6
将字符代码右移6位,并与192的二进制表示执行按位或。语句c & 0x3F
将字符串和63的二进制表示执行按位与,然后和0x80
执行按位或。两条语句执行的结果各自进行一次hexTable
查询,并将结果相连接。所以我知道多字节序列从0x80
开始,但我无法弄清楚到底发生了什么。下一个被检查的情况是这样的。
在所有其他情况下,该函数使用以下策略来生成输出字符串。
我真的被所有这些困惑了。当我去做一些调查时,我发现所有这些与十六进制相关的代码都来自这个单独的提交。它似乎是因性能相关的一部分而出现。关于为什么使用这种特定的方法,并没有大量的信息,我怀疑这个逻辑是从另一个编码函数中复制的。在某些时候我会进一步深入研究。
最后,有一些逻辑处理输出字符串返回的方式。如果
lastPos
的值为0,表示没有处理字符,则返回原始字符串。否则,返回生成的输出字符串。就是这样!我介绍了由Node
querystring
模块公开的四个函数。