Mảng được nhập – Dữ liệu nhị phân trong trình duyệt

Ilmari Heikkinen

Giới thiệu

Mảng được nhập là một tính năng bổ sung tương đối mới cho các trình duyệt, xuất phát từ nhu cầu phải có cách thức hiệu quả để xử lý dữ liệu nhị phân trong WebGL. Mảng được nhập là một mảng bộ nhớ có khung hiển thị được nhập vào đó, giống như cách các mảng hoạt động trong C. Vì một Mảng được nhập được hỗ trợ bởi bộ nhớ thô, nên công cụ JavaScript có thể truyền bộ nhớ trực tiếp sang thư viện gốc mà không cần phải chuyển đổi cẩn thận dữ liệu sang một cách biểu diễn gốc. Do đó, các mảng đã nhập hoạt động tốt hơn nhiều so với các mảng JavaScript trong việc truyền dữ liệu sang WebGL và các API khác xử lý dữ liệu nhị phân.

Các khung hiển thị mảng được nhập hoạt động như các mảng loại đơn đối với một phân đoạn của ArrayBuffer. Có khung hiển thị cho tất cả các loại số thông thường, với các tên tự mô tả như Float32Array, Float64Array, Int32Array và Uint8Array. Ngoài ra, còn có một khung hiển thị đặc biệt thay thế loại mảng pixel trong ImageData của Canvas: Uint8ClampedArray.

DataView là loại chế độ xem thứ hai và dùng để xử lý dữ liệu không đồng nhất. Thay vì có API giống mảng, đối tượng DataView cung cấp cho bạn API get/set để đọc và ghi các loại dữ liệu tuỳ ý ở các độ dời byte tuỳ ý. DataView rất hiệu quả để đọc và ghi tiêu đề tệp cũng như các dữ liệu có dạng cấu trúc khác.

Kiến thức cơ bản về cách sử dụng Mảng đã nhập

Thành phần hiển thị mảng đã nhập

Để sử dụng Arrays được nhập, bạn cần tạo một ArrayBuffer và một khung hiển thị cho đó. Cách dễ nhất là tạo một chế độ xem mảng đã nhập có kích thước và kiểu mong muốn.

// Typed array views work pretty much like normal arrays.
var f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];

Có một số loại chế độ xem mảng đã nhập. Tất cả đều chia sẻ cùng một API, vì vậy, khi bạn đã biết cách sử dụng một API, bạn sẽ biết khá nhiều cách sử dụng tất cả API. Tôi sẽ tạo một trong mỗi khung hiển thị mảng được nhập hiện có trong ví dụ tiếp theo.

// Floating point arrays.
var f64 = new Float64Array(8);
var f32 = new Float32Array(16);

// Signed integer arrays.
var i32 = new Int32Array(16);
var i16 = new Int16Array(32);
var i8 = new Int8Array(64);

// Unsigned integer arrays.
var u32 = new Uint32Array(16);
var u16 = new Uint16Array(32);
var u8 = new Uint8Array(64);
var pixels = new Uint8ClampedArray(64);

Tính năng cuối cùng có một chút đặc biệt, nó kẹp các giá trị đầu vào trong khoảng từ 0 đến 255. Điều này đặc biệt hữu ích cho các thuật toán xử lý hình ảnh Canvas vì giờ đây, bạn không phải kẹp toán học xử lý hình ảnh theo cách thủ công để tránh tràn phạm vi 8 bit.

Ví dụ: dưới đây là cách bạn áp dụng hệ số gamma cho hình ảnh được lưu trữ trong Uint8Array. Không đẹp lắm:

u8[i] = Math.min(255, Math.max(0, u8[i] * gamma));

Với Uint8ClampedArray, bạn có thể bỏ qua việc kẹp thủ công:

pixels[i] *= gamma;

Một cách khác để tạo chế độ xem mảng đã nhập là tạo ArrayBuffer trước rồi mới tạo các chế độ xem trỏ đến đó. Các API nhận được dữ liệu bên ngoài thường xử lý ArrayBuffers, vì vậy đây là cách bạn nhận chế độ xem mảng đã nhập cho các API đó.

