前言:
ArrayBuffer类在浏览器端并不会很常用,浏览器端使用该方法,大部分用于处理上传文件的操作(html5中新增了webgl,也会用来写一些喂webgl功能),因此很多前端同学会认为这个语法糖并没有太大的作用。实际来说,在浏览器端大部分需要二进制流操作的部分,都由浏览器端,帮我们解决了,最典型的列子即http请求。如果浏览器端不进行处理,那我们就需要频繁的操作二进制流进行http协议的解析和数据的解析。但ArrayBuffer对node是很常见的,由于node端有大量涉及文件操作的需求,因此掌握ArrayBuffer类还有有足够必要的
一、ArrayBuffer 对象
1、作用:
用来创建二进制数据片段,不能直接读写,只能通过“视图”(TypedArray、DataView
)进行操作。对同一个ArrayBuffer对象生成的内存中,生成多个视图,当对视图进行修改时,内存对象中的数据也会改变。创建ArrayBuffer对象以及视图,计算机只会分配固定的内存空间,而不涉及硬盘空间的写入。
本来,在设计目的上,ArrayBuffer
对象的各种TypedArray
视图,是用来向网卡、声卡之类的本机设备传送数据,所以使用本机的字节序就可以了;而DataView
视图的设计目的,是用来处理网络设备传来的数据,所以大端字节序或小端字节序是可以自行设定的。
2、API:
byteLength:创建的ArrayBuffer内存片段,获取buffer的长度(二进制),如果要分配的内存区域很大,有可能分配失败(因为没有那么多的连续空余内存),所以有必要检查是否分配成功。
if (buffer.byteLength === n) {
// 检测成功
} else {
// 检测失败
}
slice:该方法会拷贝生成新的ArrayBuffer
对象,api其他操作同Array.prototype.slice。slice
方法其实包含两步,第一步是先分配一段新内存,第二步是将原来那个ArrayBuffer
对象拷贝过去
isView:用来表示参数是否为ArrayBuffer
的视图实例。
const buffer = new ArrayBuffer(8);
ArrayBuffer.isView(buffer) // false
const v = new Int32Array(buffer);
ArrayBuffer.isView(v) // true
二、TypedArray
视图
1、作用:
用来对已经创建的ArrayBuffer对象创建视图,视图中可以选择多种数据类型,方便操作各种情况的数据。只能创建一种数据类型的视图
TypedArray 数组的所有成员,都是同一种类型。
TypedArray 数组的成员是连续的,不会有空位。
TypedArray 数组成员的默认值为 0。比如,new Array(10)
返回一个普通数组,里面没有任何成员,只是 10 个空位;new Uint8Array(10)
返回一个 TypedArray 数组,里面 10 个成员都是 0。
TypedArray 数组只是一层视图,本身不储存数据,它的数据都储存在底层的ArrayBuffer
对象之中,要获取底层对象必须使用buffer
属性。
2、API:
constructor:
(1)、使用TypedArray视图时,创建的视图区域开始范围必须与建立视图所需要的位数相同,否则会报错。
(2)、视图还可以不通过ArrayBuffer
对象,直接分配内存而生成。
(3)、TypedArray 数组的构造函数,可以接受另一个TypedArray
实例作为参数。注意,此时生成的新数组,只是复制了参数数组的值,对应的底层内存是不一样的。新数组会开辟一段新的内存储存数据,不会在原数组的内存之上建立视图。
如下代码:
//此处描述超出arrayBufeer位报错
const buffer = new ArrayBuffer(8);//创建了一位8个字节的数据
const i16 = new Int16Array(buffer, 1);
// Uncaught RangeError: start offset of Int16Array should be a multiple of 2
//不通过ArrayBuffer,TypedArray也能够生成内存
const f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];
length:length
属性表示 TypedArray 数组含有多少个成员。注意将byteLength
属性和length
属性区分,前者是字节长度,后者是成员长度。
byteOffset:byteOffset
属性返回 TypedArray 数组从底层ArrayBuffer
对象的哪个字节开始
set:TypedArray 数组的set
方法用于复制数组(普通数组或 TypedArray 数组),也就是将一段内容完全复制到另一段内存。
const a = new Uint8Array(8);
const b = new Uint8Array(8);
b.set(a);
subarray:subarray
方法是对于 TypedArray 数组的一部分,再建立一个新的视图。subarray
方法的第一个参数是起始的成员序号,第二个参数是结束的成员序号(不含该成员),如果省略则包含剩余的全部成员。所以,上面代码的a.subarray(2,3)
,意味着 b 只包含a[2]
一个成员,字节长度为 2。
const a = new Uint16Array(8);
const b = a.subarray(2,3);
a.byteLength // 16
b.byteLength // 2
of:TypedArray 数组的所有构造函数,都有一个静态方法of
,用于将参数转为一个TypedArray
实例。如
Float32Array.of(0.151, -8, 3.7)
from:静态方法from
接受一个可遍历的数据结构(比如数组)作为参数,返回一个基于这个结构的TypedArray
实例。
Uint16Array.from([0, 1, 2])
// Uint16Array [ 0, 1, 2 ]
3、字节序:
字节序指的是数值在内存中的表示方式。由于 x86 体系的计算机都采用小端字节序(little endian),相对重要的字节排在后面的内存地址,相对不重要字节排在前面的内存地址。(理解此处主要是用来对视图中位数转化时使用,位数转化时由于字节序的不同,拿到的最终数据也不不同)
比如,一个占据四个字节的 16 进制数0x12345678
,决定其大小的最重要的字节是“12”,最不重要的是“78”。小端字节序将最不重要的字节排在前面,储存顺序就是78563412
;大端字节序则完全相反,将最重要的字节排在前面,储存顺序就是12345678
。目前,所有个人电脑几乎都是小端字节序,所以 TypedArray 数组内部也采用小端字节序读写数据,或者更准确的说,按照本机操作系统设定的字节序读写数据。
这并不意味大端字节序不重要,事实上,很多网络设备和特定的操作系统采用的是大端字节序。这就带来一个严重的问题:如果一段数据是大端字节序,TypedArray 数组将无法正确解析,因为它只能处理小端字节序!为了解决这个问题,JavaScript 引入DataView
对象,可以设定字节序,
const buffer = new ArrayBuffer(16);
const int32View = new Int32Array(buffer);
for (let i = 0; i < int32View.length; i++) {
int32View[i] = i * 2;
}
// 0 2 4 6
const int16View = new Int16Array(buffer);
for (let i = 0; i < int16View.length; i++) {
console.log("Entry " + i + ": " + int16View[i]);
}
// Entry 0: 0
// Entry 1: 0
// Entry 2: 2
// Entry 3: 0
// Entry 4: 4
// Entry 5: 0
// Entry 6: 6
// Entry 7: 0
// 假定某段buffer包含如下字节 [0x02, 0x01, 0x03, 0x07]
const buffer = new ArrayBuffer(4);
const v1 = new Uint8Array(buffer);
v1[0] = 2;
v1[1] = 1;
v1[2] = 3;
v1[3] = 7;
const uInt16View = new Uint16Array(buffer);
// 计算机采用小端字节序
// 所以头两个字节等于258 258的二进制为0000000100000010 v1[0]的二进制为00000010 v1[1]的二进制
// 为00000001,由于小端字节序,因此拼接成了258,大端字节序拼接为0000001000000001
if (uInt16View[0] === 258) {
console.log('OK'); // "OK"
}
4、BYTES_PER_ELEMENT 每一种视图的构造函数,都有一个BYTES_PER_ELEMENT
属性,表示这种数据类型占据的字节数。如Int8Array.BYTES_PRE_ELEMENT
5、ArrayBuffer与字符串的转换
// ArrayBuffer 转为字符串,参数为 ArrayBuffer 对象
function ab2str(buf) {
// 注意,如果是大型二进制数组,为了避免溢出,
// 必须一个一个字符地转
if (buf && buf.byteLength < 1024) {
return String.fromCharCode.apply(null, new Uint16Array(buf));
}
const bufView = new Uint16Array(buf);
const len = bufView.length;
const bstr = new Array(len);
for (let i = 0; i < len; i++) {
bstr[i] = String.fromCharCode.call(null, bufView[i]);
}
return bstr.join('');
}
// 字符串转为 ArrayBuffer 对象,参数为字符串
function str2ab(str) {
const buf = new ArrayBuffer(str.length * 2); // 每个字符占用2个字节
const bufView = new Uint16Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
6、溢出规则
- 正向溢出(overflow):当输入值大于当前数据类型的最大值,结果等于当前数据类型的最小值加上余值,再减去 1。
- 负向溢出(underflow):当输入值小于当前数据类型的最小值,结果等于当前数据类型的最大值减去余值的绝对值,再加上 1。
三、DataView
视图
1、作用:
用来对已经创建的ArrayBuffer对象创建混合视图,能够针对当前的片段建立多种不同的类型的数据,该视图方便对各种文件类型解析(文件(如.js,.mp3等)本质上就是通过,定义好的文件结构,生成对应的文件,文件中通常会有很多的隐藏位,用来对文件描述,隐藏位中也通常会采用多种数据类型进行定义)。该视图可以设置字节序。
2、API:
buffer:返回对应的 ArrayBuffer 对象
byteLength:返回占据的内存字节长度
byteOffset:返回当前视图从对应的 ArrayBuffer 对象的哪个字节开始
getInt8、getUint8等:获取一个指定进制的字节。第一个参数指定开始位置,第二个参数为字节序,true为小端字节序,false为大端字节序(默认)
setInt8、setUint8等:设置字节。
第一个参数是字节序号,表示从哪个字节开始写入,第二个参数为写入的数据。第三个参数为字节序
四、SharedArrayBuffer(共享视图)
1、作用:ES2017 引入SharedArrayBuffer
,允许 Worker 线程与主线程共享同一块内存。SharedArrayBuffer
的 API 与ArrayBuffer
一模一样,唯一的区别是后者无法共享数据。
// 主线程
// 新建 1KB 共享内存
const sharedBuffer = new SharedArrayBuffer(1024);
// 主线程将共享内存的地址发送出去
w.postMessage(sharedBuffer);
// 在共享内存上建立视图,供写入数据
const sharedArray = new Int32Array(sharedBuffer);
// Worker 线程
onmessage = function (ev) {
// 主线程共享的数据,就是 1KB 的共享内存
const sharedBuffer = ev.data;
// 在共享内存上建立视图,方便读写
const sharedArray = new Int32Array(sharedBuffer);
// ...
};
五、Atomics对象
1、作用:
Atomics
对象,保证所有共享内存的操作都是“原子性”的,并且可以在所有线程内同步,它可以保证一个操作所对应的多条机器指令,一定是作为一个整体运行的,中间不会被打断。也就是说,它所涉及的操作都可以看作是原子性的单操作,这可以避免线程竞争,提高多线程共享内存时的操作安全。
2、API:
store:用来在共享内存中写入数据,接受三个参数:SharedBuffer 的视图、位置索引和值,返回sharedArray[index]
的值
load:用来在共享内存中读取数据,接受两个参数:SharedBuffer 的视图和位置索引,也是返回sharedArray[index]
的值
store和load,把多个线程使用共享内存的某个位置作为开关(flag),一旦该位置的值变了,就执行特定操作。这时,必须保证该位置的赋值操作,一定是在它前面的所有可能会改写内存的操作结束后执行;而该位置的取值操作,一定是在它后面所有可能会读取该位置的操作开始之前执行。store
方法和load
方法就能做到这一点,编译器不会为了优化,而打乱机器指令的执行顺序。
// 主线程 main.js
ia[42] = 314159; // 原先的值 191
Atomics.store(ia, 37, 123456); // 原先的值是 163
// Worker 线程 worker.js
while (Atomics.load(ia, 37) == 163);
console.log(ia[37]); // 123456
console.log(ia[42]); // 314159
exchange:该方法同store方法一样,都会设置值,不同的是该方法会返回替换前的值
wait:
包括四个参数,第一个参数为共享内存的视图数组,第二个参数为视图数据的位置(从0开始),第三个参数为该位置的预期值。一旦实际值等于预期值,就进入休眠,第四个参数为整数,表示过了这个时间以后,就自动唤醒,单位毫秒。该参数可选,默认值是Infinity
,即无限期的休眠,只有通过Atomics.wake()
方法才能唤醒。
返回值是一个字符串,共有三种可能的值。如果sharedArray[index]
不等于value
,就返回字符串not-equal
,否则就进入休眠。如果Atomics.wake()
方法唤醒,就返回字符串ok
;如果因为超时唤醒,就返回字符串timed-out
。
wake:
包括三个参数,第一个参数为共享内存的视图数组,第二个参数为视图数据的位置(从0开始),第三个参数为需要唤醒的 Worker 线程的数量,默认为Infinity
。
// 主线程
console.log(ia[37]); // 163
Atomics.store(ia, 37, 123456);
Atomics.wake(ia, 37, 1);//唤起ia中第37个数据中睡眠的一个线程
// Worker 线程
Atomics.wait(ia, 37, 163);//当ia中第37个数据为163的时候,线程睡眠
console.log(ia[37]); // 123456
add、sub、and、or、xor:为了避免线程共享问题,因此提供了几个计算方法,同样接受三个参数,共享视图,位置以及值,用于将已经存在的值与传递的新值进行计算。返回旧值
compareExchange:
Atomics.compareExchange(sharedArray, index, oldval, newval)
:如果sharedArray[index]
等于oldval
,就写入newval
,返回oldval
Atomics.compareExchange
的一个用途是,从 SharedArrayBuffer 读取一个值,然后对该值进行某个操作,操作结束以后,检查一下 SharedArrayBuffer 里面原来那个值是否发生变化(即被其他线程改写过)。如果没有改写过,就将它写回原来的位置,否则读取新的值,再重头进行一次操作
isLockFree:
Atomics.isLockFree(size)
:返回一个布尔值,表示Atomics
对象是否可以处理某个size
的内存锁定。如果返回false
,应用程序就需要自己来实现锁定。