Web 前端開發日誌(二):JavaScript 的二進制操做

文章爲在下之前開發時的一些記錄與當時的思考, 學習之初的內容總會有所考慮不周, 若是出錯還請多多指教.git

TL;DR

在瀏覽器中處理二進制數據,須要使用 Typed ArrayArrayBufferDataView.github

二進制數據使用的數據類型:Typed Array

在瀏覽器環境中使用的二進制數據類型通常爲 Typed Array(類型數組) ,它和普通的數組很像,只不過裏面的成員類型是嚴格要求,而且長度固定的.typescript

類型數組擁有如下幾種:canvas

  • Int8Array:每一個成員是有符號的 8 位整形,取值範圍 -128 - 127.
  • Uint8Array:每一個成員是無符號的 8 位整形,取值範圍 0 - 255.
  • Uint8ClampedArray:每一個成員是無符號的 8 位整形,取值範圍 0 - 255,和上面的類型不一樣的是,若成員超過 255 或小於 0,則取相應最大值 255 或 最小值 0,而 Uint8Array 會進行類推取一個越界後的映射值. 當在處理色彩相關邏輯時很是有用.
  • Int16Array:每一個成員爲有符號的 16 位整形,取值範圍 -32768 - 32767.
  • Uint16Array:每一個成員爲無符號的 16 位整形,取值範圍 0 - 65535.
  • Int32Array:每一個成員爲有符號的 32 位整形,取值範圍 -2147483648 - 2147483647.
  • Uint32Array:每一個成員爲無符號的 32 位整形,取值範圍 0 - 4294967295.
  • Float32Array:浮點數版本的 Int32Array.
  • Float64Array:64 位版本的 Float32Array.

簡單舉例:數組

const uInt8Array = new Uint8Array(10)
uInt8Array.length  // 10
uInt8Array[0] = 255  // 能夠操做下標.
複製代碼

類型數組的詳細文檔您能夠在這裏查閱.瀏覽器

存放數據的容器:ArrayBuffer

一個類型數組是須要存放到一個容器中的,這個容器叫作 ArrayBuffer.工具

ArrayBuffer 用來向瀏覽器申請一塊區域存放類型數組,做用有點像 malloc 的感受.學習

建立類型數組時能夠先建立一個 ArrayBuffer 而後傳入,也能夠直接建立指定長度的類型數組;若是直接建立,則瀏覽器會自動建立一個 ArrayBuffer 來存儲此類型數組:ui

const int8 = new Int8Array(10)
int8.buffer  // 這個就是存儲這個類型數組的 ArrayBuffer.

// 固然也能夠顯式建立:
const buffer = new ArrayBuffer(10)  // 申請 10 字節長度.
const int8 = new Int8Array(buffer)
複製代碼

ArrayBuffer 的詳細說明請看這裏.spa

方便操做二進制數據的工具:DataView

實際上類型數組可使用下標的方式來讀寫數組,只不過,太痛苦了點吧……還有大小端問題……

所以對於複雜的邏輯,咱們可使用 DataView 這個對象來對類型數組進行操做:

const buffer = new ArrayBuffer(16)
const dataView = new DataView(buffer, 0)
dataView.setInt16(2, 20)  // 在第二個 16 位數的位置上以大端寫入 20.
dataView.getInt16(2)  // 20

// 將 buffer 數據映射至一個 Int8Array 中查看 buffer 結構:
const int8Array = new Int8Array(buffer)  // 類型數組也能夠傳入一個 ArrayBuffer 來建立,將直接映射這個 ArrayBuffer.
console.log(int8Array) // [0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

// 以小端的方式再寫一次:
dataView.setInt16(2, 20, true)
console.log(int8Array) // [0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
複製代碼

DataView 提供了一些 API,詳細的還請各位慢慢查閱文檔.

使用案例:拼一個 goim 彈幕協議的數據包

