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

Ilmari Heikkinen

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

Khung hiển thị mảng đã nhập hoạt động như các mảng loại đơn cho một phân đoạn của ArrayBuffer. Có các thành phần hiển thị cho tất cả các loại số thông thường, với tên tự mô tả như Float32Array, Float64Array, Int32Array và Uint8Array. Ngoài ra, còn có một thành phần 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ó một API giống mảng, đối tượng DataView cung cấp cho bạn một API get/set để đọc và ghi các loại dữ liệu tuỳ ý tại các độ dời byte tuỳ ý. DataView hoạt động hiệu quả để đọc và ghi tiêu đề tệp cũng như các dữ liệu có cấu trúc tương tự khác.

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

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

Để sử dụng Mảng được nhập, bạn cần tạo một ArrayBuffer và một thành phần hiển thị cho ArrayBuffer đó. Cách dễ nhất là tạo thành phần hiển thị mảng đã nhập có kích thước và loại 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 khung hiển thị mảng được nhập. Tất cả chúng đều dùng chung một API, vì vậy, khi đã biết cách sử dụng một API, bạn sẽ biết cách sử dụng chúng. Tôi sẽ tạo một trong mỗi thành phần hiển thị mảng đã 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);

Hàm cuối cùng có chút đặc biệt, nó sẽ 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 cần 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ụ: sau đây là cách bạn áp dụng hệ số gamma cho một 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 thành phần hiển thị mảng đã nhập là trước tiên, hãy tạo một ArrayBuffer, sau đó tạo các thành phần hiển thị trỏ đến ArrayBuffer đó. Các API cung cấp dữ liệu bên ngoài thường xử lý trong ArrayBuffer, vì vậy, đây là cách bạn nhận được 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ó nhiều chế độ xem 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 vào một mảng đã nhập khác, cách nhanh nhất là sử dụng phương thức đặt mảng đã nhập. Để sử dụng như memcpy, hãy tạo Uint8Arrays cho vùng đệm của các thành phần hiển thị và sử 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 ArrayBuffer 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 đề là số nguyên không dấu 8 bit, theo sau là hai số nguyên 16 bit, theo sau 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 bằng chế độ xem mảng đã nhập nhưng hơi 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 float.

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, bạn có thể truyền tham số littleEndian (không bắt buộc) đến phương thức getter:

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

Xin lưu ý rằng thành phần hiển thị mảng đã nhập luôn ở thứ tự byte gốc. Điều này giúp các lớp này hoạt động nhanh. Bạn nên sử dụng DataView để đọc và ghi dữ liệu trong trường hợp vấn đề về khả năng kết nối.

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 giống như 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ề endianness

Thứ tự byte (endianness) là thứ tự lưu trữ các số nhiều byte 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 có giá trị nhất trước; little-endian, byte có giá trị ít nhất trước. Việc sử dụng thứ tự byte nào trong một cấu 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 thứ tự byte. 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 nguồn gốc? Lý do rất đơn giản. Khi đọc hoặc ghi dữ liệu từ ổ đĩa hoặc mạng, bạn phải chỉ định thứ tự byte của dữ liệu. Điều này đảm bảo rằng dữ liệu được diễn giải đúng cách, bất kể CPU đang hoạt động với dữ liệu đó có phải là endian hay không. Trong thế giới ngày càng kết nối mạng, điều quan trọng là phải hỗ trợ đúng cách tất cả các loại thiết bị, big- hoặc little-endian, có thể cần phải hoạt động 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ế riêng để đọc và ghi dữ liệu vào và từ các tệp cũng như mạng. DataView hoạt động trên dữ liệu có thứ tự byte được chỉ định. Bạn phải chỉ định thứ tự byte, lớn hay nhỏ, cho mọi quyền truy cập vào 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ể thứ tự byte của CPU mà trình duyệt đang chạy.

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 phải quét qua dữ liệu đó 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 thành phần hiển thị mảng được nhập nhiều byte (Int16Array, Uint16Array, v.v.) với dữ liệu được tìm nạp qua XMLHttpRequest, FileReader hoặc bất kỳ API đầu vào/đầu ra nào khác, vì thành phần hiển thị mảng được nhập sử dụng thứ tự byte 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 qua một vài ví dụ đơn giản. Thời kỳ đầu của Windows, định dạng tệp Windows BMP là định dạng chuẩn để lưu trữ hình ảnh. Tài liệu được liên kết ở trên cho biết rõ rằng tất cả các giá trị số nguyên trong tệp đều đượ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 thư viện DataStream.js đi kèm với 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();
  // ...
}