var ab = new ArrayBuffer(256); // 256-byte ArrayBuffer.
var faFull = new Uint8Array(ab);
var faFirstHalf = new Uint8Array(ab, 0, 128);
var faThirdQuarter = new Uint8Array(ab, 128, 64);
var faRest = new Uint8Array(ab, 192);

Bạn cũng có thể có một số khung hiển thị cho cùng một ArrayBuffer.

var fa = new Float32Array(64);
var ba = new Uint8Array(fa.buffer, 0, Float32Array.BYTES_PER_ELEMENT); // First float of fa.

Để sao chép một mảng đã nhập sang một mảng đã nhập khác, cách nhanh nhất là sử dụng phương thức tập hợp mảng đã nhập. Để sử dụng cách tương tự như memcpy, hãy tạo Uint8Arrays vào vùng đệm của khung hiển thị và dùng set để sao chép dữ liệu.

function memcpy(dst, dstOffset, src, srcOffset, length) {
  var dstU8 = new Uint8Array(dst, dstOffset, length);
  var srcU8 = new Uint8Array(src, srcOffset, length);
  dstU8.set(srcU8);
};

DataView

Để sử dụng ArrayBuffers chứa dữ liệu có các loại không đồng nhất, cách dễ nhất là sử dụng DataView cho vùng đệm. Giả sử chúng ta có một định dạng tệp có tiêu đề với một int không dấu 8 bit, theo sau là hai int 16 bit, tiếp theo là một mảng tải trọng gồm các số thực 32 bit. Bạn có thể đọc lại nội dung này với chế độ xem mảng được nhập nhưng có vẻ khó khăn. Với DataView, chúng ta có thể đọc tiêu đề và sử dụng chế độ xem mảng đã nhập cho mảng dấu phẩy động.

var dv = new DataView(buffer);
var vector_length = dv.getUint8(0);
var width = dv.getUint16(1); // 0+uint8 = 1 bytes offset
var height = dv.getUint16(3); // 0+uint8+uint16 = 3 bytes offset
var vectors = new Float32Array(width*height*vector_length);
for (var i=0, off=5; i<vectors.length; i++, off+=4) {
  vectors[i] = dv.getFloat32(off);
}

Trong ví dụ trên, tất cả các giá trị tôi đọc đều là Big-endian. Nếu các giá trị trong vùng đệm là Little-endian, thì bạn có thể truyền tham số LittleEndian (không bắt buộc) vào phương thức getter:

...
var width = dv.getUint16(1, true);
var height = dv.getUint16(3, true);
...
vectors[i] = dv.getFloat32(off, true);
...

Lưu ý các khung hiển thị mảng được nhập luôn theo thứ tự byte gốc. Việc này là để giúp chúng nhanh hơn. Bạn nên sử dụng DataView để đọc và ghi dữ liệu mà vấn đề có thể xảy ra.

DataView cũng có các phương thức để ghi giá trị vào vùng đệm. Các phương thức setter này được đặt tên theo cùng một cách với phương thức getter, "set" theo sau là kiểu dữ liệu.

dv.setInt32(0, 25, false); // set big-endian int32 at byte offset 0 to 25
dv.setInt32(4, 25); // set big-endian int32 at byte offset 4 to 25
dv.setFloat32(8, 2.5, true); // set little-endian float32 at byte offset 8 to 2.5

Thảo luận về tính bền vững

Thứ tự cuối, hay thứ tự byte, là thứ tự mà các số nhiều byte được lưu trữ trong bộ nhớ của máy tính. Thuật ngữ big-endian mô tả cấu trúc CPU lưu trữ byte quan trọng nhất trước; little-endian, byte ít quan trọng nhất đầu tiên. Dữ liệu cuối được sử dụng trong một kiến trúc CPU nhất định là hoàn toàn tuỳ ý; có những lý do chính đáng để chọn một trong hai. Trên thực tế, một số CPU có thể được định cấu hình để hỗ trợ cả dữ liệu Big-endian và Little-endian.

