Deno bytes 模块全解析

一、基础知识

1.1 ArrayBuffer

ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。ArrayBuffer 不能直接操作,而是要通过类型数组对象DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。

ArrayBuffer 简单说是一片内存,但是你不能直接用它。这就好比你在 C 里面,malloc 一片内存出来,你也会把它转换成 unsigned_int32 或者 int16 这些你需要的实际类型的数组/指针来用。

这就是 JS 里的 TypedArray 的作用,那些 Uint32Array 也好,Int16Array 也好,都是给 ArrayBuffer 提供了一个 “View”,MDN 上的原话叫做 “Multiple views on the same data”,对它们进行下标读写,最终都会反应到它所建立在的 ArrayBuffer 之上。

来源:https://www.zhihu.com/question/30401979

语法
new ArrayBuffer(length)
  • 参数:length 表示要创建的 ArrayBuffer 的大小,单位为字节。
  • 返回值:一个指定大小的 ArrayBuffer 对象,其内容被初始化为 0。
  • 异常:如果 length 大于 Number.MAX_SAFE_INTEGER(>= 2 ** 53)或为负数,则抛出一个 RangeError 异常。
示例

下面的例子创建了一个 8 字节的缓冲区,并使用一个 Int32Array 来引用它:

let buffer = new ArrayBuffer(8);
let view   = new Int32Array(buffer);

从 ECMAScript 2015 开始,ArrayBuffer 对象需要用 new 运算符创建。如果调用构造函数时没有使用 new,将会抛出 TypeError 异常。比如执行该语句 let ab = ArrayBuffer(10) 将会抛出以下异常:

VM109:1 Uncaught TypeError: Constructor ArrayBuffer requires 'new'
    at ArrayBuffer (<anonymous>)
    at <anonymous>:1:10

对于一些常用的 Web API,如 FileReader API 和 Fetch API 底层也是支持 ArrayBuffer,这里我们以 FileReader API 为例,看一下如何把 File 对象读取为 ArrayBuffer 对象:

const reader = new FileReader();

reader.onload = function(e) {
  let arrayBuffer = reader.result;
}

reader.readAsArrayBuffer(file);

1.2 Unit8Array

Uint8Array 数组类型表示一个 8 位无符号整型数组,创建时内容被初始化为 0。创建完后,可以以对象的方式或使用数组下标索引的方式引用数组中的元素。

语法
new Uint8Array(); // ES2017 最新语法
new Uint8Array(length); // 创建初始化为0的,包含length个元素的无符号整型数组
new Uint8Array(typedArray);
new Uint8Array(object);
new Uint8Array(buffer [, byteOffset [, length]]);
示例
// new Uint8Array(length); 
var uint8 = new Uint8Array(2);
uint8[0] = 42;
console.log(uint8[0]); // 42
console.log(uint8.length); // 2
console.log(uint8.BYTES_PER_ELEMENT); // 1

// new TypedArray(object); 
var arr = new Uint8Array([21,31]);
console.log(arr[1]); // 31

// new Uint8Array(typedArray);
var x = new Uint8Array([21, 31]);
var y = new Uint8Array(x);
console.log(y[0]); // 21

// new Uint8Array(buffer [, byteOffset [, length]]);
var buffer = new ArrayBuffer(8);
var z = new Uint8Array(buffer, 1, 4);

// new TypedArray(object); 
// 当传入一个 object 作为参数时,就像通过 TypedArray.from() 
// 方法创建一个新的类型化数组一样。
var iterable = function*(){ yield* [1,2,3]; }(); 
var uint8 = new Uint8Array(iterable); 
// Uint8Array[1, 2, 3]

1.3 ArrayBuffer 和 TypedArray

ArrayBuffer 本身只是一行 0 和 1 串。 ArrayBuffer 不知道该数组中第一个元素和第二个元素之间的分隔位置。