Sau đây là một ví dụ khác, ví dụ này là 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 dữ liệu dấu phẩy động little-endian thô xuống, đại diện cho hoạ tiết có dải động cao và cần tải dữ liệu đó lên WebGL. Dưới đây là đoạn mã diễn giải đúng 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ủ 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 ngón tay cái là: khi nhận được dữ liệu nhị phân từ máy chủ web, hãy truyền dữ liệu đó qua một DataView. Đọc từng giá trị số riêng lẻ và lưu trữ các giá trị đó trong một cấu trúc dữ liệu khác, có thể là đối tượng JavaScript (đối với một lượng nhỏ dữ liệu có cấu trúc) hoặc chế độ xem mảng đã nhập (đối với các khối dữ liệu lớn). Điều này sẽ đảm bảo 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à nhớ chỉ định đối số littleEndian 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 ngầm ẩn có định dạng và thứ tự byte (ít nhất là đối với mọi giá trị nhiều byte). Hãy nhớ xác định rõ ràng 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 Mảng được nhập. Các API hiện có bao gồm WebGL, Canvas, API Âm thanh trên web, XMLHttpRequest, WebSocket, Web Worker, API Nguồn nội dung đa phương tiện và API Tệp. Trong danh sách API, bạn có thể thấy rằng Mảng được nhập rất phù hợp với công việc đa phương tiện nhạy cảm về hiệu suất cũng như truyền dữ liệu một cách hiệu quả.

WebGL

Mục đích sử dụng đầu tiên của Mảng được nhập là trong WebGL, nơi nó được dùng để truyền dữ liệu vùng đệm và dữ liệu hình ảnh. Để đặt nội dung của đối tượng vùng đệm WebGL, bạn sử dụng lệnh gọi gl.bufferData() với Mảng đã 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 kết cấu. Sau đây là ví dụ cơ bản về cách truyền nội dung kết cấu bằng Mảng đã 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 Mảng được nhập để đọc pixel từ ngữ 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 tạo để hoạt động với thông số kỹ thuật Mảng đã nhập. Giờ đây, bạn có thể nhận được một Mảng đã nhập đại diện cho các pixel trên một phần tử canvas. Việc 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 can thiệp vào phần tử canvas.

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

XMLHttpRequest2

XMLHttpRequest đã tăng hiệu quả cho 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 rất hữu ích để truyền dữ liệu đã tìm nạp trực tiếp đến các 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';

Hãy nhớ rằng bạn phải lưu ý đến các vấn đề về thứ tự byte khi tải dữ liệu xuống từ mạng! Xem phần về thứ tự byte ở 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 các 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 lưu ý đến thứ tự byte ở đây. Hãy xem phần thứ tự byte để 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 sang các cửa sổ và Workers khác nhanh hơn rất nhiều. Khi bạn gửi một đối tượng đến Worker dưới dạng đối tượng Chuyển đổi được, đối tượng đó sẽ không truy cập được trong luồng gửi và Worker nhận sẽ có quyền sở hữu đối tượng đó. Đ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ỉ có quyền sở hữu Mảng được nhập được chuyển đến dịch vụ nhận.

Để sử dụng các đối tượng có thể chuyển với Web Worker, bạn cần sử dụng phương thức webkitPostMessage trên worker. Phương thức webkitPostMessage hoạt động giống như postMessage, nhưng cần có hai đối số thay vì chỉ một đối số. Đối số thứ hai được thêm là một mảng các đối tượng mà bạn muốn chuyển đến worker.

worker.webkitPostMessage(oneGBTypedArray, [oneGBTypedArray]);

Để lấy lại các đối tượng từ worker, worker có thể chuyển các đối tượng đó trở lại luồng chính theo cách tương tự.

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

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

Media Source API

Gần đây, các thành phần nội dung nghe nhìn cũng có một số tính năng hữu ích của Mảng đã nhập ở dạng Media Source API (API Nguồn nội dung nghe nhìn). Bạn có thể trực tiếp truyền một Mảng đã nhập chứa dữ liệu video đến một phần tử video bằng webkitSourceAppend. Thao tác này sẽ khiến phần tử video thêm dữ liệu video vào sau video hiện có. SourceAppend rất hữu ích khi bạn muốn phát nhiều video bằng một phần tử video duy nhất trong quảng cáo xen kẽ, danh sách phát, nội dung phát trực tuyến và các trường hợp sử dụng khác.

video.webkitSourceAppend(uint8Array);

WebSocket nhị phân

Bạn cũng có thể sử dụng Mảng đã nhập với WebSocket để tránh phải chuyển đổi tất cả dữ liệu thành chuỗi. Rất 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á! Như vậy là bạn đã hoàn tất việc xem xét API. Hãy chuyển sang tìm hiểu các thư viện bên thứ ba để xử lý Mảng được nhập.

Thư viện bên thứ ba

jDataView

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

Eric Bidelman thuộc 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 trong 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ý chuỗi trong Mảng đã nhập có chút phiền toái, nhưng bạn có thể sử dụng thư viện stringencoding để giải quyết vấn đề này. Phương thức mã hoá chuỗi (stringEncoding) triển khai quy cách mã hoá chuỗi Mảng đã nhập được đề xuất, vì vậy, đây cũng là một cách hay để giúp bạn nắm bắt những gì sắp xảy ra.

