基于现在市面上到处都是 Vue/React 之类的源码分析文章实在是太多了。(虽然我也写过 Vite的源码解析
所以这次来写点不一样的。由于微信这边用的是 protobuf 来进行 rpc 调用。所以有时候需要将 JS 中的 Number 类型转换为 Long 类型传给后端。目前用的最多的就是 Long.js 了,然而市面上分析这个库的文章少的出奇。唯一能搜到的一篇讲的也非常的简略 Long.js源码分析与学习
其实想要弄懂它的源码还是非常难的。需要十分了解原码 反码 补码 的相关知识知道计算机进行四则运算的原理以及了解 IEEE754 浮点数标准。不过懂了之后对位运算的理解以及 JS中数字的存储规则以及为什么 0.1+0.2 = 0.30000000000000004 这种问题都有了完美的答案了。我 fork 下来的仓库是https://github.com/zhangyuang/long.js-design/blob/master/src/long.js
其中包含了我这篇文章所写的中文注释
下一步是写 Rust 版本的 long.js 来看看能否提升性能,虽然 long.js 本身已经用了 wasm,看起来提升空间不大
简介
A Long class for representing a 64 bit two's-complement integer value derived from theClosure Libraryfor stand-alone use and extended with unsigned support.
顾名思义。long.js 使用了 高位(high) 低位 (low) 分别是32位有符号数补码的形式来表示一个64位数。这是因为在 js 的世界中 最大的安全数的范围是 `-2^53 -1 ~ 2^53-1` 并且在进行位运算的时候 js 会先将数字转为 32位有符号数补码在进行运算。
IEEE754
Please read this article for understand IEEE754
32位浮点数存储规则
64为浮点数存储规则
为什么最大的安全数字是 2^53 - 1
As IEEE754 says, double type variable has 1 sign, 11 exponent, 52 fraction。
Due to Number 1 is the value beginning default, so the exponent max value is 53. why MAX_SAFE_INTEGER is 2^53 - 1 because if number is more than 2^53 such as 2^53 and 2^53 + 1 whose fraction will be same.
根据 IEEE754 标准。64为双精度浮点数的存储规则由1位符号位,11位指数位,52位数值位组成。同时IEEE754规定尾数第一位隐含为1不写,所以一共有53位。为什么超过了 2^53 -1 就是不安全的。因为超出了的数字在二进制的表示基础上,总有另一个数字的二进制表示跟它一样。所以无法区分它们之间的关系的正确性。
reference
【算法】解析IEEE 754 标准www.cnblogs.com源码解析
fromInt
function fromInt(value, unsigned) {// 32 位数转 long类型// 对超出32位数的情况以及负数的情况都做了特殊处理// Long { low: 16777216, high: 0, unsigned: false }var obj, cachedObj, cache;if (unsigned) {// 无符号数value >>>= 0; // 无符号右移0保证得到的是无符号数,if (cache = (0 <= value && value < 256)) {// 只缓存0-256之间的结果cachedObj = UINT_CACHE[value];if (cachedObj)return cachedObj;}// 这里|0是因为位运算会转成32位有符号数补码在进行计算。// 保证得到的数字的正负值的正确性如果超出 Math.pow(2,31) -1 则为负数obj = fromBits(value, (value | 0) < 0 ? -1 : 0, true); if (cache)UINT_CACHE[value] = obj;return obj;} else {// 有符号数// 如果value大于2147483647则得到的结果为负数如 2147483648 Long { low: -2147483648, high: -1, unsigned: false }value |= 0;if (cache = (-128 <= value && value < 128)) {// 只缓存-128到128之间的结果cachedObj = INT_CACHE[value];if (cachedObj)return cachedObj;}obj = fromBits(value, value < 0 ? -1 : 0, false);if (cache)INT_CACHE[value] = obj;return obj;}
}
fromNumber
function fromNumber(value, unsigned) {if (isNaN(value))// 不合法数字返回有符号0/无符号0return unsigned ? UZERO : ZERO;if (unsigned) {if (value < 0)// 如果是无符号且传入负数则返回0return UZERO;if (value >= TWO_PWR_64_DBL)// 如果数字大于等于 Math.pow(2,64) 即 64 位无符号数能表示的最大值 Math.pow(2,64)-1// 返回最大的无符号数 即 fromBits(0xFFFFFFFF|0, 0xFFFFFFFF|0, true); Long { low: -1, high: -1, unsigned: true }return MAX_UNSIGNED_VALUE;} else {// 有符号数最大值是Math.pow(2,63)-1最小值是-Math.pow(2,63)if (value <= -TWO_PWR_63_DBL)return MIN_VALUE; // fromBits(0, 0x80000000|0, false); Long {low: 0, high: -2147483648, unsigned: false}if (value >= TWO_PWR_63_DBL - 1) // 这里改成 -1 更容易理解return MAX_VALUE; // fromBits(0xFFFFFFFF, 0x7FFFFFFF|0, false); Long {low: -1, high: 2147483647, unsigned: false}}if (value < 0)return fromNumber(-value, unsigned).neg(); // 如果是负数则先转换为正数再取反+1return fromBits((value % TWO_PWR_32_DBL) | 0, (value / TWO_PWR_32_DBL) | 0, unsigned); // 分成2个32位有符号数来表示
}
fromString
function fromString(str, unsigned, radix) {if (str.length === 0)throw Error('empty string');if (str === "NaN" || str === "Infinity" || str === "+Infinity" || str === "-Infinity") // 非法字符串返回0return ZERO;if (typeof unsigned === 'number') {// 输入的无符号标志为number则赋值给radix// For goog.math.long compatibilityradix = unsigned,unsigned = false;} else {unsigned = !! unsigned;}radix = radix || 10; // 默认是10进制if (radix < 2 || 36 < radix)throw RangeError('radix');var p;if ((p = str.indexOf('-')) > 0) // 如果字符串包含 - 则抛错throw Error('interior hyphen');else if (p === 0) {// 如果是负数,则返回对应正数的取反+1值return fromString(str.substring(1), unsigned, radix).neg();}// Do several (8) digits each time through the loop, so as to// minimize the calls to the very expensive emulated div.var radixToPower = fromNumber(pow_dbl(radix, 8)); // Math.pow(radix, 8)var result = ZERO;for (var i = 0; i < str.length; i += 8) {var size = Math.min(8, str.length - i), // 取剩下的长度和8的更小值value = parseInt(str.substring(i, i + size), radix); // 每次处理8位字符串转换为number类型如果不足8位则取剩下的所有字符if (size < 8) {var power = fromNumber(pow_dbl(radix, size)); //如果剩下的数字不足8位,则先乘以剩下的数字的位数result = result.mul(power).add(fromNumber(value)); // 再把结果加上数字本身} else {// 如果长度大于8则处理// 之前处理的结果先乘以Long { low: 100000000, high: 0, unsigned: false }result = result.mul(radixToPower);result = result.add(fromNumber(value)); // 加上这次的数字}}result.unsigned = unsigned;return result;
}
add
LongPrototype.add = function add(addend) {// Long 对象相加// 把每个64位对象分成4块。每16位为一块// 如果不分块32位为一块,当结果溢出时会丢失进位if (!isLong(addend))addend = fromValue(addend);// Divide each number into 4 chunks of 16 bits, and then sum the chunks.var a48 = this.high >>> 16; // 得到高位的前16位var a32 = this.high & 0xFFFF; // 得到高位的后16位var a16 = this.low >>> 16; // 得到低位的前16位var a00 = this.low & 0xFFFF; // 得到低位的后16位var b48 = addend.high >>> 16;var b32 = addend.high & 0xFFFF;var b16 = addend.low >>> 16;var b00 = addend.low & 0xFFFF;var c48 = 0, c32 = 0, c16 = 0, c00 = 0;c00 += a00 + b00;c16 += c00 >>> 16; // 低位后16位相加,然后右移16位。只保留超过16位的进位c00 &= 0xFFFF; // 只保留后16位c16 += a16 + b16; // 同样重复上述操作。每次对16位进行运算。保留进位,同时本身只保留16位c32 += c16 >>> 16;c16 &= 0xFFFF;c32 += a32 + b32;c48 += c32 >>> 16;c32 &= 0xFFFF;c48 += a48 + b48;c48 &= 0xFFFF;// 低位的前16位左移然后与上低位的后16位,得到一个正确的32位有符号数return fromBits((c16 << 16) | c00, (c48 << 16) | c32, this.unsigned);
};
multiply
LongPrototype.multiply = function multiply(multiplier) {// Long 类型相乘if (this.isZero())return ZERO;if (!isLong(multiplier))multiplier = fromValue(multiplier);// use wasm support if presentif (wasm) {// 使用wasm提升性能var low = wasm["mul"](this.low,this.high,multiplier.low,multiplier.high);return fromBits(low, wasm["get_high"](), this.unsigned);}if (multiplier.isZero())// 被乘数是0则返回0return ZERO;if (this.eq(MIN_VALUE))// 如果被乘数是奇数则返回最小值,否则返回0return multiplier.isOdd() ? MIN_VALUE : ZERO;if (multiplier.eq(MIN_VALUE))return this.isOdd() ? MIN_VALUE : ZERO;if (this.isNegative()) {if (multiplier.isNegative())return this.neg().mul(multiplier.neg());elsereturn this.neg().mul(multiplier).neg();} else if (multiplier.isNegative())return this.mul(multiplier.neg()).neg();// If both longs are small, use float multiplicationif (this.lt(TWO_PWR_24) && multiplier.lt(TWO_PWR_24))return fromNumber(this.toNumber() * multiplier.toNumber(), this.unsigned);// Divide each long into 4 chunks of 16 bits, and then add up 4x4 products.// We can skip products that would overflow.// 同样把 Long 对象分为4块var a48 = this.high >>> 16; // 高位的前16位var a32 = this.high & 0xFFFF; // 高位的后16位var a16 = this.low >>> 16;var a00 = this.low & 0xFFFF;var b48 = multiplier.high >>> 16;var b32 = multiplier.high & 0xFFFF;var b16 = multiplier.low >>> 16;var b00 = multiplier.low & 0xFFFF;var c48 = 0, c32 = 0, c16 = 0, c00 = 0;c00 += a00 * b00;c16 += c00 >>> 16; // c16默认为加上低位后16位相乘的进位c00 &= 0xFFFF; // 只保留后16位c16 += a16 * b00; // 被乘数的后16位与乘数的前16位相乘并且加上之前的进位c32 += c16 >>> 16; // 加上结果的进位c16 &= 0xFFFF;c16 += a00 * b16;c32 += c16 >>> 16;c16 &= 0xFFFF;c32 += a32 * b00;c48 += c32 >>> 16;c32 &= 0xFFFF;c32 += a16 * b16;c48 += c32 >>> 16;c32 &= 0xFFFF;c32 += a00 * b32;c48 += c32 >>> 16;c32 &= 0xFFFF;// 高位的前16位,再加上前面的进位之后再加上本值。溢出的进位则舍去不考虑。// 因为最终的结果还是 4 chunks组成的对象。所以不需要考虑 a48 * b16 这样的结果,因为超出了64位的范畴会舍去c48 += a48 * b00 + a32 * b16 + a16 * b32 + a00 * b48;c48 &= 0xFFFF;return fromBits((c16 << 16) | c00, (c48 << 16) | c32, this.unsigned);
};
neg
LongPrototype.negate = function negate() {if (!this.unsigned && this.eq(MIN_VALUE))return MIN_VALUE;return this.not().add(ONE); // 取反+1,原码->补码
};