ArrayBuffer trong TypeScript

ArrayBuffer trong TypeScript

Bài viết này giải thích về ArrayBuffer trong TypeScript.

Chúng tôi sẽ giải thích về ArrayBuffer trong TypeScript một cách từng bước, từ cơ bản đến các kỹ thuật thực tế.

YouTube Video

ArrayBuffer trong TypeScript

ArrayBuffer là một đối tượng tích hợp dùng để đại diện cho “vùng nhớ thô” cho dữ liệu nhị phân. Nó đại diện cho một bộ đệm thô với độ dài cố định, được phủ lên bởi TypedArray hoặc DataView để đọc và ghi.

Khái niệm cơ bản và đặc điểm của ArrayBuffer

ArrayBuffer là một dãy byte có độ dài cố định. Bạn chỉ định kích thước theo byte khi tạo, và độ dài này không thể thay đổi sau đó.

1// Create a new ArrayBuffer of 16 bytes.
2const buf = new ArrayBuffer(16);
3console.log(buf.byteLength); // 16
  • Đoạn mã này tạo một bộ đệm trống gồm 16 byte. Bạn có thể kiểm tra kích thước bằng cách sử dụng byteLength.

TypedArray và DataView — Các khung nhìn để thao tác với bộ đệm

ArrayBuffer không có khả năng đọc hoặc ghi dữ liệu. Do đó, các thao tác thực tế được thực hiện thông qua TypedArray hoặc DataView, bằng cách chỉ định các kiểu như số nguyên hoặc số thực và thứ tự byte (endianness) khi truy cập dữ liệu.

1// Create a buffer and a Uint8Array view over it. Then write bytes and read them.
2const buffer = new ArrayBuffer(8);
3const u8 = new Uint8Array(buffer);
4
5u8[0] = 0x41; // 'A'
6u8[1] = 0x42; // 'B'
7console.log(u8); // Uint8Array(8) [ 65, 66, 0, 0, 0, 0, 0, 0 ]
  • Uint8Array là một kiểu xem mảng theo từng byte và có thể truy cập như một mảng bình thường. Nếu bạn tạo nhiều cửa sổ (view) trên cùng một ArrayBuffer, chúng sẽ chia sẻ cùng vùng nhớ cho việc đọc và ghi.

DataView: Đọc và ghi tại vị trí tuỳ ý và kiểm soát thứ tự byte

DataView hữu ích cho việc đọc và ghi từng byte một cách chi tiết, và cho phép bạn chỉ định thứ tự byte (little hoặc big endian).

 1// Using DataView to write/read multi-byte values with endianness control.
 2const buf2 = new ArrayBuffer(8);
 3const view = new DataView(buf2);
 4
 5// Write a 32-bit integer (little-endian)
 6view.setInt32(0, 0x12345678, true);
 7
 8// Read it back as little-endian and big-endian
 9const little = view.getInt32(0, true);
10const big = view.getInt32(0, false);
11console.log(little.toString(16)); // "12345678"
12console.log(big.toString(16));    // "78563412"
  • DataView cho phép bạn đọc và ghi giá trị bằng cách chỉ định các vị trí offset trong bộ nhớ, rất thích hợp để triển khai các giao thức yêu cầu xử lý thứ tự byte của mạng.

Chuyển đổi giữa chuỗi và ArrayBuffer (TextEncoder / TextDecoder)

Để chuyển đổi văn bản sang nhị phân và ngược lại, hãy sử dụng TextEncoderTextDecoder.

 1// Convert string -> ArrayBuffer and back using TextEncoder/TextDecoder.
 2const encoder = new TextEncoder();
 3const decoder = new TextDecoder();
 4
 5// Unicode escape sequences
 6const str = "\u3053\u3093\u306B\u3061\u306F";
 7const encoded = encoder.encode(str); // Uint8Array
 8console.log(encoded); // Uint8Array([...])
 9
10// If you need an ArrayBuffer specifically:
11const ab = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
12console.log(ab.byteLength); // bytes length of encoded text
13
14// Decode back
15const decoded = decoder.decode(encoded);
16console.log(decoded);
  • TextEncoder.encode trả về một Uint8Array, vì vậy nếu bạn cần một ArrayBuffer, bạn nên tham chiếu đến thuộc tính .buffer của nó. Với phương thức slice, bạn có thể trích xuất dữ liệu cần thiết bằng cách chỉ định vị trí offset và độ dài.