Tại sao bạn cần quan tâm đến tính cuối cùng? Lý do rất đơn giản. Khi đọc hoặc ghi dữ liệu từ ổ đĩa hoặc mạng, phải chỉ định tính cuối của dữ liệu. Điều này đảm bảo dữ liệu được diễn giải chính xác, bất kể CPU cuối cùng đang hoạt động với dữ liệu đó. Trong thế giới ngày càng kết nối mạng của chúng ta, điều cần thiết là phải hỗ trợ đúng cách tất cả các loại thiết bị, dù lớn hay nhỏ, có thể cần phải làm việc với dữ liệu nhị phân đến từ máy chủ hoặc các thiết bị ngang hàng khác trên mạng.

Giao diện DataView được thiết kế đặc biệt để đọc và ghi dữ liệu vào và từ tệp và mạng. DataView hoạt động dựa trên dữ liệu có độc giả được chỉ định. Tính cuối cùng, dù lớn hay nhỏ, phải được chỉ định cho mọi lượt truy cập của mọi giá trị, đảm bảo rằng bạn nhận được kết quả nhất quán và chính xác khi đọc hoặc ghi dữ liệu nhị phân, bất kể thuộc tính cuối của CPU mà trình duyệt đang chạy là gì.

Thông thường, khi ứng dụng của bạn đọc dữ liệu nhị phân từ máy chủ, bạn sẽ cần quét qua đó một lần để chuyển đổi dữ liệu đó thành cấu trúc dữ liệu mà ứng dụng của bạn sử dụng nội bộ. Bạn nên sử dụng DataView trong giai đoạn này. Bạn không nên sử dụng trực tiếp các chế độ xem mảng đã nhập nhiều byte (Int16Array, Uint16Array, v.v.) với dữ liệu được tìm nạp thông qua XMLHttpRequest, FileReader hoặc bất kỳ API đầu vào/đầu ra nào khác, vì các chế độ xem mảng đã nhập sử dụng tính toàn vẹn gốc của CPU. Chúng ta sẽ nói thêm về điều này ở phần sau.

Hãy cùng xem xét một vài ví dụ đơn giản. Định dạng tệp Windows BMP từng là định dạng chuẩn để lưu trữ hình ảnh trong những ngày đầu của Windows. Tài liệu được liên kết ở trên chỉ ra rõ rằng tất cả các giá trị số nguyên trong tệp được lưu trữ ở định dạng Little-endian. Dưới đây là một đoạn mã phân tích cú pháp phần đầu của tiêu đề BMP bằng cách sử dụng thư viện DataStream.js kèm theo bài viết này:

function parseBMP(arrayBuffer) {
  var stream = new DataStream(arrayBuffer, 0,
    DataStream.LITTLE_ENDIAN);
  var header = stream.readUint8Array(2);
  var fileSize = stream.readUint32();
  // Skip the next two 16-bit integers
  stream.readUint16();
  stream.readUint16();
  var pixelOffset = stream.readUint32();
  // Now parse the DIB header
  var dibHeaderSize = stream.readUint32();
  var imageWidth = stream.readInt32();
  var imageHeight = stream.readInt32();
  // ...
}

Đây là một ví dụ khác, ví dụ này từ bản minh hoạ kết xuất Dải động cao trong dự án mẫu WebGL. Bản minh hoạ này tải xuống dữ liệu dấu phẩy động thô, nhỏ, đại diện cho hoạ tiết có phạm vi động cao và cần tải dữ liệu đó lên WebGL. Dưới đây là đoạn mã diễn giải chính xác các giá trị dấu phẩy động trên tất cả cấu trúc CPU. Giả sử biến “arrayBuffer” là một ArrayBuffer vừa được tải xuống từ máy chủ thông qua XMLHttpRequest:

var arrayBuffer = ...;
var data = new DataView(arrayBuffer);
var tempArray = new Float32Array(
  data.byteLength / Float32Array.BYTES_PER_ELEMENT);
var len = tempArray.length;
// Incoming data is raw floating point values
// with little-endian byte ordering.
for (var jj = 0; jj < len; ++jj) {
  tempArray[jj] =
    data.getFloat32(jj * Float32Array.BYTES_PER_ELEMENT, true);
}
gl.texImage2D(...other arguments...,
  gl.RGB, gl.FLOAT, tempArray);

