站会
在一次日常站会上,组员们轮流分享昨天的工作进展。一个组员提到:“昨天我整天都在排查一个BUG,今天还得继续。”
出于好奇,我问:“是什么BUG让你排查了这么久还没解决呢?”
他解释说:“是关于一个数据选择弹窗的问题。这个弹窗用表格展示数据,并且表格具有选择功能。问题在于,编辑这个弹窗时,表格中原本应该显示为已选状态的数据并没有正确显示已选状态。”
我猜测道:“是不是因为表格中数据的主键ID是大数值导致的?”
他回答说:“大数值?我不太确定。”
我有些质疑地问:“那你昨天都是怎么排查的?需要花一整天的时间,难道是在摸鱼吗?”
“没有摸鱼,只是这个BUG真得有点难搞,那个什么是大数值?”
“行吧,姑且信你,我待会给你看看。”
排查
表格使用的是 Ant Design 4.0 提供的 Table
组件。我检查了组件的 rowKey
属性配置,如下所示:
<Table rowKey={record => record.obj_id}></Table>
这表明表格行的 key
是通过数据中的 obj_id
字段来指定的。随后,我进一步查看了服务端返回的数据。
可以看到一条数据中的 obj_id
字段值为 "898186844400803840"
,这是一个18位的数值。
在ES6(ECMAScript 2015)之前,JavaScript没有专门的整数类型,所有的数字都被表示为双精度64位浮点数(遵循IEEE 754标准)。这意味着在这种情况下,JavaScript能够安全地表示的整数范围是从−253+1-2^{53} + 1−253+1到253−12^{53} - 1253−1(即-9,007,199,254,740,991到9,007,199,254,740,991)。可以简单地认为超过16位的数值就是大数值。
JavaScript中很多操作处理大数值时会导致大数值失去精度。比如 Number("898186844400803840")
可以看到 "898186844400803840"
和 "898186844400803800"
的区别在第16位后,从 40
变成 00
这就是大数值失去精度的表现。
在看一下表格的数据展示,如下图所示:
可以确定的是,从服务端返回的数据到在表格中的渲染过程是没有问题的。那么,可能出现问题的地方还有两个:一是在选择数据后,数据被传递到父组件的过程中;二是父组件将已选数据发送回选择数据组件的过程中。
定位
我检查了他将数据传递给父组件的逻辑代码,发现了一个可疑点。
在上述代码中,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]);
经过修改并重新验证,问题得到了解决,数据选择弹窗现在可以正确展示已选择状态。
反思
这个表面上不起眼的BUG为何花费了如此长的时间来排查?除了对大数值的概念不甚了解外,还有一个关键原因是对JavaScript中可能导致大数值失去精度的操作缺乏深入理解。
大数值通常由两种表示方式,一个是用数值类型表示,一个是字符串类型表示。
如果用数值类型表示一个大数值,而且你不能直接修改源代码或源数据,这种情况比较棘手,因为一旦 JavaScript 解析器处理这个数值,它可能已经失去了精度。
这种情况通常发生在你从某个源(比如一个API或者外部数据文件)接收到一个数值类型的大数值,如果数据源头不能修改,只能使用第三方库lossless-json、json-bigint来解决。
如果用字符串类型表示一个大数值,在JS中只要有把其转成Number类型的值就会失去精度,不管是显式转换还是隐式转换。
显式转换,比如 Number()
、parseInt()
、parseFloat()
、Math.floor
、Math.ceil
、Math.round
等等。
隐式转换,比如除了加法外的算术运算符、JSON.parse
、switch
语句、sort
的回调函数等等。