goim 是 B 站搞的一個彈幕協議,在 WebSocket 上一樣能夠根據此協議進行數據設計,不過在 WebSocket 上使用它的話是就要使用二進制方式傳輸數據,而非文本,因此數據包是須要在瀏覽器進行拼接的.

根據 Github 的文檔能夠看到其數據包格式:

四個字節表示包長度,兩個字節表示頭部長度,兩個字節表示協議版本,四個字節表示當前操做,四個字節爲順序 ID 標記,剩下的爲數據本體.

那麼就能夠寫一個簡單的建立代碼:

// 根據文檔定義 offset.
const packetOffset = 0
const headerOffset = 4
const verOffset = 6
const opOffset = 8
const seqOffset = 12
const bodyOffset = 16

// 彈幕協議包頭的基礎長度爲 16.
const headerLength = 16

/** * 建立一個數據包. * * @param {IPacketOption} option * @returns {ArrayBuffer} */
function createPacket (option: IPacketOption): ArrayBuffer {
  const headerBuffer = new ArrayBuffer(headerLength)
  const headerView = new DataView(headerBuffer, 0)

  const bodyBuffer = stringToArrayBuffer(option.body)

  headerView.setInt32(packetOffset, headerLength + bodyBuffer.byteLength)  // 設置包長度, 長度 4 字節。
  headerView.setInt16(headerOffset, headerLength)  // 設置頭部度. 4 字節.
  headerView.setInt16(verOffset, option.version)  // 設置版本. 2 字節.
  headerView.setInt32(opOffset, option.operation)  // 設置操做標識符, 4 字節.
  headerView.setInt32(seqOffset, option.sequence)  // 設置序列號, 4 字節.
  return mergeArrayBuffer(headerBuffer, bodyBuffer)
}

/** * Packet 建立參數. * * @interface IPacketOption */
interface IPacketOption {
  version: number
  operation: number
  sequence: number
  body: string
}

/** * 將字符串轉換爲基於 Int8Array 的 ArrayBuffer. * * @param {string} content * @returns {ArrayBuffer} */
function stringToArrayBuffer (content: string): ArrayBuffer {
  const buffer = new ArrayBuffer(content.length)
  const bufferView = new Int8Array(buffer)

  for (let i = 0, length = content.length; i < length; i++) {
    bufferView[i] = content.charCodeAt(i)
  }

  return buffer
}

/** * 合併多個 ArrayBuffer 至同一個 ArrayBuffer 中. * * @param {...ArrayBuffer[]} arrayBuffers * @returns {ArrayBuffer} */
function mergeArrayBuffer (...arrayBuffers: ArrayBuffer[]): ArrayBuffer {
  let totalLength = 0
  arrayBuffers.forEach(item => {
    totalLength += item.byteLength
  })

  const result = new Int8Array(totalLength)
  let offset = 0
  arrayBuffers.forEach(item => {
    result.set(new Int8Array(item), offset)
    offset += item.byteLength
  })

  return result.buffer
}
複製代碼

好像能夠減小操做?

這裏有一段好像能夠減小操做?

// 這段代碼的大體意思是將 "存儲了像素信息的數組中的數據繪製在 Canvas 中".

const buffer = new ArrayBuffer(imageData.data.length)
const buffer8 = new Uint8ClampedArray(buffer)
const data = new Uint32Array(buffer)

for (let y = 0; y < canvasHeight; y++) {
  for (let x = 0; x < canvasWidth; x++) {
    if (typeof pixelArr[y] === 'undefined') { continue }

    const value = pixelArr[y][x]

    if (typeof value === 'undefined' || value === null) { continue }

    data[y * canvasWidth + x] =
      255 << 24 |
      value[2] << 16 |
      value[1] << 8 |
      value[0]
  }
}

imageData.data.set(buffer8)
context.putImageData(imageData, 0, 0)
複製代碼
相關文章
相關標籤/搜索