Cắt và sao chép ArrayBuffer

Phương thức slice trả về một ArrayBuffer mới. ArrayBuffer không thể thay đổi kích thước; nếu cần thay đổi, hãy tạo một bộ đệm mới và sao chép dữ liệu vào đó.

 1// Slice an ArrayBuffer and copy to a new sized buffer.
 2const original = new Uint8Array([1,2,3,4,5]).buffer;
 3const part = original.slice(1, 4); // bytes 1..3
 4console.log(new Uint8Array(part)); // Uint8Array [ 2, 3, 4 ]
 5
 6// Resize: create a new buffer and copy existing content
 7const larger = new ArrayBuffer(10);
 8const target = new Uint8Array(larger);
 9target.set(new Uint8Array(original), 0);
10console.log(target); // first bytes filled with original data
  • Vì phương thức slice trả về một ArrayBuffer mới, nếu bạn ưu tiên hiệu suất hoặc muốn chia sẻ tham chiếu, bạn có thể sử dụng phương thức subarray của TypedArray để tham chiếu đến cùng một bộ đệm.

Sự khác biệt giữa TypedArray.subarray và việc sao chép

subarray của TypedArray trả về một cửa sổ (view) mới tham chiếu cùng một ArrayBuffer.

1// subarray shares the same underlying buffer; modifying one affects the other.
2const arr = new Uint8Array([10,20,30,40]);
3const viewSub = arr.subarray(1,3); // shares memory
4viewSub[0] = 99;
5console.log(arr); // Uint8Array [10, 99, 30, 40]
  • Các khung nhìn chia sẻ có thể tránh được chi phí sao chép, nhưng vì chúng tham chiếu cùng một bộ đệm nên bạn cần chú ý đến các tác động phụ.

Nối các Buffer (kết hợp nhiều ArrayBuffer)

ArrayBuffer có độ dài cố định, để kết hợp nhiều buffer hãy tạo một ArrayBuffer mới và sao chép dữ liệu vào.

 1// Concatenate multiple ArrayBuffers
 2function concatBuffers(buffers: ArrayBuffer[]): ArrayBuffer {
 3  const total = buffers.reduce((sum, b) => sum + b.byteLength, 0);
 4  const result = new Uint8Array(total);
 5  let offset = 0;
 6  for (const b of buffers) {
 7    const u8 = new Uint8Array(b);
 8    result.set(u8, offset);
 9    offset += u8.length;
10  }
11  return result.buffer;
12}
13
14const a = new Uint8Array([1,2]).buffer;
15const b = new Uint8Array([3,4,5]).buffer;
16const c = concatBuffers([a, b]);
17console.log(new Uint8Array(c)); // [1,2,3,4,5]
  • Nếu bạn thường xuyên nối một lượng lớn dữ liệu, tính toán trước tổng kích thước và phân bổ bộ đệm mới chỉ một lần như trong mã này sẽ hiệu quả hơn.

Truyền ArrayBuffer đến Worker (chuyển nhượng quyền sở hữu - Transferable)

Trong postMessage của trình duyệt, bạn có thể chuyển một ArrayBuffer như một đối tượng "transferable". Quyền sở hữu có thể được chuyển mà không cần sao chép, giúp tránh chi phí sao chép.

1// Example: posting an ArrayBuffer to a Worker as a transferable object (browser)
2const worker = new Worker('worker.js');
3const bufferToSend = new Uint8Array([1,2,3,4]).buffer;
4
5// Transfer ownership to the worker (main thread no longer owns it)
6worker.postMessage(bufferToSend, [bufferToSend]);
7
8// After transfer, bufferToSend.byteLength === 0 in many browsers (detached)
9console.log(bufferToSend.byteLength); // may be 0
  • Bằng cách chỉ định các đối tượng cần chuyển trong một mảng làm đối số thứ hai của postMessage, bạn có thể chuyển quyền sở hữu mà không cần sao chép các đối tượng có thể chuyển như ArrayBuffer.
  • Sau khi chuyển, bộ đệm sẽ trở nên "detached" (bị tách rời) phía bên gốc và không thể truy cập được. Sử dụng SharedArrayBuffer cho phép truy cập đồng thời từ nhiều luồng, nhưng việc sử dụng nó có kèm theo các yêu cầu về bảo mật và hạn chế môi trường.