Quy tắc chung là: khi nhận được dữ liệu nhị phân từ máy chủ web, hãy chuyển dữ liệu đó bằng DataView. Đọc các giá trị số riêng lẻ và lưu trữ chúng trong một số cấu trúc dữ liệu khác, đối tượng JavaScript (đối với lượng nhỏ dữ liệu có cấu trúc) hoặc chế độ xem mảng đã nhập (đối với khối dữ liệu lớn). Việc này sẽ đảm bảo rằng mã của bạn hoạt động chính xác trên mọi loại CPU. Ngoài ra, hãy sử dụng DataView để ghi dữ liệu vào tệp hoặc mạng và đảm bảo chỉ định đối số littleEndian một cách phù hợp cho các phương thức set khác nhau để tạo định dạng tệp mà bạn đang tạo hoặc sử dụng.

Hãy nhớ rằng tất cả dữ liệu truyền qua mạng đều có định dạng và tính chất cuối (ít nhất là đối với bất kỳ giá trị nhiều byte nào). Hãy đảm bảo xác định rõ và ghi lại định dạng của tất cả dữ liệu mà ứng dụng của bạn gửi qua mạng.

API trình duyệt sử dụng Mảng được nhập

Tôi sẽ cung cấp cho bạn thông tin tổng quan ngắn gọn về các API trình duyệt hiện đang sử dụng Typed Arrays. Loại hình cắt hiện tại bao gồm WebGL, Canvas, Web Audio API, XMLHttpRequests, WebSockets, Web Workers, Media Source API và File API. Từ danh sách API, bạn có thể thấy rằng Mảng được nhập rất phù hợp với những công việc nội dung đa phương tiện đòi hỏi hiệu suất cao cũng như truyền dữ liệu một cách hiệu quả.

WebGL

Lần đầu tiên sử dụng Arrays là Typed Arrays trong WebGL, trong đó WebGL được dùng để truyền dữ liệu vùng đệm và dữ liệu hình ảnh. Để đặt nội dung cho đối tượng vùng đệm WebGL, bạn hãy sử dụng lệnh gọi gl.bufferData() với một Mảng được nhập.

var floatArray = new Float32Array([1,2,3,4,5,6,7,8]);
gl.bufferData(gl.ARRAY_BUFFER, floatArray);

Mảng đã nhập cũng được dùng để truyền dữ liệu hoạ tiết. Dưới đây là ví dụ cơ bản về cách truyền nội dung kết cấu bằng Mảng được nhập.

var pixels = new Uint8Array(16*16*4); // 16x16 RGBA image
gl.texImage2D(
  gl.TEXTURE_2D, // target
  0, // mip level
  gl.RGBA, // internal format
  16, 16, // width and height
  0, // border
  gl.RGBA, //format
  gl.UNSIGNED_BYTE, // type
  pixels // texture data
);

Bạn cũng cần có Typed Arrays (Mảng được nhập) để đọc các pixel từ bối cảnh WebGL.

