原来前端二进制数组有这么多门道

一. 前言

日常的开发工作中,或多或少都会接触到一些高性能,大数据处理相关的场景(3D渲染、WebGL),这时候用传统的js来处理这些数据就会显的捉襟见肘。我们前端开发迫切的需要一种新的方法新的技术手段来处理对应的问题,二进制数组就是一个不错的选择。

正好自己这段时间在搞大文件上传的问题,顺带了解了一下二进制数组相关知识。这里分享一下哈!有问题的地方,欢迎各位jym指出!

二. 简介

2.1 诞生流程

简单点来说,二进制数组是js操作二进制数据的接口。

传统的js处理数据是文本格式,这会牵涉到一个性能问题,如果与显卡等底层驱动数据交互的时候,性能就会很差。

为了满足js显卡之间的大量数据实时通信,两者之间的通信必须是 二进制 (二进制在传输过程中不像传统文本格式(XML、JSON)需要复杂的转码过程)。在这个过程中,JS需要提供一个接口,可以直接操作二进制数据,保证操作前后数据格式不发生变化。这种流程下,JS脚本的性能将会大大提高。

2.2 类型

二进制数组由三种对象构成:

  • ArrayBuffer对象 :是 JS 中用于表示固定长度二进制数据的底层对象,创建后无法调整大小,需通过视图进行读写操作。

这里必须要了解的一点: ArrayBuffer对象作为内存区域可以存放多种类型的数据。不同的数据类型有不同的解读方式 。ArrayBuffer有两种类型的视图,一种是类型化数组视图(TypedArray),另一种是数据视图(dataView)。

  • TypedArray视图 : 解读的类数组类型必须为同一类型(可以看为简单类型)。
  • DataView视图 :可以解读不同类型的类数组(可以看为复杂类型)。

下面这些是TypedArrayDataView支持的数据类型。

PS:

  • 除了一些3D场景,常见的Canvas、File API、XHR等也用到了二进制数组。
  • 二进制数组其实不是数组,它是类数组对象。

三.API介绍和使用

3.1 ArrayBuffer对象

3.1.1 介绍

ArrayBuffer代表的是存储二进制数据的一段内存

ArrayBuffer是一个构造函数,通过new关键词创建。可以分配一块连续的内存用于存储二进制数据

js 复制代码
const buffer = new ArrayBuffer(32)  // 分配一个32字节的内存。每个字节都默认为0

这里注意一下:分配的单位是字节

3.1.2 方法

ArrayBuffer.prototype.byteLength

ArrayBuffer实例的byteLength属性,返回所分配的内存区域的字节长度。

js 复制代码
const buffer = new ArrayBuffer(32);
buffer.btyeLength  // 32

当我们想要分配一段很大的内存空间时,可以用来检测是否分配成功。

js 复制代码
const buffer = new ArrayBuffer(n)

if (n === buffer.btyeLength) {
    // success
}

ArrayBuffer.prototype.slice()

slice方法,允许对ArrayBuffer分配的内存进行切割截取。然后形成一个新的ArrayBuffer对象

js 复制代码
const buffer1 = new ArrayBuffer(32)
const buffer2 = buffer1.slice(0, 4)  // 从 0 开始, 到 3 结束。(不包含末尾)
console.log(buffer1)
console.log(buffer2)

可以看出,slice并不会改变原始ArrayBuffer分配的内存。

slice方法,是开辟了一个新的内存空间,然后讲截取的部分复制过去。

3.2 TypedArray视图

上面介绍到了,视图就是对ArrayBuffer开辟的内存进行解读操作同一段内存,因为数据类型的不同,有不同种的解读方式

TypedArray视图,要保证ArrayBuffer中的成员都是同一类型

3.2.1 介绍