Dưới đây là ví dụ cơ bản về cách sử dụng stringencoding:

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. Như tên gọi, thành phần này hoạt động giống như DataView, ngoại trừ việc thành phần này hoạt động với các bit. Với BitView, bạn có thể nhận và thiết lập 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 số nguyên 6 bit và 12 bit tại các độ dời bit tuỳ ý.

Số nguyên 12 bit rất phù hợp để xử lý toạ độ màn hình, vì màn hình thườ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 số nguyên 12 bit thay vì số nguyên 32 bit, bạn có thể giảm kích thước 62%. Để có một ví dụ cực kỳ nghiêm trọng hơn, tôi đã làm việc với các Shapefile sử dụng số thực có độ 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 đó đã làm giảm kích thước tệp xuống một phần mười. Bạn có thể xem bản minh hoạ về điều đó tại đây.

Sau đâ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 bạn dễ dàng xử lý các tệp nhị phân trong JavaScript. Thay vì phân tích cú pháp từng ký tự trong 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ể lấy ArrayBuffer bằng XMLHttpRequest và trực tiếp xử lý ArrayBuffer đó bằng DataView. Điều này giúp bạn dễ dàng 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 tệp shapefile và chuyển tệp đó thành mô hình WebGL. Hoặc đọc thẻ EXIF từ tệp JPEG và hiển thị các thẻ đó trong ứng dụng trình chiếu.

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

DataStream.js là một thư viện Mảng được nhập, có chức năng đọc và ghi các đại lượng vô hướng, chuỗi, mảng và cấu trúc dữ liệu từ ArrayBuffer theo kiểu tệp.

Ví dụ về cách đọc một mảng số thực dấu phẩy động 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);

DataStream.js thực sự hữu ích khi đọc dữ liệu phức tạp hơn. Giả sử bạn có một phương thức đọc trong các đ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 sử dụng phương thức DataStream.readStruct để đọc các cấu trúc 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. Thư viện này có các hàm gọi lại để xử lý các loại phức tạp, cũng như xử lý các mảng dữ liệu và 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 [tên, loại]. Bạn có thể tạo cấu trúc lồng nhau bằng cách tạo một mảng cho loại đó. Mảng được xác định bằng cách sử dụng 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 dưới dạng hàm gọi lại). Phần tử đầu tiên của định nghĩa mảng không được sử dụng.

Sau đây là các giá trị có thể sử dụng cho kiểu này:

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ề cách đọc 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 được nhập bắt đầu từ giai đoạn triển khai ban đầu của WebGL, khi chúng tôi nhận thấy việc truyền các mảng JavaScript đến trình điều khiển đồ hoạ đang gây ra các vấn đề về hiệu suất. Với các mảng JavaScript, liên kết WebGL phải phân bổ một mảng gốc và điền vào mảng đó bằng cách duyệt qua mảng JavaScript và truyền mọi đối tượng JavaScript trong mảng đó thành loại gốc bắt buộc.

Để khắc phục nút thắt cổ chai trong việc chuyển đổi dữ liệu, Vladimir Vukicevic của Mozilla đã viết CanvasFloatArray: một mảng nổi kiểu C có giao diện JavaScript. Giờ đây, bạn có thể chỉnh sửa CanvasFloatArray trong JavaScript và truyền trực tiếp CanvasFloatArray đó đến WebGL mà không cần làm thêm gì trong liên kết. Trong các vòng lặp tiếp theo, CanvasFloatArray được đổi tên thành WebGLFloatArray, được đổi tên thêm thành Float32Array và được chia thành ArrayBuffer hỗ trợ và khung hiển thị Float32Array-đã nhập để truy cập bộ đệm. Các loại 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ý.

Cân nhắc về thiết kế

Ngay từ đầu, thiết kế của Mảng đã nhập được định hướng bởi nhu cầu truyền dữ liệu nhị phân vào các thư viện gốc một cách hiệu quả. Vì lý do này, các thành phần hiển thị mảng đã nhập sẽ hoạt động dựa trên dữ liệu được căn chỉnh theo thứ tự byte 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 quá trình vận hành như gửi dữ liệu đỉnh tới thẻ đồ hoạ.

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

Việc phân tách thiết kế giữa quá trình tập hợp dữ liệu trong bộ nhớ (sử dụng chế độ xem mảng đã nhập) và I/O (sử dụng DataView) là một thiết kế có chủ ý. Các công cụ JavaScript hiện đại tối ưu hoá mạnh mẽ các thành phần hiển thị mảng đã nhập và đạt được hiệu suất cao trên các phép toán số học với các thành phần hiển thị đó. Quyết định thiết kế này đã giúp đạt được mức hiệu suất hiện tại của các thành phần hiển thị mảng đã nhập.

Tài liệu tham khảo