var pixels = new Uint8Array(320*240*4); // 320x240 RGBA image
gl.readPixels(0, 0, 320, 240, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

Canvas 2D

Gần đây, đối tượng Canvas ImageData đã được điều chỉnh để tương thích với thông số kỹ thuật Typed Arrays. Giờ đây, bạn có thể nhận được kiểu biểu diễn Mảng được nhập của các pixel trên phần tử canvas. Điều này rất hữu ích vì giờ đây, bạn cũng có thể tạo và chỉnh sửa các mảng pixel canvas mà không phải thao tác với phần tử canvas.

var imageData = ctx.getImageData(0,0, 200, 100);
var typedArray = imageData.data // data is a Uint8ClampedArray

XMLHttpRequest2

XMLHttpRequest đã tăng cường Mảng được nhập và giờ đây bạn có thể nhận được phản hồi Mảng được nhập thay vì phải phân tích cú pháp chuỗi JavaScript thành Mảng được nhập. Điều này thực sự thuận tiện cho việc truyền trực tiếp dữ liệu đã tìm nạp sang API đa phương tiện và phân tích cú pháp các tệp nhị phân được tìm nạp từ mạng.

Bạn chỉ cần đặt responseType của đối tượng XMLHttpRequest thành "arraybuffer".

xhr.responseType = 'arraybuffer';

Xin lưu ý rằng bạn phải lưu ý đến các vấn đề về tính cuối khi tải dữ liệu xuống từ mạng! Xem phần về tính bền vững ở trên.

API tệp

FileReader có thể đọc nội dung tệp dưới dạng ArrayBuffer. Sau đó, bạn có thể đính kèm thành phần hiển thị mảng đã nhập và DataViews vào vùng đệm để thao tác với nội dung của vùng đệm.

reader.readAsArrayBuffer(file);

Bạn cũng nên chú ý đến tính cuối cùng ở đây. Hãy xem mục khả năng kết nối để biết thông tin chi tiết.

Đối tượng có thể chuyển

Các đối tượng có thể chuyển trong postMessage giúp việc truyền dữ liệu nhị phân đến các cửa sổ khác và Trình chạy web nhanh hơn rất nhiều. Khi bạn gửi một đối tượng đến một Worker dưới dạng một đối tượng Có thể chuyển, đối tượng này sẽ không thể truy cập được trong luồng gửi và Worker nhận quyền sở hữu đối tượng này. Điều này cho phép triển khai được tối ưu hoá cao, trong đó dữ liệu đã gửi không được sao chép mà chỉ chuyển quyền sở hữu Mảng được nhập cho bộ nhận.

Để sử dụng các đối tượng Có thể chuyển bằng Trình chạy web, bạn cần sử dụng phương thức webkitPostMessage trên trình thực thi này. Phương thức webkitPostMessage hoạt động giống như postMessage, nhưng phương thức này sẽ nhận hai đối số thay vì chỉ một đối số. Đối số thứ hai được thêm vào là một mảng các đối tượng mà bạn muốn chuyển sang worker.

worker.webkitPostMessage(oneGBTypedArray, [oneGBTypedArray]);

Để đưa các đối tượng trở lại từ worker, worker này có thể chuyển các đối tượng đó trở lại luồng chính theo cùng một cách.

webkitPostMessage({results: grand, youCanHaveThisBack: oneGBTypedArray}, [oneGBTypedArray]);

Không có bản sao nào cả!

API nguồn nội dung đa phương tiện

Gần đây, các phần tử nội dung đa phương tiện cũng đạt được một số ưu điểm nhất định của Typed Array ở dạng API Nguồn nội dung nghe nhìn. Bạn có thể trực tiếp truyền một Mảng được nhập chứa dữ liệu video tới phần tử video bằng cách sử dụng webkitSourceAppend. Thao tác này giúp phần tử video nối thêm dữ liệu video vào sau video hiện tại. SourceAppend là công cụ tuyệt vời để thực hiện quảng cáo xen kẽ, danh sách phát, phát trực tuyến và các mục đích khác mà bạn có thể muốn phát một vài video bằng một phần tử video duy nhất.

video.webkitSourceAppend(uint8Array);

WebSocket nhị phân

Bạn cũng có thể sử dụng Mảng được nhập với WebSocket để tránh phải tạo chuỗi cho tất cả dữ liệu của mình. Phù hợp để viết các giao thức hiệu quả và giảm thiểu lưu lượng truy cập mạng.

socket.binaryType = 'arraybuffer';

Thế thì tốt quá! Phần xem xét API đã kết thúc. Hãy chuyển sang xem các thư viện của bên thứ ba để xử lý Mảng được nhập.

Thư viện của bên thứ ba

jDataView

jDataView triển khai shim DataView cho tất cả các trình duyệt. DataView từng là tính năng chỉ dành cho WebKit, nhưng hiện tại được hỗ trợ bởi hầu hết các trình duyệt khác. Nhóm nhà phát triển Mozilla đang trong quá trình tạo bản vá để bật DataView trên Firefox.

Eric Bidelman trong nhóm Quan hệ nhà phát triển Chrome đã viết một ví dụ về trình đọc thẻ MP3 ID3 nhỏ sử dụng jDataView. Dưới đây là ví dụ về cách sử dụng từ bài đăng trên blog:

var dv = new jDataView(arraybuffer);

// "TAG" starts at byte -128 from EOF.
// See http://en.wikipedia.org/wiki/ID3
if (dv.getString(3, dv.byteLength - 128) == 'TAG') {
  var title = dv.getString(30, dv.tell());
  var artist = dv.getString(30, dv.tell());
  var album = dv.getString(30, dv.tell());
  var year = dv.getString(4, dv.tell());
} else {
  // no ID3v1 data found.
}

mã hoá chuỗi

Hiện tại, việc xử lý các chuỗi trong Mảng được nhập (Typed Arrays) khá khó khăn, nhưng có thư viện mã hoá chuỗi sẽ giúp bạn làm việc đó. Mã hoá chuỗi triển khai thông số mã hoá chuỗi Chuỗi được nhập được đề xuất, vì vậy, đây cũng là một cách hiệu quả để nắm bắt những điều sắp tới.

Dưới đây là ví dụ cơ bản về cách mã hoá chuỗi:

var uint8array = new TextEncoder(encoding).encode(string);
var string = new TextDecoder(encoding).decode(uint8array);

BitView.js

Tôi đã viết một thư viện thao tác bit nhỏ cho Mảng được nhập có tên là BitView.js. Đúng như tên gọi, nó hoạt động rất giống với DataView, ngoại trừ việc nó hoạt động với các bit. Với BitView, bạn có thể nhận và đặt giá trị của một bit tại một độ lệch bit nhất định trong ArrayBuffer. BitView cũng có các phương thức để lưu trữ và tải các int 6 bit và 12 bit tại các độ lệch bit tùy ý.

Int 12 bit rất phù hợp để làm việc với toạ độ màn hình, vì màn hình có xu hướng có ít hơn 4096 pixel dọc theo kích thước dài hơn. Bằng cách sử dụng int 12 bit thay vì int 32 bit, bạn có thể giảm kích thước 62%. Đối với một ví dụ nghiêm ngặt hơn, tôi đã làm việc với các Shapefile sử dụng độ chính xác đơn 64 bit cho các toạ độ, nhưng tôi không cần độ chính xác vì mô hình sẽ chỉ được hiển thị ở kích thước màn hình. Việc chuyển sang toạ độ cơ sở 12 bit với delta 6 bit để mã hoá các thay đổi từ toạ độ trước đó đã khiến kích thước tệp giảm xuống một phần mười. Bạn có thể xem bản minh hoạ tính năng này tại đây.

Dưới đây là ví dụ về cách sử dụng BitView.js:

var bv = new BitView(arrayBuffer);
bv.setBit(4, 1); // Set fourth bit of arrayBuffer to 1.
bv.getBit(17); // Get 17th bit of arrayBuffer.

bv.getBit(50*8 + 3); // Get third bit of 50th byte in arrayBuffer.

bv.setInt6(3, 18); // Write 18 as a 6-bit int to bit position 3 in arrayBuffer.
bv.getInt12(9); // Read a 12-bit int from bit position 9 in arrayBuffer.

DataStream.js

Một trong những điều thú vị nhất về mảng đã nhập là cách chúng giúp việc xử lý các tệp nhị phân trong JavaScript trở nên dễ dàng hơn. Thay vì phân tích cú pháp từng ký tự chuỗi và chuyển đổi các ký tự thành số nhị phân theo cách thủ công, giờ đây, bạn có thể nhận ArrayBuffer với XMLHttpRequest và trực tiếp xử lý bằng DataView. Điều này giúp bạn dễ dàng, chẳng hạn như tải tệp MP3 và đọc các thẻ siêu dữ liệu để sử dụng trong trình phát âm thanh. Hoặc tải vào một tệp shapefile và chuyển thành mô hình WebGL. Hoặc đọc các thẻ EXIF trên một tệp JPEG và hiển thị chúng trong ứng dụng trình chiếu của bạn.

Vấn đề với ArrayBuffer XHRs là việc đọc dữ liệu giống như cấu trúc từ bộ đệm có vẻ hơi khó khăn. DataView phù hợp để đọc một vài số cùng lúc theo cách an toàn cho người dùng cuối. Chế độ xem mảng được nhập phù hợp để đọc mảng gồm số nội bộ gốc được căn chỉnh theo kích thước phần tử. Điều chúng tôi cảm thấy còn thiếu là cách đọc các mảng và cấu trúc dữ liệu theo cách thuận tiện và an toàn cho người dùng cuối. Nhập DataStream.js.

DataStream.js là một thư viện Arrays được nhập, đọc và ghi các đại lượng vô hướng, chuỗi, mảng và cấu trúc của dữ liệu từ ArrayBuffers theo cách giống như tệp.

Ví dụ về cách đọc một mảng số thực có độ chính xác đơn từ một ArrayBuffer:

// without DataStream.js
var dv = new DataView(buffer);
var f32 = new Float32Array(buffer.byteLength / 4);
var littleEndian = true;
for (var i = 0; i<f32.length; i++) {
  f32[i] = dv.getFloat32(i*4, littleEndian);
}

// with DataStream.js
var ds = new DataStream(buffer);
ds.endianness = DataStream.LITTLE_ENDIAN;
var f32 = ds.readFloat32Array(ds.byteLength / 4);

Trong trường hợp DataStream.js thực sự hữu ích là trong việc đọc dữ liệu phức tạp hơn. Giả sử bạn có một phương thức đọc bằng điểm đánh dấu JPEG:

// without DataStream.js
var dv = new DataView(buffer);
var objs = [];
for (var i=0; i<buffer.byteLength;) {
  var obj = {};
  obj.tag = dv.getUint16(i);
  i += 2;
  obj.length = dv.getUint16(i);
  i += 2;
  obj.data = new Uint8Array(obj.length - 2);
  for (var j=0; j<obj.data.length; j++,i++) {
    obj.data[j] = dv.getUint8(i);
  }
  objs.push(obj);
}

// with DataStream.js
var ds = new DataStream(buffer);
ds.endianness = ds.BIG_ENDIAN;
var objs = [];
while (!ds.isEof()) {
  var obj = {};
  obj.tag = ds.readUint16();
  obj.length = ds.readUint16();
  obj.data = ds.readUint8Array(obj.length - 2);
  objs.push(obj);
}

Hoặc dùng phương thức DataStream.readStruct để đọc các cấu trúc của dữ liệu. Phương thức readStruct lấy một mảng định nghĩa cấu trúc chứa các loại thành phần cấu trúc. Lớp này có các hàm callback để xử lý các loại phức tạp, đồng thời xử lý các mảng dữ liệu và các cấu trúc lồng nhau:

// with DataStream.readStruct
ds.readStruct([
  'objs', ['[]', [ // objs: array of tag,length,data structs
    'tag', 'uint16',
    'length', 'uint16',
    'data', ['[]', 'uint8', function(s,ds){ return s.length - 2; }], // get length with a function
  '*'] // read in as many struct as there are
]);

Như bạn có thể thấy, định nghĩa cấu trúc là một mảng phẳng gồm các cặp [name, type]. Các cấu trúc lồng nhau được thực hiện bằng cách có một mảng cho loại này. Mảng được xác định bằng cách sử dụng một mảng ba phần tử, trong đó phần tử thứ hai là loại phần tử mảng và phần tử thứ ba là độ dài mảng (dưới dạng số, tham chiếu đến trường đã đọc trước đó hoặc hàm gọi lại). Phần tử đầu tiên của định nghĩa mảng chưa được sử dụng.

Loại này có thể có các giá trị sau:

Number types

Unsuffixed number types use DataStream endianness.
To explicitly specify endianness, suffix the type with
'le' for little-endian or 'be' for big-endian,
e.g. 'int32be' for big-endian int32.

  'uint8' -- 8-bit unsigned int
  'uint16' -- 16-bit unsigned int
  'uint32' -- 32-bit unsigned int
  'int8' -- 8-bit int
  'int16' -- 16-bit int
  'int32' -- 32-bit int
  'float32' -- 32-bit float
  'float64' -- 64-bit float

String types

  'cstring' -- ASCII string terminated by a zero byte.
  'string:N' -- ASCII string of length N.
  'string,CHARSET:N' -- String of byteLength N encoded with given CHARSET.
  'u16string:N' -- UCS-2 string of length N in DataStream endianness.
  'u16stringle:N' -- UCS-2 string of length N in little-endian.
  'u16stringbe:N' -- UCS-2 string of length N in big-endian.

Complex types

  [name, type, name_2, type_2, ..., name_N, type_N] -- Struct

  function(dataStream, struct) {} -- Callback function to read and return data.

  {get: function(dataStream, struct) {}, set: function(dataStream, struct) {}}
  -- Getter/setter functions to reading and writing data. Handy for using the
     same struct definition for both reading and writing.

  ['', type, length] -- Array of given type and length. The length can be either
                        a number, a string that references a previously-read
                        field, or a callback function(struct, dataStream, type){}.
                        If length is set to '*', elements are read from the
                        DataStream until a read fails.

Bạn có thể xem ví dụ trực tiếp về việc đọc trong siêu dữ liệu JPEG tại đây. Bản minh hoạ sử dụng DataStream.js để đọc cấu trúc cấp thẻ của tệp JPEG (cùng với một số phân tích cú pháp EXIF) và jpg.js để giải mã và hiển thị hình ảnh JPEG trong JavaScript.

Lịch sử của mảng đã nhập

Mảng đã nhập bắt đầu trong giai đoạn triển khai ban đầu của WebGL, khi chúng tôi nhận thấy rằng việc truyền mảng JavaScript đến trình điều khiển đồ họa gây ra các sự cố về hiệu suất. Với mảng JavaScript, việc liên kết WebGL phải phân bổ một mảng gốc và lấp đầy mảng đó bằng cách di chuyển qua mảng JavaScript và truyền mọi đối tượng JavaScript trong mảng sang loại gốc cần thiết.

Để khắc phục nút thắt cổ chai của việc chuyển đổi dữ liệu, Vladimir Vukicevic của Mozilla đã viết CanvasFloatArray: một mảng float kiểu C với giao diện JavaScript. Giờ đây, bạn có thể chỉnh sửa CanvasFloatArray trong JavaScript và chuyển trực tiếp sang WebGL mà không phải thực hiện thêm bất kỳ thao tác nào trong việc liên kết. Trong các lần lặp tiếp theo, CanvasFloatArray được đổi tên thành WebGLFloatArray, tiếp tục được đổi tên thành Float32Array và tách thành một ArrayBuffer sao lưu và chế độ xem Float32Array đã được nhập để truy cập vào vùng đệm. Các kiểu cũng được thêm vào cho các kích thước số nguyên và dấu phẩy động khác cũng như các biến thể đã ký/chưa ký.

Những điều cần lưu ý khi thiết kế

Ngay từ đầu, việc thiết kế Mảng được nhập đã được thúc đẩy bởi nhu cầu truyền dữ liệu nhị phân đến thư viện gốc một cách hiệu quả. Vì lý do này, các chế độ xem mảng đã nhập sẽ hoạt động dựa trên dữ liệu được căn chỉnh trong tính năng đặc trưng gốc của CPU máy chủ. Những quyết định này giúp JavaScript có thể đạt được hiệu suất tối đa trong các thao tác như gửi dữ liệu đỉnh đến thẻ đồ hoạ.

DataView được thiết kế riêng cho I/O tệp và mạng, trong đó dữ liệu luôn có tính đặc trưng được chỉ định và có thể không được căn chỉnh để có hiệu suất tối đa.

Việc phân chia thiết kế giữa tập hợp dữ liệu trong bộ nhớ (sử dụng thành phần hiển thị mảng đã nhập) và I/O (sử dụng DataView) là một hoạt động có chủ ý. Các công cụ JavaScript hiện đại tối ưu hoá các khung hiển thị mảng được nhập rất nhiều và đạt được hiệu suất cao trong các thao tác số. Mức hiệu suất hiện tại của thành phần hiển thị mảng đã nhập được thực hiện nhờ quyết định thiết kế này.

Tài liệu tham khảo