Xử lý trong Node.js (chuyển đổi giữa Buffer)

Trong Node.js, bạn có thể chuyển đổi giữa kiểu nhị phân Buffer của Node.js và ArrayBuffer. Điều này hữu ích khi bạn phát triển TypeScript hướng tới cả trình duyệt và Node.js.

 1// Convert ArrayBuffer <-> Node.js Buffer
 2// In Node.js environment:
 3const ab = new Uint8Array([10,20,30]).buffer;
 4const nodeBuffer = Buffer.from(ab); // ArrayBuffer -> Buffer
 5console.log(nodeBuffer); // <Buffer 0a 14 1e>
 6
 7const backToAb = nodeBuffer.buffer.slice(
 8    nodeBuffer.byteOffset,
 9    nodeBuffer.byteOffset + nodeBuffer.byteLength
10);
11console.log(new Uint8Array(backToAb)); // Uint8Array [10,20,30]
  • Buffer.from(arrayBuffer) trong Node.js thường tạo một bản sao, nhưng đôi khi có thể dùng chung tham chiếu, do đó hãy lưu ý các chỉ số offset.

Những lưu ý về hiệu năng và thực tiễn tốt nhất

Để tối ưu hiệu suất và xử lý ArrayBuffer hiệu quả, cần chú ý một số điểm quan trọng. Dưới đây, chúng tôi liệt kê và giải thích các thực tiễn tốt nhất.

  • Giảm số lượng thao tác sao chép Khi xử lý dữ liệu nhị phân lớn, hãy dùng subarray (view chia sẻ) hoặc transferable để giảm việc sao chép.

  • Cấp phát mảng lớn một lần duy nhất Việc cấp phát các bộ đệm nhỏ nhiều lần sẽ làm tăng chi phí tài nguyên. Nếu có thể, hãy cấp phát một bộ đệm lớn một lần và sử dụng từng phần của nó khi cần thiết.

  • Chỉ định thứ tự byte một cách rõ ràng Khi xử lý giá trị nhiều byte, hãy dùng DataView và chỉ định rõ thứ tự byte. Big-endian thường là tiêu chuẩn cho các giao thức mạng.

Các ví dụ sử dụng phổ biến

ArrayBuffer được sử dụng rộng rãi cả trong trình duyệt và Node.js.

  1. Phân tích và xây dựng giao thức nhị phân (xử lý thông tin header bằng DataView)
  2. Xử lý dữ liệu media như hình ảnh và âm thanh (fetch(...).then(res => res.arrayBuffer()))
  3. Bộ nhớ chia sẻ cho WebAssembly (bộ nhớ Wasm hoạt động dựa trên ArrayBuffer)
  4. Chuyển xử lý nặng sang Workers (truyền ArrayBuffer như transferable)

Đoạn mã sau là ví dụ về việc lấy và phân tích dữ liệu nhị phân.

1// Example: fetch binary data in browser and inspect first bytes
2async function fetchAndInspect(url: string) {
3  const resp = await fetch(url);
4  const ab = await resp.arrayBuffer();
5  const u8 = new Uint8Array(ab, 0, Math.min(16, ab.byteLength));
6  console.log('first bytes:', u8);
7}
  • Đoạn mã này tải dữ liệu nhị phân từ bất kỳ URL nào trong trình duyệt và hiển thị một vài byte đầu. Đoạn mã tải dữ liệu nhận được qua API fetch thành ArrayBuffer và kiểm tra 16 byte đầu tiên bằng Uint8Array.

Tóm tắt

ArrayBuffer là đại diện cho vùng nhớ thô, giúp thực hiện các thao tác nhị phân hiệu quả thông qua TypedArray và DataView. Bằng cách thiết kế để tránh sao chép không cần thiết và chỉ định rõ thứ tự byte, bạn có thể xử lý nhị phân hiệu quả và an toàn.

Bạn có thể làm theo bài viết trên bằng cách sử dụng Visual Studio Code trên kênh YouTube của chúng tôi. Vui lòng ghé thăm kênh YouTube.

YouTube Video