一个排查了一天的BUG,你在摸鱼🐟吧!

3 阅读4分钟

站会

在一次日常站会上,组员们轮流分享昨天的工作进展。一个组员提到:“昨天我整天都在排查一个BUG,今天还得继续。”

出于好奇,我问:“是什么BUG让你排查了这么久还没解决呢?”

他解释说:“是关于一个数据选择弹窗的问题。这个弹窗用表格展示数据,并且表格具有选择功能。问题在于,编辑这个弹窗时,表格中原本应该显示为已选状态的数据并没有正确显示已选状态。”

我猜测道:“是不是因为表格中数据的主键ID是大数值导致的?”

他回答说:“大数值?我不太确定。”

我有些质疑地问:“那你昨天都是怎么排查的?需要花一整天的时间,难道是在摸鱼吗?”

“没有摸鱼,只是这个BUG真得有点难搞,那个什么是大数值?”

“行吧,姑且信你,我待会给你看看。”

排查

表格使用的是 Ant Design 4.0 提供的 Table 组件。我检查了组件的 rowKey 属性配置,如下所示:

<Table rowKey={record => record.obj_id}></Table>

这表明表格行的 key 是通过数据中的 obj_id 字段来指定的。随后,我进一步查看了服务端返回的数据。

image.png

可以看到一条数据中的 obj_id 字段值为 "898186844400803840",这是一个18位的数值。

在ES6(ECMAScript 2015)之前,JavaScript没有专门的整数类型,所有的数字都被表示为双精度64位浮点数(遵循IEEE 754标准)。这意味着在这种情况下,JavaScript能够安全地表示的整数范围是从253+1-2^{53} + 125312^{53} - 1(即-9,007,199,254,740,991到9,007,199,254,740,991)。可以简单地认为超过16位的数值就是大数值。

JavaScript中很多操作处理大数值时会导致大数值失去精度。比如 Number("898186844400803840")

image.png

可以看到 "898186844400803840""898186844400803800" 的区别在第16位后,从 40 变成 00 这就是大数值失去精度的表现。

在看一下表格的数据展示,如下图所示:

image.png

可以确定的是,从服务端返回的数据到在表格中的渲染过程是没有问题的。那么,可能出现问题的地方还有两个:一是在选择数据后,数据被传递到父组件的过程中;二是父组件将已选数据发送回选择数据组件的过程中。

定位

我检查了他将数据传递给父组件的逻辑代码,发现了一个可疑点。

image.png

在上述代码中,JSON.parse 被用来转换数据中的每个值。在这个转换过程中,如果 item[key] 是以字符串形式出现的数值,并且这个字符串能够被 JSON.parse() 解析为 JSON 中的数值类型,那么 JSON.parse() 将会把它转换为 JavaScript 的 Number 类型。

这种转换过程中可能会出现精度丢失的问题。因为一旦字符串表示的数值的位数超过16位后,在转换为 Number 类型时就无法保证其精度完整无损。

解决

我们通过正则表达式排除了这种情况,如下所示:

newItem[key] = typeof item[key] === 'string' && /^\d{16,}$/.test(item[key]) ? 
               item[key] : 
               JSON.parse(item[key]);

经过修改并重新验证,问题得到了解决,数据选择弹窗现在可以正确展示已选择状态。

image.png

反思

这个表面上不起眼的BUG为何花费了如此长的时间来排查?除了对大数值的概念不甚了解外,还有一个关键原因是对JavaScript中可能导致大数值失去精度的操作缺乏深入理解。

大数值通常由两种表示方式,一个是用数值类型表示,一个是字符串类型表示。

如果用数值类型表示一个大数值,而且你不能直接修改源代码或源数据,这种情况比较棘手,因为一旦 JavaScript 解析器处理这个数值,它可能已经失去了精度。

这种情况通常发生在你从某个源(比如一个API或者外部数据文件)接收到一个数值类型的大数值,如果数据源头不能修改,只能使用第三方库lossless-json、json-bigint来解决。

如果用字符串类型表示一个大数值,在JS中只要有把其转成Number类型的值就会失去精度,不管是显式转换还是隐式转换。

显式转换,比如 Number()parseInt()parseFloat()Math.floorMath.ceilMath.round等等。

隐式转换,比如除了加法外的算术运算符、JSON.parseswitch 语句、sort的回调函数等等。