本文在介紹 ArrayBuffer 和 TypedArray 的基礎上,詳細剖析了 Deno bytes 模塊的功能與具體實現,並站在 v8 的角度簡單分析了 JSArrayBuffer 和 JSTypedArray 類。
一、基礎知識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)
下面的例子創建了一個 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 Unit8ArrayUint8Array 數組類型表示一個 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 和 TypedArrayArrayBuffer 本身只是一行 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 的角度來簡單認識一下 ArrayBuffer 和 Uint8Array 。
在 src/api/api.h 文件中,我們可以看到 DECLARE_OPEN_HANDLE 和 OPEN_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) \
...接著我們來看一下 ArrayBuffer 和 Uint8Array 經過宏替換後的結果:
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> 句柄時,會先使用 NewJSArrayBufferView 對 JSArrayBuffer 對象進行封裝,然後再調用 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;
}
三、參考資源a-cartoon-intro-to-arraybuffers-and-sharedarraybuffers