(图片来源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers

为了提供上下文,实际上要将其分解为多个盒子,我们需要将其包装在所谓的视图中。可以使用类型数组添加这些数据视图,并且你可以使用许多不同类型的类型数组。

例如,你可以有一个 Int8 类型的数组,它将把这个数组分成 8-bit 的字节数组。

(图片来源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers

或者你也可以有一个无符号 Int16 数组,它会把数组分成 16-bit 的字节数组,并且把它当作无符号整数来处理。

(图片来源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers

你甚至可以在同一基本缓冲区上拥有多个视图。对于相同的操作,不同的视图会给出不同的结果。

例如,如果我们从这个 ArrayBuffer 的 Int8 视图中获取 0 & 1 元素的值(-19 & 100),它将给我们与 Uint16 视图中元素 0 (25837)不同的值,即使它们包含完全相同的位。

(图片来源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers

这样,ArrayBuffer 基本上就像原始内存一样。它模拟了使用 C 之类的语言进行的直接内存访问。你可能想知道为什么我们不让程序直接访问内存,而是添加了这种抽象层,因为直接访问内存将导致一些安全漏洞

1.4 v8 句柄

句柄提供对 JavaScript 对象在堆中位置的引用。V8 垃圾收集器回收了无法再访问的对象所使用的内存。在垃圾收集过程中,垃圾收集器通常将对象移动到堆中的不同位置。当垃圾收集器移动对象时,垃圾收集器还会使用对象的新位置来更新所有引用该对象的句柄。

如果无法从 JavaScript 访问对象并且没有引用该对象的句柄,则该对象被视为垃圾。垃圾收集器会不时删除所有被视为垃圾的对象。V8 的垃圾回收机制是 V8 性能的关键。

句柄在 V8 中只是一个统称,它其实还分为多种类型:

  • 本地句柄(v8::Local):本地句柄保存在堆栈中,并在调用适当的析构函数时被删除。这些句柄的生存期由一个句柄作用域决定,该作用域通常是在函数调用开始时创建的。删除句柄作用域后,垃圾回收器可以自由地释放先前由句柄作用域中的句柄引用的那些对象,前提是它们不再可从 JavaScript 或其他句柄访问。
  • 持久句柄(v8::Persistent):持久句柄提供对堆分配的 JavaScript 对象的引用,就像本地句柄一样。有两种类型,它们处理的引用的生存期管理不同。当需要为多个函数调用保留对一个对象的引用时,或者当句柄生存期不对应于 C++ 作用域时,请使用持久句柄。
  • 永生句柄(v8::Eternal):Eternal 是用于永远不会被删除的 JavaScript 对象的持久句柄。它的使用成本更低,因为它使垃圾回收器不必确定对象的活动性。
  • 其他句柄

用一个更形象的比喻,那么 v8::Local 更像是 JavaScript 中的 let 。在 V8 中,内存的分配都交付给了 V8,那么我们就最好不要使用自己的 new 方法来创建对象,而是使用 v8::Local 里的各种方法来创建一个对象。由 v8::Local 创建的对象,能够被 v8 自动进行管理,也就是传说中的GC (垃圾清理机制)。

Persistent 代表的是持久的意思,更类似全局变量,申请和释放一定要记得使用:Persistent::New,Persistent::Dispose 这两个方法,否则会内存侧漏。

来源于:https://zhuanlan.zhihu.com/p/35371048 —— V8概念以及编程入门

二、Bytes 模块详解

bytes 模块旨在为字节切片的操作提供支持,接下来我们将逐一分析该模块提供的所有方法。

2.1 repeat

作用:重复给定二进制数组的字节并返回新的二进制数组。

使用示例:

import { repeat } from "https://deno.land/std/bytes/mod.ts";

repeat(new Uint8Array([1]), 3); // returns Uint8Array(3) [ 1, 1, 1 ]

源码实现:

// std/bytes/mod.ts
import { copyBytes } from "../io/util.ts";

export function repeat(b: Uint8Array, count: number): Uint8Array {
  if (count === 0) {
    return new Uint8Array();
  }

  if (count < 0) {
    throw new Error("bytes: negative repeat count");
  } else if ((b.length * count) / count !== b.length) {
    throw new Error("bytes: repeat count causes overflow");
  }

  const int = Math.floor(count);

  if (int !== count) {
    throw new Error("bytes: repeat count must be an integer");
  }

  // 根据源Uint8Array的长度与重复次数来创建新的空间 
  const nb = new Uint8Array(b.length * count);

  // 执行字节拷贝任务
  let bp = copyBytes(b, nb);

  for (; bp < nb.length; bp *= 2) {
    copyBytes(nb.slice(0, bp), nb, bp);
  }

  return nb;
}

在以上代码中,会对 count 参数的值进行各种校验,从而保证代码的安全性。之后,会根据源Uint8Array的长度与重复次数来创建新的空间,然后使用封装 copyBytes 方法执行字节拷贝操作。这里我们从 V8 的角度来简单认识一下 ArrayBufferUint8Array

src/api/api.h 文件中,我们可以看到 DECLARE_OPEN_HANDLEOPEN_HANDLE_LIST 这两个宏:

// src/api/api.h 
#define DECLARE_OPEN_HANDLE(From, To)                              \
  static inline v8::internal::Handle<v8::internal::To> OpenHandle( \
      const From* that, bool allow_empty_handle = false);

  OPEN_HANDLE_LIST(DECLARE_OPEN_HANDLE)
    
#define OPEN_HANDLE_LIST(V)                    \
  V(Template, TemplateInfo)                    \
  V(ArrayBuffer, JSArrayBuffer)                \
  V(ArrayBufferView, JSArrayBufferView)        \
  V(TypedArray, JSTypedArray)                  \
  V(Uint8Array, JSTypedArray)                  \
  V(Uint8ClampedArray, JSTypedArray)           \
  V(Int8Array, JSTypedArray)                   \
  V(Uint16Array, JSTypedArray)                 \
  V(Int16Array, JSTypedArray)                  \
  V(Uint32Array, JSTypedArray)                 \
  V(Int32Array, JSTypedArray)                  \
  V(Float32Array, JSTypedArray)                \
  V(Float64Array, JSTypedArray)                \
  V(DataView, JSDataView)                      \
  V(SharedArrayBuffer, JSArrayBuffer)          \
  ...

接着我们来看一下 ArrayBufferUint8Array 经过宏替换后的结果:

static inline v8::internal::Handle<v8::internal::JSArrayBuffer> OpenHandle( \
      const ArrayBuffer* that, bool allow_empty_handle = false);

static inline v8::internal::Handle<v8::internal::JSTypedArray> OpenHandle( \
      const Uint8Array* that, bool allow_empty_handle = false);

下面我们顺藤摸瓜,先找到 JSArrayBuffer 类:

// src/objects/js-array-buffer.h
namespace v8 {
namespace internal {

class ArrayBufferExtension;

class JSArrayBuffer
    : public TorqueGeneratedJSArrayBuffer<JSArrayBuffer, JSObject> {
 public:
// V8支持的的JSArrayBuffer的最大长度
// 在32位架构上,我们将此限制为2GB。因此,我们可以继续使用
// Unsigned31 校验边界来限制其最大长度。      
#if V8_HOST_ARCH_32_BIT
  static constexpr size_t kMaxByteLength = kMaxInt;
#else
// 对于非32位架构,如64位架构,最大值为2^53-1    
  static constexpr size_t kMaxByteLength = kMaxSafeInteger;
#endif
  }
}

上述代码中 kMaxSafeInteger 的定义如下:

// ES6 section 20.1.2.6 Number.MAX_SAFE_INTEGER
constexpr uint64_t kMaxSafeIntegerUint64 = 9007199254740991;  // 2^53-1
constexpr double kMaxSafeInteger = static_cast<double>(kMaxSafeIntegerUint64);

这里知道对于非 32 位架构,JSArrayBuffer 的大小最大为 2^53-1。那为什么是这个值呢?这里我们得先来了解一下 Number.MAX_SAFE_INTEGER 常量,它表示在 JavaScript 中最大的安全整数(2^53-1)。

MAX_SAFE_INTEGER 是一个值为 9007199254740991 的常量。因为 JavaScript 的数字存储使用了 IEEE 754 中规定的双精度浮点数数据类型,而这一数据类型能够安全存储 -(2^53 - 1) 到 2^53 - 1 之间的数值(包含边界值)。

这里安全存储的意思是指能够准确区分两个不相同的值,例如 Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 将得到 true 的结果,而这在数学上是错误的,参考 Number.isSafeInteger() 获取更多信息。

前面我们已经提到了创建 ArrayBuffer 的语法是:new ArrayBuffer(length),其中参数 length 的类型是 Number 类型,所以其对应的最大的安全整数为(2^53-1)。

介绍完上述内容我们再来看一下 repeat 函数中的 (b.length * count) / count !== b.length 这行代码:

// std/bytes/mod.ts
export function repeat(b: Uint8Array, count: number): Uint8Array {
  if (count < 0) {
      throw new Error("bytes: negative repeat count");
  } else if ((b.length * count) / count !== b.length) {
      throw new Error("bytes: repeat count causes overflow");
  }
  //...
}

为什么通过 (b.length * count) / count !== b.length 这行代码可以判断是否越界呢?这里废话不多说,我们直接看以下计算结果:

(9007199254740991 * 1.1) / 1.1
9007199254740990

好的,下面我们继续介绍如何创建 Handle<JSTypedArray> 句柄:

// src/heap/factory.cc
Handle<JSTypedArray> Factory::NewJSTypedArray(ExternalArrayType type,
                                              Handle<JSArrayBuffer> buffer,
                                              size_t byte_offset,
                                              size_t length) {
  size_t element_size;
  ElementsKind elements_kind;
  ForFixedTypedArray(type, &element_size, &elements_kind);
  size_t byte_length = length * element_size;

  CHECK_LE(length, JSTypedArray::kMaxLength);
  CHECK_EQ(length, byte_length / element_size);
  CHECK_EQ(0, byte_offset % ElementsKindToByteSize(elements_kind));

  Handle<Map> map;
  switch (elements_kind) {
#define TYPED_ARRAY_FUN(Type, type, TYPE, ctype)                              \
  case TYPE##_ELEMENTS:                                                       \
    map =                                                                     \
        handle(isolate()->native_context()->type##_array_fun().initial_map(), \
               isolate());                                                    \
    break;

    TYPED_ARRAYS(TYPED_ARRAY_FUN)
#undef TYPED_ARRAY_FUN

    default:
      UNREACHABLE();
  }
  Handle<JSTypedArray> typed_array =
      Handle<JSTypedArray>::cast(NewJSArrayBufferView(
          map, empty_byte_array(), buffer, byte_offset, byte_length));
  typed_array->set_length(length);
  typed_array->SetOffHeapDataPtr(isolate(), buffer->backing_store(),
                                 byte_offset);
  return typed_array;
}

通过观察上述代码,我们可以知道再创建 Handle<JSTypedArray> 句柄时,会先使用 NewJSArrayBufferViewJSArrayBuffer 对象进行封装,然后再调用 Handle<JSTypedArray>::cast 方法把 NewJSArrayBufferView 对象转换为最终的 Handle<JSTypedArray>

这里也进一步印证前面提到的内容:即 ArrayBuffer 不能直接操作,而是要通过 TypedArray 对象或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。

2.2 concat

作用:合并两个二进制数组并返回新的二进制数组。

使用示例:

import { concat } from "https://deno.land/std/bytes/mod.ts";

concat(new Uint8Array([1, 2]), new Uint8Array([3, 4]));
// returns Uint8Array(4) [ 1, 2, 3, 4 ]

源码实现:

export function concat(a: Uint8Array, b: Uint8Array): Uint8Array {
  const output = new Uint8Array(a.length + b.length);
  output.set(a, 0);
  output.set(b, a.length);
  return output;
}

在 concat 方法体中,Uint8Array 对象的 set 方法用于从指定数组中读取值,并将其存储在类型化数组中。该方法的签名是:

typedarray.set(array[, offset])
typedarray.set(typedarray[, offset])

其中 offset 参数是可选的,该参数指定从什么地方开始使用源数组的值进行写入操作。如果忽略该参数,则默认为 0(也就是说,从目标数组的下标为 0 处开始,使用源数组的值覆盖重写)。

2.3 findIndex

作用:从给定的二进制数组中查找二进制模式的第一个索引。

使用示例:

import { findIndex } from "https://deno.land/std/bytes/mod.ts";

findIndex(
  new Uint8Array([1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 3]),
  new Uint8Array([0, 1, 2])
);
// => returns 2

源码实现:

// std/bytes/mod.ts
export function findIndex(a: Uint8Array, pat: Uint8Array): number {
  const s = pat[0];
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== s) continue;
    // 记录第一个匹配元素的位置
    const pin = i;
    // 已匹配的元素个数
    let matched = 1;
    let j = i;
    // 循环匹配其余的元素
    while (matched < pat.length) {
      j++;
      if (a[j] !== pat[j - pin]) {
        break;
      }
      matched++;
    }
    if (matched === pat.length) {
      return pin;
    }
  }
  return -1;
}

2.4 findLastIndex

作用:从给定的二进制数组中查找二进制模式的最后一个索引。

使用示例:

import { findLastIndex } from "https://deno.land/std/bytes/mod.ts";

findLastIndex(
  new Uint8Array([0, 1, 2, 0, 1, 2, 0, 1, 3]),
  new Uint8Array([0, 1, 2])
);
// => returns 3

源码实现:

// std/bytes/mod.ts
export function findLastIndex(a: Uint8Array, pat: Uint8Array): number {
  const e = pat[pat.length - 1];
  for (let i = a.length - 1; i >= 0; i--) {
    if (a[i] !== e) continue;
    const pin = i;
    let matched = 1;
    let j = i;
    while (matched < pat.length) {
      j--;
      if (a[j] !== pat[pat.length - 1 - (pin - j)]) {
        break;
      }
      matched++;
    }
    if (matched === pat.length) {
      return pin - pat.length + 1;
    }
  }
  return -1;
}

2.5 equal

作用:检查给定的二进制数组是否相等。

使用示例:

import { equal } from "https://deno.land/std/bytes/mod.ts";

equal(new Uint8Array([0, 1, 2, 3]), new Uint8Array([0, 1, 2, 3])); // returns true
equal(new Uint8Array([0, 1, 2, 3]), new Uint8Array([0, 1, 2, 4])); // returns false

源码实现:

// std/bytes/mod.ts
export function equal(a: Uint8Array, match: Uint8Array): boolean {
  // 优先判断TypedArray数组的长度是否相等
  if (a.length !== match.length) return false;
  // 对TypedArray数组的每一项进行比对
  for (let i = 0; i < match.length; i++) {
    if (a[i] !== match[i]) return false;
  }
  return true;
}

2.6 hasPrefix

作用:检查二进制数组是否具有二进制前缀。

使用示例:

import { hasPrefix } from "https://deno.land/std/bytes/mod.ts";

hasPrefix(new Uint8Array([0, 1, 2]), new Uint8Array([0, 1])); // returns true
hasPrefix(new Uint8Array([0, 1, 2]), new Uint8Array([1, 2])); // returns false

源码实现:

// std/bytes/mod.ts
export function hasPrefix(a: Uint8Array, prefix: Uint8Array): boolean {
  for (let i = 0, max = prefix.length; i < max; i++) {
    if (a[i] !== prefix[i]) return false;
  }
  return true;
}

在 Deno 1.0.0 发布之后,本人才开始踏上学习 Deno 的征程,有写得不好的地方,欢迎小伙伴们指出,对 Deno 感兴趣的小伙伴可以加我个人微信号 semlinker 一起学习与交流哈。

三、参考资源

5 个赞