目前,市面上TypedArray视图一共包括以下几种类型(比较常用的)。

  • Int8Array8位有符号整数,length === 1(字节
  • Unit8Array8位无符号整数, length === 1(字节
  • Uint8ClampedArray8位无符号整数,length === 1(字节),溢出处理不同
  • Int16Array16位有符号整数,length === 2(字节
  • Uint16Array16位无符号整数,length === 2(字节
  • Int32Array32位有符号整数,length === 4(字节
  • Uint32Array32位无符号整数,length === 4(字节
  • Float32Array32位浮点数,length === 4(字节
  • Float64Array64位浮点数,length === 8(字节

通过这些构造函数生成的数组,统一称之为TypedArray视图。这些数组有以下几个特性:

  • 本身不存储数据。
  • 内部类型都是一种。
  • 数组成员是连续的。
  • 数组成员大小默认都为0

3.2.2 用法

TypedArray(buffer, start, length)

  • start:开始字节位置
  • length:字节长度

举几个例子:

js 复制代码
const buffer = new ArrayBuffer(32);
const t1 = new Int32Array(buffer);  // 创建一个指向buffer的int32类型视图,开始位置默认为0,长度整个buffer
js 复制代码
const t2 = new Int32Array(buffer, 0, 4)  // 开始于字节0,长度为4
js 复制代码
const t2 = new Int32Array(buffer, 4, 4)  // 开始于字节4,长度为4

除了上面这些用法,构造函数还可以直接接受数,生成TypedArray实例。

javascript 复制代码
const t5 = new Uint8Array([255, 255, 255, 255]);  // 把一个普通数组,变成8位无符号整数的TypedArray实例

如果你经常弄Canvas的,话这种结构应该很熟悉!

我们操作TypedArray数组的话,方法和数组方法基本是通用的。下面写一个简单的附值的例子。

js 复制代码
const buffer = new ArrayBuffer(8);
const t1 = new Uint8Array(buffer); 
t1[0] = 255;
t1[1] = 255;
// [255, 255, 0, 0, 0, 0, 0, 0]

对于这种TypeArray数组,我们也可以转为真正的数组

js 复制代码
Array.from(t1)
Array.prototype.slice.call(t1)

同时,TypeArray数组也部署了Iterator接口。支持使用for...of遍历

我们一层一层点开它的原型,确实有。

ps:注意一下,TypedArray数组没有concat方法

TypedArray.prototype.buffer

TypedArray实例的buffer属性,返回整段内存区域对应的ArrayBuffer对象。该属性为只读属性。

3.2.3 BYTES_PER_ELEMENT属性

每一种视图的构造函数,都有一个BYTES_PER_ELEMENT属性,表示这种数据类型占据的字节。

js 复制代码
Int8Array.BYTES_PER_ELEMENT // 1
Uint8Array.BYTES_PER_ELEMENT // 1
Int16Array.BYTES_PER_ELEMENT // 2
Uint16Array.BYTES_PER_ELEMENT // 2
Int32Array.BYTES_PER_ELEMENT // 4
Uint32Array.BYTES_PER_ELEMENT // 4
Float32Array.BYTES_PER_ELEMENT // 4
Float64Array.BYTES_PER_ELEMENT // 8

3.2.4 边界和溢出

每种视图的构造函数都有固定的范围。比如Unit8Array,范围就是0~255。超过边界就会溢出。

js 复制代码
const t = new Uint8Array([255, 256, 0, 0])   // [255, 0, 0, 0]

// 这里的256就超过了范围。

从网上找到一张表,这里面写的很详细。

3.3 字节序

3.3.1 介绍

其实对于前端开发,字节序的了解真的不多。字节序指的是数值在内存中的表示方式

js 复制代码
let buffer = new ArrayBuffer(16);
let int32View = new Int32Array(buffer);

上面这个代码,生成了一个16字节的内存(ArrayBuffer对象)。然后在他基础上创建一个Int32Array,带符号的32位整数视图,每个32位整数占据 32 / 8 = 4个字节。所以一共可以写入4个32位整数

我们往里面写入一点数据:

js 复制代码
int32View[0] = 0
int32View[1] = 2
int32View[2] = 4
int32View[3] = 6   // Int32Array(4) [0, 2, 4, 6]

这时候我们用Int16Array去创建视图,结果完全不同。

js 复制代码
var int16View = new Int16Array(buffer);
 // Int16Array(8) [0, 0, 2, 0, 4, 0, 6, 0]

因为是16位整数视图,所以每个16位占 16 / 8 = 2 个字节。一共可以写入8个16位整数。为什么读出来的结果是2在前6在后呢?因为x86体系的计算机都采用小端字节序(little endian)

3.3.2 小端字节序

相对重要的字节排在后面的内存地址,相对不重要字节排在前面的内存地址

对于我们前端而言,TypedArray就是小端,DataView大端小端都可以自行设计

3.4 DataView视图

当大量数据出现不同的时候,DataView这种视图就可以使用上了。

3.4.1 介绍

简单使用,其实类似于TypedArray

js 复制代码
const buffer = new ArrayBuffer(24);
const dv = new DataView(buffer);

3.4.2 使用

DataView视图,通过以下几种方法读取和写入内存。

读取内存

  • getInt8:读取1个字节,返回一个8位整数。
  • getUint8:读取1个字节,返回一个无符号的8位整数。
  • getInt16:读取2个字节,返回一个16位整数。
  • getUint16:读取2个字节,返回一个无符号的16位整数。
  • getInt32:读取4个字节,返回一个32位整数。
  • getUint32:读取4个字节,返回一个无符号的32位整数。
  • getFloat32:读取4个字节,返回一个32位浮点数。
  • getFloat64:读取8个字节,返回一个64位浮点数。

写入内存

  • setInt8:写入1个字节的8位整数。
  • setUint8:写入1个字节的8位无符号整数。
  • setInt16:写入2个字节的16位整数。
  • setUint16:写入2个字节的16位无符号整数。
  • setInt32:写入4个字节的32位整数。
  • setUint32:写入4个字节的32位无符号整数。
  • setFloat32:写入4个字节的32位浮点数。
  • setFloat64:写入8个字节的64位浮点数。

这个DataView,我确实用的不多,这里就不过多描述了。有时间多研究研究。

四. 使用场景

4.1 Canvas

Canvas元素设计到处理色彩rgb等问题,都是要处理内部二进制数组的。

js 复制代码
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

这里这个imageData就是二进制数组。

4.2 WebSocket

WebSocket也可以传输二进制数据。

js 复制代码
let ws = new WebSocket('ws://127.0.0.1:8080');
ws.binaryType = 'arraybuffer';   // 设置类型

// 链接打开,发送二进制数据
ws.addEventListener('open', () => {
    let t1 = new Uint8Array(4);
    ws.send(t1.buffer);
})

ws.addEventListener('message', function (event) {
  let arrayBuffer = event.data;   // 获取数据
});

4.3 File

File可以通过readAsArrayBuffer读取二进制文件。

js 复制代码
const up = document.getElementById('input');
const file = up.files[0];

const reader = new FileReader();   // 读取对象
reader.readAsArrayBuffer(file);  // 
相关推荐
天天摸鱼的java工程师几秒前
谈谈你对 Seata 的理解?8 年 Java 开发:从业务踩坑到源码级解析(附实战代码)
java·后端·面试
华仔啊4 分钟前
SpringBoot+MySQL+Vue实现文件共享系统
java·前端·后端
绝无仅有9 分钟前
常用 Kubernetes (K8s) 命令指南
后端·面试·github
穷儒公羊21 分钟前
第二章 设计模式故事会之策略模式:魔王城里的勇者传说
python·程序人生·设计模式·面试·跳槽·策略模式·设计规范
页面仔Dony24 分钟前
流式数据获取与展示
前端·javascript
似水流年流不尽思念30 分钟前
Spring Bean有哪些生命周期回调方法?有哪几种实现方式?
后端·spring·面试
张志鹏PHP全栈32 分钟前
postcss-px-to-viewport如何实现单页面使用?
前端
恋猫de小郭33 分钟前
iOS 26 正式版即将发布,Flutter 完成全新 devicectl + lldb 的 Debug JIT 运行支持
android·前端·flutter
前端进阶者1 小时前
electron-vite_20外部依赖包上线后如何更新
前端·javascript·electron
uhakadotcom1 小时前
如何安装和使用开源的Meilisearch
后端·面试·github