ArrayBuffer & DataView

434 阅读4分钟

ArrayBuffer & DataView

在网络I/O(最常见的就是读取HTTP响应时),或者在一些3D图形(WebGL)应用中,都会涉及到ArrayBuffer相关的内容。

1. 与数组的差异

Javascript中,有数组的概念,有学过C的同学,也会接触到C语言中的数组。但本质上,这两种数组不是同一个东西。

  • 在C语言中,在调用栈定义数组,是在内存中申请一段连续的空间,这段连续空间的首地址,就是数组的名字
  • 在Javascript中,数组与底层的内存没有关系,甚至某些情况下,数组内存中并不是连续的。

内存不连续产生的问题

  1. 在读取文件时(包括网络I/O,也是读取特殊的“文件”),需要使用内存连续的缓冲区(Buffer)

缓冲区在一些语言中,并不是一个特殊概念,就是一个简单的定长字节数组,在读取文件时,每次读取缓冲区大小的字节数据,然后处理缓冲区中的数据,处理完成后继续读取,直到文件读取完成。

但是在Javascript中,并不是传统意义上的数组,或者叫顺序表。在Javascript中,我们用起来感觉像是一个数组,但它并不是连续的字节数组(顺序表),普通的数组就不能用来做缓冲区,因此引入了ArrayBuffer

  1. 在3D图形编程时,也需要缓冲区

在使用WebGL进行图形编程时,需要给GPU传递一系列的顶点信息,包括:顶点的位置、向量、UV映射等等信息。CPU和GPU通信本身就是一个较慢的过程,因此,不可能一个顶点一个顶点的传送,都是放在内存连续的缓冲区里一次性传过去。

2. 创建一个ArrayBuffer

最常见的使用场景是在I/O(读取HTTP响应、读取文件)时,会有相应的方法得到存储了数据的ArrayBuffer。

也可以直接使用构造器进行创建:

new ArrayBuffer(length: number)

type Options = {
    maxByteLength: number
}
new ArrayBuffer(length:number, options: Options)

构造器有两个参数:

  1. 用来指定这个arraybuffer有多少个字节
  2. options是一个实验性的内容,可以用来创建可变长度的arraybuffer

对于ArrayBuffer,不会直接对它本身进行操作,而是通过DataViewTypedArray操作。

3. 通过DataView操作ArrayBuffer

DataView提供了对于ArrayBufffer各种类型的读写操作,最重要的是可以不用关心平台的端序。默认的读写都是大端序,也可以通过参数指定使用小端序。关于端序的内容,如果想要了解,可以看端序(字节序) - 掘金 (juejin.cn)

创建一个DataView

DataView(buffer: ArrrayBuffer, byteOffset?: number, byteLength?: number)

DataView的构造器有三个参数:

  1. buffer,被操作的ArrayBuffer实例
  2. byteOffset,从第几个字节开始,默认情况是从第一个字节开始
  3. byteLength,要操作的字节长度
const buffer = new ArrayBuffer(10)

/** 操作整个arrayBuffer */
const dataview = new DataView(buffer)
const dataview = new DataView(buffer, 0)
const dataview = new DataView(buffer, 0, 10)
// 上面三种的创建方式是等价的
// byteOffset: 0; byteLength: 10

/** 操作部分arrayBuffer */
const dataview = new DataView(buffer, 2)
// byteOffset: 2; byteLength: 10

const dataview = new DataView(buffer, 2, 4)
// byteOffset: 2; byteLength: 4

对于设置byteOffsetbyteLength,如果越界,会抛出异常:

const buffer = new ArrayBuffer(10)
const dataview = new DataView(buffer, 2, 10)
// Uncaught RangeError: Invalid DataView length 10

因为buffer本身的长度只有10,而创建DataView时,却包含了不存在的buffer[10]buffer[11]

DataView创建完成,就可以使用它提供的实例方法了。方法有很多,但都很有规律,都是成对出现的getXXX()setXXX()

getXXX(byteOffset, littleEndian = false)有两个参数:

  • 第一个参数是字节偏移,和getXXXXXX类型是无关的,并不是说Uint8的偏移是1字节,Uint16的偏移是两个字节。
  • 第二个是一个布尔值,表示是否使用小端序,默认使用大端序。

setXXX(byteOffset, value, littleEndian = false)有三个参数: 其中的byteOffsetlittleEndiangetXXX一样,就不再重复了。多出来的value就是要设置的值。

实例代码:

const buffer = new ArrayBuffer(4)
const dataview = new DataView(buffer)

dataview.setUint16(0, 0x0102)
dataview.setUint16(1, 0x0304)

// 结果
// buffer: 1 3 4 0

dataview.getUint16(0)
// 0x0103 => 1 * 2^8 + 3 = 259

dataview.getUint16(1)
// 0x0304 => 3 * 2^8 + 4 = 772