普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月25日掘金 前端

JS之类型化数组

作者 若梦plus
2025年12月24日 23:09

JS之类型化数组

引言

在传统JavaScript中,数组是动态类型的通用容器,可以存储任意类型的数据,但这种灵活性以性能为代价。随着Web应用对高性能计算的需求日益增长(WebGL图形渲染、音视频处理、大文件操作、WebAssembly互操作),JavaScript引入了类型化数组(Typed Arrays)—— 一种专门用于处理二进制数据的高效数据结构。

类型化数组提供了对原始二进制数据缓冲区的视图访问,使JavaScript能够以接近原生性能的方式处理大量数值数据。本文将深入探讨类型化数组的设计原理、内存模型、性能特性,以及在现代Web开发中的实际应用场景。


一、与普通数组的区别

1.1 核心差异对比

类型化数组与普通JavaScript数组存在本质区别,理解这些差异是正确使用类型化数组的前提。

关键区别对照表
特性 类型化数组 普通数组
元素类型 固定类型(Int8, Uint32, Float64等) 任意类型(数字、字符串、对象等)
内存布局 连续、紧凑的二进制内存块 稀疏数组,可能存在holes
性能 高性能(2-5倍速度提升) 相对较慢
内存占用 精确可控(每个元素固定字节数) 不可预测(每个元素8-16字节以上)
索引访问 仅数字索引 0 到 length-1 任意字符串作为key
长度可变性 长度固定,创建后不可改变 长度可动态变化
可存储内容 仅数值 任意JavaScript值
原型方法 部分数组方法(map, filter等) 完整数组方法(push, pop等)
底层存储 ArrayBuffer二进制缓冲区 JavaScript对象
代码示例对比
// 普通数组 - 灵活但低效
const regularArray = [];
regularArray[0] = 42;              // 数字
regularArray[1] = 'hello';         // 字符串
regularArray[2] = { name: 'obj' }; // 对象
regularArray[100] = 'sparse';      // 稀疏数组
console.log(regularArray.length);  // 101(中间有holes)

// 类型化数组 - 高效但类型固定
const typedArray = new Int32Array(4);
typedArray[0] = 42;                // 正确
typedArray[1] = 3.14;              // 会被截断为 3
// typedArray[2] = 'hello';        // 无效,会变成 0
// typedArray[2] = { name: 'obj' };// 无效,会变成 0
console.log(typedArray.length);    // 4(长度固定)
console.log(typedArray);           // Int32Array(4) [42, 3, 0, 0]

1.2 性能对比

类型化数组在数值计算场景下具有显著性能优势。

// 性能基准测试
function benchmarkArrays() {
  const size = 1000000;
  const iterations = 100;

  // 测试普通数组
  console.time('普通数组求和');
  const arr = new Array(size);
  for (let i = 0; i < size; i++) arr[i] = Math.random();

  for (let iter = 0; iter < iterations; iter++) {
    let sum = 0;
    for (let i = 0; i < size; i++) {
      sum += arr[i];
    }
  }
  console.timeEnd('普通数组求和');

  // 测试Float64Array
  console.time('Float64Array求和');
  const typedArr = new Float64Array(size);
  for (let i = 0; i < size; i++) typedArr[i] = Math.random();

  for (let iter = 0; iter < iterations; iter++) {
    let sum = 0;
    for (let i = 0; i < size; i++) {
      sum += typedArr[i];
    }
  }
  console.timeEnd('Float64Array求和');
}

benchmarkArrays();
/*
典型输出:
普通数组求和: 1842.50ms
Float64Array求和: 623.20ms  (快约3倍!)
*/

1.3 内存占用对比

function compareMemoryUsage() {
  const size = 1000000;

  // 普通数组 - 内存占用不确定
  const regularArray = new Array(size);
  for (let i = 0; i < size; i++) {
    regularArray[i] = i;
  }
  // 估算内存占用: ~8-16 MB(每个元素8-16字节)

  // Int32Array - 精确内存控制
  const int32Array = new Int32Array(size);
  for (let i = 0; i < size; i++) {
    int32Array[i] = i;
  }

  console.log('Int32Array字节长度:', int32Array.byteLength);
  console.log('Int32Array内存占用:', (int32Array.byteLength / 1024 / 1024).toFixed(2), 'MB');
  // 输出: 4000000 bytes = 3.81 MB

  console.log('每个元素字节数:', int32Array.BYTES_PER_ELEMENT); // 4
}

compareMemoryUsage();

1.4 方法差异

const regularArray = [1, 2, 3, 4, 5];
const typedArray = new Int32Array([1, 2, 3, 4, 5]);

// ✅ 两者都支持的方法
console.log(regularArray.map(x => x * 2));    // [2, 4, 6, 8, 10]
console.log(typedArray.map(x => x * 2));      // Int32Array [2, 4, 6, 8, 10]

console.log(regularArray.filter(x => x > 2)); // [3, 4, 5]
console.log(typedArray.filter(x => x > 2));   // Int32Array [3, 4, 5]

// ❌ 普通数组独有的方法(类型化数组不支持)
regularArray.push(6);      // ✅ 可以
// typedArray.push(6);     // ❌ TypeError: typedArray.push is not a function

regularArray.pop();        // ✅ 可以
// typedArray.pop();       // ❌ TypeError

regularArray.splice(1, 2); // ✅ 可以
// typedArray.splice(1, 2);// ❌ TypeError

// ✅ 类型化数组独有的属性
console.log(typedArray.buffer);           // ArrayBuffer对象
console.log(typedArray.byteLength);       // 20(5个元素 × 4字节)
console.log(typedArray.byteOffset);       // 0
console.log(typedArray.BYTES_PER_ELEMENT);// 4

二、类型化数组有哪些

2.1 完整类型列表

JavaScript提供了11种类型化数组,覆盖不同的整数和浮点数类型。

类型化数组架构图
graph TB
    A[ArrayBuffer 原始内存缓冲区] --> B[TypedArray视图]
    A --> C[DataView视图]

    B --> D1[Int8Array<br/>8位有符号整数<br/>-128 到 127]
    B --> D2[Uint8Array<br/>8位无符号整数<br/>0 到 255]
    B --> D3[Uint8ClampedArray<br/>8位无符号整数 钳位<br/>0 到 255]
    B --> D4[Int16Array<br/>16位有符号整数<br/>-32768 到 32767]
    B --> D5[Uint16Array<br/>16位无符号整数<br/>0 到 65535]
    B --> D6[Int32Array<br/>32位有符号整数<br/>-2^31 到 2^31-1]
    B --> D7[Uint32Array<br/>32位无符号整数<br/>0 到 2^32-1]
    B --> D8[Float32Array<br/>32位IEEE浮点数]
    B --> D9[Float64Array<br/>64位IEEE浮点数]
    B --> D10[BigInt64Array<br/>64位有符号BigInt]
    B --> D11[BigUint64Array<br/>64位无符号BigInt]

    C --> E[灵活的混合类型读写]

    style A fill:#FF6B6B,color:#fff
    style B fill:#4ECDC4,color:#000
    style C fill:#FFD93D,color:#000
详细类型说明表
类型 字节数 取值范围 用途
Int8Array 1 -128 到 127 小整数、ASCII字符
Uint8Array 1 0 到 255 二进制数据、像素RGB值
Uint8ClampedArray 1 0 到 255(钳位) Canvas像素数据
Int16Array 2 -32,768 到 32,767 音频样本
Uint16Array 2 0 到 65,535 Unicode字符
Int32Array 4 -2,147,483,648 到 2,147,483,647 大整数计算
Uint32Array 4 0 到 4,294,967,295 颜色RGBA值
Float32Array 4 ±1.18e-38 到 ±3.4e38 WebGL坐标、3D图形
Float64Array 8 ±5e-324 到 ±1.8e308 高精度科学计算
BigInt64Array 8 -2^63 到 2^63-1 超大整数
BigUint64Array 8 0 到 2^64-1 超大无符号整数

2.2 类型选择示例

// 示例1: 8位整数类型
const int8 = new Int8Array(4);
int8[0] = 127;   // 最大值
int8[1] = -128;  // 最小值
int8[2] = 200;   // 溢出: 200 - 256 = -56
console.log(int8); // Int8Array(4) [127, -128, -56, 0]

const uint8 = new Uint8Array(4);
uint8[0] = 255;  // 最大值
uint8[1] = 0;    // 最小值
uint8[2] = -10;  // 负数溢出: 256 - 10 = 246
uint8[3] = 300;  // 溢出: 300 - 256 = 44
console.log(uint8); // Uint8Array(4) [255, 0, 246, 44]

// Uint8ClampedArray 特殊钳位行为
const clamped = new Uint8ClampedArray(4);
clamped[0] = 255;  // 正常
clamped[1] = 300;  // 钳位到 255
clamped[2] = -10;  // 钳位到 0
clamped[3] = 128.6;// 四舍五入到 129
console.log(clamped); // Uint8ClampedArray(4) [255, 255, 0, 129]

// 示例2: 浮点数类型
const float32 = new Float32Array(3);
float32[0] = 3.14159265359;
console.log(float32[0]); // 3.1415927410125732 (精度损失)

const float64 = new Float64Array(3);
float64[0] = 3.14159265359;
console.log(float64[0]); // 3.14159265359 (高精度)

// 示例3: BigInt类型
const bigInt64 = new BigInt64Array(2);
bigInt64[0] = 9007199254740991n;      // JavaScript安全整数最大值
bigInt64[1] = 9223372036854775807n;   // BigInt64最大值
console.log(bigInt64);

// 示例4: 每个类型的字节大小
console.log('Int8Array:', Int8Array.BYTES_PER_ELEMENT);          // 1
console.log('Int16Array:', Int16Array.BYTES_PER_ELEMENT);        // 2
console.log('Int32Array:', Int32Array.BYTES_PER_ELEMENT);        // 4
console.log('Float32Array:', Float32Array.BYTES_PER_ELEMENT);    // 4
console.log('Float64Array:', Float64Array.BYTES_PER_ELEMENT);    // 8
console.log('BigInt64Array:', BigInt64Array.BYTES_PER_ELEMENT);  // 8

2.3 类型选择决策树

graph TD
    A[需要存储数值数据] --> B{整数还是浮点数?}

    B -->|整数| C{是否有负数?}
    B -->|浮点数| D{精度要求}

    C -->|有| E{数值范围?}
    C -->|无| F{数值范围?}

    E -->|小 -128~127| G[Int8Array]
    E -->|中 -32K~32K| H[Int16Array]
    E -->|大 -2B~2B| I[Int32Array]
    E -->|超大| J[BigInt64Array]

    F -->|小 0~255| K{是否Canvas像素?}
    F -->|中 0~65K| L[Uint16Array]
    F -->|大 0~4B| M[Uint32Array]
    F -->|超大| N[BigUint64Array]

    K -->|是| O[Uint8ClampedArray]
    K -->|否| P[Uint8Array]

    D -->|单精度足够| Q[Float32Array]
    D -->|需要高精度| R[Float64Array]

    style G fill:#4ECDC4,color:#000
    style H fill:#4ECDC4,color:#000
    style I fill:#4ECDC4,color:#000
    style J fill:#4ECDC4,color:#000
    style L fill:#4ECDC4,color:#000
    style M fill:#4ECDC4,color:#000
    style N fill:#4ECDC4,color:#000
    style O fill:#FFD93D,color:#000
    style P fill:#4ECDC4,color:#000
    style Q fill:#50C878,color:#fff
    style R fill:#50C878,color:#fff

三、创建和使用类型化数组

3.1 创建类型化数组的多种方式

方式1: 指定长度创建
// 创建指定长度的类型化数组(元素初始化为0)
const arr1 = new Int32Array(5);
console.log(arr1); // Int32Array(5) [0, 0, 0, 0, 0]
console.log(arr1.length);      // 5
console.log(arr1.byteLength);  // 20 (5 × 4字节)
方式2: 从普通数组创建
// 从普通数组或类数组对象创建
const arr2 = new Float32Array([1, 2, 3, 4, 5]);
console.log(arr2); // Float32Array(5) [1, 2, 3, 4, 5]

// 从Set创建
const set = new Set([10, 20, 30]);
const arr3 = new Uint16Array(set);
console.log(arr3); // Uint16Array(3) [10, 20, 30]
方式3: 从ArrayBuffer创建
// 创建ArrayBuffer
const buffer = new ArrayBuffer(16); // 16字节缓冲区

// 从ArrayBuffer创建不同视图
const view8 = new Uint8Array(buffer);   // 16个8位元素
const view16 = new Uint16Array(buffer); // 8个16位元素
const view32 = new Uint32Array(buffer); // 4个32位元素

console.log(view8.length);  // 16
console.log(view16.length); // 8
console.log(view32.length); // 4

// 指定偏移量和长度
const partialView = new Uint8Array(buffer, 4, 8);
console.log(partialView.length); // 8(从第4字节开始,读取8个字节)
方式4: 从另一个类型化数组创建
// 复制另一个类型化数组
const original = new Int32Array([1, 2, 3, 4, 5]);
const copy = new Int32Array(original);
console.log(copy); // Int32Array(5) [1, 2, 3, 4, 5]

// 类型转换
const floatArray = new Float32Array([1.5, 2.7, 3.9]);
const intArray = new Int32Array(floatArray); // 自动截断小数
console.log(intArray); // Int32Array(3) [1, 2, 3]

3.2 基本操作

读取和写入元素
const arr = new Int16Array(5);

// 写入元素
arr[0] = 100;
arr[1] = 200;
arr[2] = 300;

// 读取元素
console.log(arr[0]); // 100
console.log(arr[1]); // 200

// 使用set方法批量设置
arr.set([10, 20, 30], 2); // 从索引2开始设置
console.log(arr); // Int16Array(5) [100, 200, 10, 20, 30]

// 使用fill填充
arr.fill(0); // 全部填充为0
console.log(arr); // Int16Array(5) [0, 0, 0, 0, 0]

arr.fill(99, 1, 4); // 从索引1到3填充99
console.log(arr); // Int16Array(5) [0, 99, 99, 99, 0]
数组方法
const numbers = new Float32Array([1.5, 2.5, 3.5, 4.5, 5.5]);

// map - 映射转换
const doubled = numbers.map(x => x * 2);
console.log(doubled); // Float32Array(5) [3, 5, 7, 9, 11]

// filter - 过滤
const filtered = numbers.filter(x => x > 3);
console.log(filtered); // Float32Array(3) [3.5, 4.5, 5.5]

// reduce - 归约
const sum = numbers.reduce((acc, val) => acc + val, 0);
console.log(sum); // 17.5

// forEach - 遍历
numbers.forEach((value, index) => {
  console.log(`[${index}] = ${value}`);
});

// find - 查找
const found = numbers.find(x => x > 4);
console.log(found); // 4.5

// some / every - 检测
console.log(numbers.some(x => x > 5));  // true
console.log(numbers.every(x => x > 0)); // true

// sort - 排序
const unsorted = new Int32Array([5, 2, 8, 1, 9]);
unsorted.sort();
console.log(unsorted); // Int32Array(5) [1, 2, 5, 8, 9]
切片操作
const original = new Uint8Array([10, 20, 30, 40, 50, 60]);

// slice - 创建新数组(复制数据)
const sliced = original.slice(1, 4);
console.log(sliced); // Uint8Array(3) [20, 30, 40]
sliced[0] = 99;
console.log(original[1]); // 20(原数组不受影响)

// subarray - 创建视图(零拷贝,共享内存)
const subView = original.subarray(1, 4);
console.log(subView); // Uint8Array(3) [20, 30, 40]
subView[0] = 99;
console.log(original[1]); // 99(原数组被修改!)

console.log('slice会复制:', sliced.buffer !== original.buffer);
console.log('subarray共享内存:', subView.buffer === original.buffer);

3.3 ArrayBuffer与视图的关系

多个视图共享同一缓冲区
// 创建16字节缓冲区
const buffer = new ArrayBuffer(16);

// 创建多个视图
const view8 = new Uint8Array(buffer);
const view16 = new Uint16Array(buffer);
const view32 = new Uint32Array(buffer);

// 通过8位视图写入数据
view8[0] = 0xFF;
view8[1] = 0x00;
view8[2] = 0xFF;
view8[3] = 0x00;

// 通过16位视图读取(小端序)
console.log(view16[0].toString(16)); // ff (0x00FF)
console.log(view16[1].toString(16)); // ff (0x00FF)

// 通过32位视图读取
console.log(view32[0].toString(16)); // ff00ff

// 验证共享
console.log(view8.buffer === view16.buffer); // true
console.log(view8.buffer === view32.buffer); // true
内存布局可视化
const buffer = new ArrayBuffer(8);
const uint8View = new Uint8Array(buffer);
const uint32View = new Uint32Array(buffer);

// 写入32位整数
uint32View[0] = 0x12345678;
uint32View[1] = 0xABCDEF00;

// 查看字节布局(小端序系统)
console.log('字节布局:');
console.log([...uint8View].map(b => b.toString(16).padStart(2, '0')));
// 小端序输出: ['78', '56', '34', '12', '00', 'ef', 'cd', 'ab']
// 大端序输出: ['12', '34', '56', '78', 'ab', 'cd', 'ef', '00']

// 内存布局示意
console.log(`
内存地址:  0    1    2    3    4    5    6    7
字节值:   78   56   34   12   00   ef   cd   ab
          |___uint32[0]___|   |___uint32[1]___|
          0x12345678          0xABCDEF00
`);

3.4 实用工具函数

类型转换工具
// 工具类:类型化数组转换
class TypedArrayUtils {
  // 转换为普通数组
  static toArray(typedArray) {
    return Array.from(typedArray);
  }

  // 从十六进制字符串创建Uint8Array
  static fromHex(hexString) {
    const bytes = hexString.match(/.{1,2}/g);
    return new Uint8Array(bytes.map(byte => parseInt(byte, 16)));
  }

  // 转换为十六进制字符串
  static toHex(typedArray) {
    return Array.from(typedArray)
      .map(b => b.toString(16).padStart(2, '0'))
      .join('');
  }

  // 从Base64字符串创建
  static fromBase64(base64String) {
    const binaryString = atob(base64String);
    const bytes = new Uint8Array(binaryString.length);
    for (let i = 0; i < binaryString.length; i++) {
      bytes[i] = binaryString.charCodeAt(i);
    }
    return bytes;
  }

  // 转换为Base64字符串
  static toBase64(typedArray) {
    const binaryString = String.fromCharCode(...typedArray);
    return btoa(binaryString);
  }
}

// 使用示例
const hexData = 'deadbeef';
const bytes = TypedArrayUtils.fromHex(hexData);
console.log(bytes); // Uint8Array(4) [222, 173, 190, 239]
console.log(TypedArrayUtils.toHex(bytes)); // 'deadbeef'

const base64 = TypedArrayUtils.toBase64(bytes);
console.log(base64); // '3q2+7w=='
console.log(TypedArrayUtils.fromBase64(base64)); // Uint8Array(4) [222, 173, 190, 239]
数据操作工具
// 拼接多个类型化数组
function concatenate(...arrays) {
  const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
  const result = new arrays[0].constructor(totalLength);

  let offset = 0;
  for (const arr of arrays) {
    result.set(arr, offset);
    offset += arr.length;
  }

  return result;
}

// 使用示例
const arr1 = new Uint8Array([1, 2, 3]);
const arr2 = new Uint8Array([4, 5, 6]);
const arr3 = new Uint8Array([7, 8, 9]);
const combined = concatenate(arr1, arr2, arr3);
console.log(combined); // Uint8Array(9) [1, 2, 3, 4, 5, 6, 7, 8, 9]

// 比较两个类型化数组
function equals(arr1, arr2) {
  if (arr1.length !== arr2.length) return false;
  for (let i = 0; i < arr1.length; i++) {
    if (arr1[i] !== arr2[i]) return false;
  }
  return true;
}

console.log(equals(arr1, new Uint8Array([1, 2, 3]))); // true
console.log(equals(arr1, new Uint8Array([1, 2, 4]))); // false

四、DataView:灵活的二进制数据视图

4.1 DataView基础

DataView提供了比TypedArray更灵活的二进制数据访问方式,支持混合类型读写和显式字节序控制。

// 创建DataView
const buffer = new ArrayBuffer(24);
const dataView = new DataView(buffer);

// 写入不同类型的数据
dataView.setInt8(0, -42);                    // 1字节,有符号
dataView.setUint8(1, 255);                   // 1字节,无符号
dataView.setInt16(2, -1000, true);           // 2字节,小端序
dataView.setUint16(4, 65535, false);         // 2字节,大端序
dataView.setInt32(6, -123456, true);         // 4字节
dataView.setUint32(10, 4294967295, true);    // 4字节
dataView.setFloat32(14, 3.14, true);         // 4字节,IEEE 754
dataView.setFloat64(18, Math.PI, true);      // 8字节

// 读取数据
console.log(dataView.getInt8(0));            // -42
console.log(dataView.getUint8(1));           // 255
console.log(dataView.getInt16(2, true));     // -1000
console.log(dataView.getUint16(4, false));   // 65535
console.log(dataView.getFloat32(14, true));  // 3.140000104904175
console.log(dataView.getFloat64(18, true));  // 3.141592653589793

4.2 字节序(Endianness)

// 检测系统字节序
function getEndianness() {
  const buffer = new ArrayBuffer(2);
  const uint8 = new Uint8Array(buffer);
  const uint16 = new Uint16Array(buffer);

  uint16[0] = 0xAABB;

  if (uint8[0] === 0xBB) {
    return 'Little-Endian'; // 低字节在前(x86/x64)
  } else {
    return 'Big-Endian';    // 高字节在前(网络字节序)
  }
}

console.log('系统字节序:', getEndianness());

// 字节序转换工具
class ByteOrderConverter {
  // 16位字节序转换
  static swap16(value) {
    return ((value & 0xFF) << 8) | ((value >> 8) & 0xFF);
  }

  // 32位字节序转换
  static swap32(value) {
    return (
      ((value & 0xFF) << 24) |
      ((value & 0xFF00) << 8) |
      ((value >> 8) & 0xFF00) |
      ((value >> 24) & 0xFF)
    );
  }

  // 从大端序读取32位整数
  static readUint32BE(buffer, offset = 0) {
    const view = new DataView(buffer);
    return view.getUint32(offset, false); // false = 大端序
  }

  // 从小端序读取32位整数
  static readUint32LE(buffer, offset = 0) {
    const view = new DataView(buffer);
    return view.getUint32(offset, true); // true = 小端序
  }
}

// 示例:跨平台数据交换
const buffer = new ArrayBuffer(4);
const dataView = new DataView(buffer);

// 写入大端序(网络字节序)
dataView.setUint32(0, 0x12345678, false);
console.log('大端序读取:', ByteOrderConverter.readUint32BE(buffer, 0).toString(16)); // 12345678

// 写入小端序
dataView.setUint32(0, 0x12345678, true);
console.log('小端序读取:', ByteOrderConverter.readUint32LE(buffer, 0).toString(16)); // 12345678

4.3 二进制协议解析

// 示例:解析自定义二进制协议头
class ProtocolParser {
  /*
   * 协议格式:
   * [0-3]   Magic Number (4字节) - 0x89504E47
   * [4-7]   Version (4字节)
   * [8-11]  Payload Length (4字节)
   * [12-15] Checksum (4字节)
   * [16-19] Timestamp (4字节)
   * [20+]   Payload Data
   */

  static MAGIC_NUMBER = 0x89504E47;
  static HEADER_SIZE = 20;

  static parseHeader(buffer) {
    const view = new DataView(buffer);

    const magic = view.getUint32(0, false);
    if (magic !== this.MAGIC_NUMBER) {
      throw new Error('Invalid magic number');
    }

    return {
      magic: magic.toString(16),
      version: view.getUint32(4, false),
      payloadLength: view.getUint32(8, false),
      checksum: view.getUint32(12, false),
      timestamp: view.getUint32(16, false),
      payloadOffset: this.HEADER_SIZE
    };
  }

  static createHeader(version, payloadLength, checksum) {
    const buffer = new ArrayBuffer(this.HEADER_SIZE);
    const view = new DataView(buffer);

    view.setUint32(0, this.MAGIC_NUMBER, false);
    view.setUint32(4, version, false);
    view.setUint32(8, payloadLength, false);
    view.setUint32(12, checksum, false);
    view.setUint32(16, Math.floor(Date.now() / 1000), false);

    return buffer;
  }
}

// 使用示例
const header = ProtocolParser.createHeader(1, 1024, 0xDEADBEEF);
const parsed = ProtocolParser.parseHeader(header);
console.log(parsed);
/*
{
  magic: '89504e47',
  version: 1,
  payloadLength: 1024,
  checksum: 3735928559,
  timestamp: 1703347200,
  payloadOffset: 20
}
*/

五、实战应用场景

5.1 WebGL纹理数据处理

// WebGL纹理生成器
class TextureGenerator {
  // 创建渐变纹理
  static createGradientTexture(width, height) {
    // 使用Uint8Array存储RGBA像素数据
    const size = width * height * 4; // RGBA = 4 bytes per pixel
    const data = new Uint8Array(size);

    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        const index = (y * width + x) * 4;

        // 计算渐变颜色
        const r = Math.floor((x / width) * 255);
        const g = Math.floor((y / height) * 255);
        const b = 128;
        const a = 255;

        data[index] = r;
        data[index + 1] = g;
        data[index + 2] = b;
        data[index + 3] = a;
      }
    }

    return data;
  }

  // 创建噪声纹理
  static createNoiseTexture(width, height) {
    const size = width * height * 4;
    const data = new Uint8Array(size);

    for (let i = 0; i < size; i += 4) {
      const value = Math.floor(Math.random() * 256);
      data[i] = value;       // R
      data[i + 1] = value;   // G
      data[i + 2] = value;   // B
      data[i + 3] = 255;     // A
    }

    return data;
  }
}

// 在WebGL中使用
function uploadTextureToWebGL(gl, textureData, width, height) {
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  gl.texImage2D(
    gl.TEXTURE_2D,
    0,                    // mipmap level
    gl.RGBA,              // internal format
    width,
    height,
    0,                    // border
    gl.RGBA,              // format
    gl.UNSIGNED_BYTE,     // type
    textureData           // Uint8Array数据
  );

  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

  return texture;
}

// 使用示例
const gradientTexture = TextureGenerator.createGradientTexture(256, 256);
console.log('纹理数据大小:', gradientTexture.byteLength, 'bytes'); // 262144 bytes

5.2 音频处理

// 音频波形生成器
class AudioWaveformGenerator {
  constructor(audioContext) {
    this.audioContext = audioContext;
    this.sampleRate = audioContext.sampleRate;
  }

  // 生成正弦波
  generateSineWave(frequency, duration, amplitude = 0.5) {
    const sampleCount = Math.floor(this.sampleRate * duration);
    const buffer = this.audioContext.createBuffer(
      1,                    // 单声道
      sampleCount,
      this.sampleRate
    );

    // 获取Float32Array类型的音频数据
    const channelData = buffer.getChannelData(0);

    for (let i = 0; i < sampleCount; i++) {
      const t = i / this.sampleRate;
      channelData[i] = amplitude * Math.sin(2 * Math.PI * frequency * t);
    }

    return buffer;
  }

  // 生成方波
  generateSquareWave(frequency, duration, amplitude = 0.5) {
    const sampleCount = Math.floor(this.sampleRate * duration);
    const buffer = this.audioContext.createBuffer(1, sampleCount, this.sampleRate);
    const channelData = buffer.getChannelData(0);

    const period = this.sampleRate / frequency;

    for (let i = 0; i < sampleCount; i++) {
      channelData[i] = ((i % period) < (period / 2)) ? amplitude : -amplitude;
    }

    return buffer;
  }
}

// 使用示例
const audioContext = new AudioContext();
const generator = new AudioWaveformGenerator(audioContext);

// 生成440Hz的A音
const tone = generator.generateSineWave(440, 1.0);

// 播放
const source = audioContext.createBufferSource();
source.buffer = tone;
source.connect(audioContext.destination);
source.start();

5.3 文件分片上传

// 大文件分片上传器
class ChunkedFileUploader {
  constructor(file, chunkSize = 1024 * 1024) { // 默认1MB每片
    this.file = file;
    this.chunkSize = chunkSize;
    this.totalChunks = Math.ceil(file.size / chunkSize);
    this.uploadedChunks = 0;
  }

  async upload(url, onProgress) {
    for (let chunkIndex = 0; chunkIndex < this.totalChunks; chunkIndex++) {
      const start = chunkIndex * this.chunkSize;
      const end = Math.min(start + this.chunkSize, this.file.size);

      // 读取文件片段为ArrayBuffer
      const chunkData = await this.readChunk(start, end);

      // 上传分片
      await this.uploadChunk(url, chunkIndex, chunkData);

      this.uploadedChunks++;
      if (onProgress) {
        onProgress({
          chunkIndex,
          totalChunks: this.totalChunks,
          progress: (this.uploadedChunks / this.totalChunks) * 100
        });
      }
    }
  }

  async readChunk(start, end) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      const blob = this.file.slice(start, end);

      reader.onload = (e) => resolve(e.target.result);
      reader.onerror = reject;

      reader.readAsArrayBuffer(blob);
    });
  }

  async uploadChunk(url, chunkIndex, chunkData) {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/octet-stream',
        'X-Chunk-Index': chunkIndex.toString(),
        'X-Total-Chunks': this.totalChunks.toString()
      },
      body: chunkData
    });

    if (!response.ok) {
      throw new Error(`Upload failed: ${response.statusText}`);
    }
  }
}

// 使用示例
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const uploader = new ChunkedFileUploader(file, 1024 * 1024);

  await uploader.upload('/api/upload', (progress) => {
    console.log(`上传进度: ${progress.progress.toFixed(2)}%`);
  });

  console.log('上传完成!');
});

六、性能优化最佳实践

6.1 避免频繁分配

// ❌ 错误:频繁创建新数组
function processDataBad(iterations) {
  for (let i = 0; i < iterations; i++) {
    const temp = new Float32Array(1000); // 每次循环都分配
    // ... 处理
  }
}

// ✅ 正确:重用数组
function processDataGood(iterations) {
  const temp = new Float32Array(1000); // 只分配一次
  for (let i = 0; i < iterations; i++) {
    temp.fill(0); // 清空重用
    // ... 处理
  }
}

6.2 使用subarray而非slice

const original = new Uint8Array(1000);

// ❌ slice创建新数组(拷贝数据)
const copied = original.slice(100, 200);

// ✅ subarray创建视图(零拷贝)
const view = original.subarray(100, 200);

6.3 批量操作

// ❌ 逐个设置
const arr = new Float32Array(1000);
for (let i = 0; i < 1000; i++) {
  arr[i] = i;
}

// ✅ 使用set批量设置
const source = new Float32Array(1000);
for (let i = 0; i < 1000; i++) {
  source[i] = i;
}
const arr2 = new Float32Array(1000);
arr2.set(source);

6.4 内存池管理

// 类型化数组内存池
class TypedArrayPool {
  constructor(arrayType, initialSize = 10) {
    this.ArrayType = arrayType;
    this.pool = [];
    this.inUse = new Set();

    // 预分配
    for (let i = 0; i < initialSize; i++) {
      this.pool.push(new arrayType(0));
    }
  }

  acquire(size) {
    let array = this.pool.find(arr => arr.length >= size && !this.inUse.has(arr));

    if (!array) {
      array = new this.ArrayType(size);
      this.pool.push(array);
    }

    this.inUse.add(array);
    return array.subarray(0, size);
  }

  release(array) {
    const originalArray = this.pool.find(arr =>
      arr.buffer === array.buffer &&
      arr.byteOffset === array.byteOffset
    );

    if (originalArray) {
      this.inUse.delete(originalArray);
    }
  }

  getStats() {
    return {
      totalArrays: this.pool.length,
      inUse: this.inUse.size,
      available: this.pool.length - this.inUse.size
    };
  }
}

// 使用示例
const pool = new TypedArrayPool(Float32Array, 5);

function processData(data) {
  const buffer = pool.acquire(data.length);

  // 处理数据
  for (let i = 0; i < data.length; i++) {
    buffer[i] = data[i] * 2;
  }

  // ... 使用buffer

  // 释放回池
  pool.release(buffer);
}

七、总结与建议

7.1 何时使用类型化数组

适用场景:

  • ✅ WebGL/WebGPU图形渲染
  • ✅ Canvas像素操作
  • ✅ Web Audio音频处理
  • ✅ 二进制文件读写
  • ✅ 网络协议解析
  • ✅ WebSocket二进制通信
  • ✅ WebAssembly数据交换
  • ✅ 大量数值计算
  • ✅ 图像/视频处理

不适用场景:

  • ❌ 存储混合类型数据(字符串、对象等)
  • ❌ 需要动态改变数组长度
  • ❌ 数据量很小(< 100个元素)
  • ❌ 需要频繁push/pop操作
  • ❌ 不关心性能的业务逻辑

7.2 类型选择建议

场景 推荐类型 原因
Canvas像素数据 Uint8ClampedArray 自动钳位0-255,符合像素值特性
WebGL顶点坐标 Float32Array GPU友好,足够精度
音频样本 Float32Array 音频处理标准格式
RGB颜色值 Uint8Array 0-255范围,内存高效
二进制协议 Uint8Array + DataView 灵活的字节级访问
索引数据 Uint16Array 或 Uint32Array 根据顶点数量选择
科学计算 Float64Array 高精度
大整数ID BigInt64Array 超出安全整数范围

7.3 关键要点

  1. 类型化数组是固定长度的 - 创建后无法改变大小
  2. 元素类型必须统一 - 只能存储特定数值类型
  3. 性能优于普通数组 - 2-5倍速度提升,更少内存占用
  4. 基于ArrayBuffer - 多个视图可共享同一内存
  5. subarray是零拷贝 - 与原数组共享内存
  6. DataView最灵活 - 支持混合类型和字节序控制
  7. 注意字节序 - 跨平台数据交换需显式指定
  8. 重用而非重建 - 使用对象池减少GC压力

类型化数组是JavaScript处理二进制数据和高性能数值计算的基石,在WebGL、Canvas、音视频处理、网络通信、WebAssembly等现代Web技术栈中扮演着核心角色。掌握类型化数组的原理和最佳实践,是构建高性能Web应用的必备技能。


八、SharedArrayBuffer与多线程编程

8.1 SharedArrayBuffer基础

SharedArrayBuffer允许多个Worker线程共享同一块内存,实现真正的多线程并行计算。

// 主线程 main.js
const sharedBuffer = new SharedArrayBuffer(1024); // 1KB共享内存
const sharedArray = new Int32Array(sharedBuffer);

// 初始化共享数据
for (let i = 0; i < sharedArray.length; i++) {
  sharedArray[i] = i;
}

// 创建多个Worker共享同一内存
const worker1 = new Worker('worker.js');
const worker2 = new Worker('worker.js');

worker1.postMessage({ buffer: sharedBuffer, startIndex: 0, endIndex: 128 });
worker2.postMessage({ buffer: sharedBuffer, startIndex: 128, endIndex: 256 });

// 监听结果
worker1.onmessage = (e) => {
  console.log('Worker 1 完成:', e.data);
};

worker2.onmessage = (e) => {
  console.log('Worker 2 完成:', e.data);
};
// worker.js
self.onmessage = (e) => {
  const { buffer, startIndex, endIndex } = e.data;
  const array = new Int32Array(buffer);

  // 并行处理数据
  for (let i = startIndex; i < endIndex; i++) {
    array[i] = array[i] * 2; // 每个元素乘以2
  }

  self.postMessage({ status: 'complete', range: [startIndex, endIndex] });
};

8.2 Atomics原子操作

Atomics提供原子操作,避免多线程竞争条件。

// 原子操作示例
class AtomicCounter {
  constructor(sharedBuffer, index = 0) {
    this.array = new Int32Array(sharedBuffer);
    this.index = index;
  }

  // 原子增加
  increment() {
    return Atomics.add(this.array, this.index, 1);
  }

  // 原子减少
  decrement() {
    return Atomics.sub(this.array, this.index, 1);
  }

  // 原子读取
  load() {
    return Atomics.load(this.array, this.index);
  }

  // 原子存储
  store(value) {
    return Atomics.store(this.array, this.index, value);
  }

  // 比较并交换(CAS)
  compareExchange(expectedValue, newValue) {
    return Atomics.compareExchange(
      this.array,
      this.index,
      expectedValue,
      newValue
    );
  }
}

// 使用示例:多线程安全计数器
const sharedBuffer = new SharedArrayBuffer(4);
const counter = new AtomicCounter(sharedBuffer);

// 在多个Worker中安全地增加计数
// Worker 1: counter.increment();
// Worker 2: counter.increment();
// Worker 3: counter.increment();

8.3 线程同步:等待与通知

// 生产者-消费者模式
class SharedQueue {
  constructor(size) {
    // 布局:[head, tail, ...data]
    this.buffer = new SharedArrayBuffer((size + 2) * 4);
    this.array = new Int32Array(this.buffer);
    this.size = size;
    this.headIndex = 0;
    this.tailIndex = 1;
    this.dataStart = 2;
  }

  // 生产者:添加数据
  enqueue(value) {
    while (true) {
      const tail = Atomics.load(this.array, this.tailIndex);
      const head = Atomics.load(this.array, this.headIndex);
      const count = (tail - head + this.size) % this.size;

      // 队列已满,等待消费者
      if (count >= this.size - 1) {
        Atomics.wait(this.array, this.tailIndex, tail);
        continue;
      }

      const index = this.dataStart + (tail % this.size);
      Atomics.store(this.array, index, value);

      const newTail = (tail + 1) % this.size;
      Atomics.store(this.array, this.tailIndex, newTail);

      // 通知消费者
      Atomics.notify(this.array, this.headIndex, 1);
      break;
    }
  }

  // 消费者:取出数据
  dequeue() {
    while (true) {
      const head = Atomics.load(this.array, this.headIndex);
      const tail = Atomics.load(this.array, this.tailIndex);

      // 队列为空,等待生产者
      if (head === tail) {
        Atomics.wait(this.array, this.headIndex, head);
        continue;
      }

      const index = this.dataStart + (head % this.size);
      const value = Atomics.load(this.array, index);

      const newHead = (head + 1) % this.size;
      Atomics.store(this.array, this.headIndex, newHead);

      // 通知生产者
      Atomics.notify(this.array, this.tailIndex, 1);
      return value;
    }
  }
}

8.4 并行图像处理

// 主线程:并行图像滤镜
class ParallelImageProcessor {
  constructor(workerCount = 4) {
    this.workerCount = workerCount;
    this.workers = [];

    for (let i = 0; i < workerCount; i++) {
      this.workers.push(new Worker('image-worker.js'));
    }
  }

  async processImage(imageData) {
    const { width, height, data } = imageData;
    const pixelCount = width * height;

    // 创建共享内存
    const sharedBuffer = new SharedArrayBuffer(data.length);
    const sharedArray = new Uint8ClampedArray(sharedBuffer);
    sharedArray.set(data);

    // 分配任务给Worker
    const chunkSize = Math.ceil(pixelCount / this.workerCount);
    const promises = this.workers.map((worker, i) => {
      const startPixel = i * chunkSize;
      const endPixel = Math.min((i + 1) * chunkSize, pixelCount);

      return new Promise((resolve) => {
        worker.onmessage = () => resolve();
        worker.postMessage({
          buffer: sharedBuffer,
          width,
          height,
          startPixel,
          endPixel
        });
      });
    });

    await Promise.all(promises);

    // 返回处理后的数据
    return new ImageData(
      new Uint8ClampedArray(sharedBuffer),
      width,
      height
    );
  }
}

// image-worker.js
self.onmessage = (e) => {
  const { buffer, width, startPixel, endPixel } = e.data;
  const pixels = new Uint8ClampedArray(buffer);

  // 灰度化滤镜
  for (let i = startPixel; i < endPixel; i++) {
    const offset = i * 4;
    const r = pixels[offset];
    const g = pixels[offset + 1];
    const b = pixels[offset + 2];

    const gray = Math.floor(0.299 * r + 0.587 * g + 0.114 * b);

    pixels[offset] = gray;
    pixels[offset + 1] = gray;
    pixels[offset + 2] = gray;
    // Alpha保持不变
  }

  self.postMessage({ status: 'complete' });
};

九、高级图像处理算法

9.1 卷积滤镜引擎

// 通用卷积滤镜引擎
class ConvolutionFilter {
  static applyKernel(imageData, kernel) {
    const { width, height, data } = imageData;
    const output = new Uint8ClampedArray(data.length);
    const kernelSize = Math.sqrt(kernel.length);
    const half = Math.floor(kernelSize / 2);

    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        let r = 0, g = 0, b = 0;

        // 应用卷积核
        for (let ky = 0; ky < kernelSize; ky++) {
          for (let kx = 0; kx < kernelSize; kx++) {
            const px = Math.min(width - 1, Math.max(0, x + kx - half));
            const py = Math.min(height - 1, Math.max(0, y + ky - half));
            const pi = (py * width + px) * 4;
            const weight = kernel[ky * kernelSize + kx];

            r += data[pi] * weight;
            g += data[pi + 1] * weight;
            b += data[pi + 2] * weight;
          }
        }

        const i = (y * width + x) * 4;
        output[i] = Math.min(255, Math.max(0, r));
        output[i + 1] = Math.min(255, Math.max(0, g));
        output[i + 2] = Math.min(255, Math.max(0, b));
        output[i + 3] = data[i + 3];
      }
    }

    return new ImageData(output, width, height);
  }

  // 预定义滤镜
  static KERNELS = {
    // 边缘检测(Sobel算子)
    edgeDetect: [
      -1, -1, -1,
      -1,  8, -1,
      -1, -1, -1
    ],

    // 锐化
    sharpen: [
       0, -1,  0,
      -1,  5, -1,
       0, -1,  0
    ],

    // 浮雕
    emboss: [
      -2, -1, 0,
      -1,  1, 1,
       0,  1, 2
    ],

    // 高斯模糊(3x3)
    gaussianBlur: [
      1/16, 2/16, 1/16,
      2/16, 4/16, 2/16,
      1/16, 2/16, 1/16
    ],

    // 运动模糊
    motionBlur: [
      1/9, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 1/9, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 1/9, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 1/9, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 1/9, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 1/9, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 1/9, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 1/9, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 1/9
    ]
  };
}

// 使用示例
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

// 应用边缘检测
const edges = ConvolutionFilter.applyKernel(
  imageData,
  ConvolutionFilter.KERNELS.edgeDetect
);
ctx.putImageData(edges, 0, 0);

9.2 颜色空间转换

// 颜色空间转换工具
class ColorSpaceConverter {
  // RGB转HSV
  static rgbToHsv(r, g, b) {
    r /= 255;
    g /= 255;
    b /= 255;

    const max = Math.max(r, g, b);
    const min = Math.min(r, g, b);
    const delta = max - min;

    let h = 0;
    if (delta !== 0) {
      if (max === r) {
        h = ((g - b) / delta) % 6;
      } else if (max === g) {
        h = (b - r) / delta + 2;
      } else {
        h = (r - g) / delta + 4;
      }
      h *= 60;
      if (h < 0) h += 360;
    }

    const s = max === 0 ? 0 : delta / max;
    const v = max;

    return { h, s, v };
  }

  // HSV转RGB
  static hsvToRgb(h, s, v) {
    const c = v * s;
    const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
    const m = v - c;

    let r, g, b;
    if (h < 60) {
      [r, g, b] = [c, x, 0];
    } else if (h < 120) {
      [r, g, b] = [x, c, 0];
    } else if (h < 180) {
      [r, g, b] = [0, c, x];
    } else if (h < 240) {
      [r, g, b] = [0, x, c];
    } else if (h < 300) {
      [r, g, b] = [x, 0, c];
    } else {
      [r, g, b] = [c, 0, x];
    }

    return {
      r: Math.round((r + m) * 255),
      g: Math.round((g + m) * 255),
      b: Math.round((b + m) * 255)
    };
  }

  // 批量转换图像到HSV
  static imageToHSV(imageData) {
    const { width, height, data } = imageData;
    const hsvData = new Float32Array(width * height * 3);

    for (let i = 0; i < width * height; i++) {
      const offset = i * 4;
      const { h, s, v } = this.rgbToHsv(
        data[offset],
        data[offset + 1],
        data[offset + 2]
      );

      const hsvOffset = i * 3;
      hsvData[hsvOffset] = h;
      hsvData[hsvOffset + 1] = s;
      hsvData[hsvOffset + 2] = v;
    }

    return hsvData;
  }

  // 调整图像色调
  static adjustHue(imageData, hueDelta) {
    const { width, height, data } = imageData;
    const output = new Uint8ClampedArray(data.length);

    for (let i = 0; i < width * height; i++) {
      const offset = i * 4;
      const { h, s, v } = this.rgbToHsv(
        data[offset],
        data[offset + 1],
        data[offset + 2]
      );

      const newH = (h + hueDelta) % 360;
      const { r, g, b } = this.hsvToRgb(newH, s, v);

      output[offset] = r;
      output[offset + 1] = g;
      output[offset + 2] = b;
      output[offset + 3] = data[offset + 3];
    }

    return new ImageData(output, width, height);
  }
}

十、3D数学运算库

10.1 向量与矩阵运算

// 高性能3D数学库
class Vec3 {
  constructor(x = 0, y = 0, z = 0) {
    this.data = new Float32Array([x, y, z]);
  }

  get x() { return this.data[0]; }
  set x(v) { this.data[0] = v; }
  get y() { return this.data[1]; }
  set y(v) { this.data[1] = v; }
  get z() { return this.data[2]; }
  set z(v) { this.data[2] = v; }

  // 向量加法
  add(other) {
    return new Vec3(
      this.x + other.x,
      this.y + other.y,
      this.z + other.z
    );
  }

  // 向量减法
  sub(other) {
    return new Vec3(
      this.x - other.x,
      this.y - other.y,
      this.z - other.z
    );
  }

  // 标量乘法
  scale(scalar) {
    return new Vec3(
      this.x * scalar,
      this.y * scalar,
      this.z * scalar
    );
  }

  // 点积
  dot(other) {
    return this.x * other.x + this.y * other.y + this.z * other.z;
  }

  // 叉积
  cross(other) {
    return new Vec3(
      this.y * other.z - this.z * other.y,
      this.z * other.x - this.x * other.z,
      this.x * other.y - this.y * other.x
    );
  }

  // 长度
  length() {
    return Math.sqrt(this.dot(this));
  }

  // 归一化
  normalize() {
    const len = this.length();
    return len > 0 ? this.scale(1 / len) : new Vec3();
  }
}

// 4x4矩阵
class Mat4 {
  constructor() {
    this.data = new Float32Array(16);
    this.identity();
  }

  // 单位矩阵
  identity() {
    this.data.fill(0);
    this.data[0] = 1;
    this.data[5] = 1;
    this.data[10] = 1;
    this.data[15] = 1;
    return this;
  }

  // 矩阵乘法
  multiply(other) {
    const result = new Mat4();
    const a = this.data;
    const b = other.data;
    const out = result.data;

    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 4; j++) {
        let sum = 0;
        for (let k = 0; k < 4; k++) {
          sum += a[i * 4 + k] * b[k * 4 + j];
        }
        out[i * 4 + j] = sum;
      }
    }

    return result;
  }

  // 透视投影矩阵
  static perspective(fov, aspect, near, far) {
    const mat = new Mat4();
    const f = 1.0 / Math.tan(fov / 2);
    const nf = 1 / (near - far);

    mat.data[0] = f / aspect;
    mat.data[5] = f;
    mat.data[10] = (far + near) * nf;
    mat.data[11] = -1;
    mat.data[14] = 2 * far * near * nf;
    mat.data[15] = 0;

    return mat;
  }

  // 平移矩阵
  static translation(x, y, z) {
    const mat = new Mat4();
    mat.data[12] = x;
    mat.data[13] = y;
    mat.data[14] = z;
    return mat;
  }

  // 旋转矩阵(绕X轴)
  static rotationX(angle) {
    const mat = new Mat4();
    const c = Math.cos(angle);
    const s = Math.sin(angle);

    mat.data[5] = c;
    mat.data[6] = s;
    mat.data[9] = -s;
    mat.data[10] = c;

    return mat;
  }

  // 缩放矩阵
  static scaling(x, y, z) {
    const mat = new Mat4();
    mat.data[0] = x;
    mat.data[5] = y;
    mat.data[10] = z;
    return mat;
  }
}

// 使用示例:3D变换
const position = new Vec3(1, 2, 3);
const direction = new Vec3(0, 1, 0);
const normalized = direction.normalize();

const translation = Mat4.translation(5, 0, 0);
const rotation = Mat4.rotationX(Math.PI / 4);
const transform = translation.multiply(rotation);

console.log('变换矩阵:', transform.data);

十一、文件格式深度解析

11.1 JPEG文件结构解析

// JPEG文件解析器
class JPEGParser {
  /*
   * JPEG文件结构:
   * - SOI (Start of Image): 0xFFD8
   * - APP0 (JFIF Header): 0xFFE0
   * - SOF0 (Start of Frame): 0xFFC0
   * - DHT (Huffman Table): 0xFFC4
   * - SOS (Start of Scan): 0xFFDA
   * - EOI (End of Image): 0xFFD9
   */

  static async parse(file) {
    const arrayBuffer = await file.arrayBuffer();
    const data = new Uint8Array(arrayBuffer);
    const view = new DataView(arrayBuffer);

    // 验证JPEG签名
    if (view.getUint16(0, false) !== 0xFFD8) {
      throw new Error('Not a valid JPEG file');
    }

    const info = {
      width: 0,
      height: 0,
      components: 0,
      precision: 0,
      markers: []
    };

    let offset = 2;

    while (offset < data.length) {
      // 查找标记
      if (data[offset] !== 0xFF) {
        offset++;
        continue;
      }

      const marker = view.getUint16(offset, false);
      const length = view.getUint16(offset + 2, false);

      info.markers.push({
        marker: marker.toString(16),
        offset,
        length
      });

      // 解析SOF0(帧头)
      if (marker === 0xFFC0) {
        info.precision = data[offset + 4];
        info.height = view.getUint16(offset + 5, false);
        info.width = view.getUint16(offset + 7, false);
        info.components = data[offset + 9];
      }

      // 跳到下一个标记
      offset += 2 + length;

      // 遇到EOI结束
      if (marker === 0xFFD9) break;
    }

    return info;
  }

  // 提取EXIF信息
  static extractEXIF(arrayBuffer) {
    const view = new DataView(arrayBuffer);
    const data = new Uint8Array(arrayBuffer);

    // 查找APP1标记(0xFFE1,包含EXIF)
    let offset = 2;
    while (offset < data.length - 1) {
      if (view.getUint16(offset, false) === 0xFFE1) {
        const length = view.getUint16(offset + 2, false);

        // 验证EXIF标识
        const exifId = String.fromCharCode(...data.slice(offset + 4, offset + 10));
        if (exifId === 'Exif\0\0') {
          // 解析EXIF数据
          const exifStart = offset + 10;
          const byteOrder = view.getUint16(exifStart, false);
          const littleEndian = byteOrder === 0x4949; // "II"

          return {
            found: true,
            offset: exifStart,
            length: length - 8,
            littleEndian
          };
        }
      }
      offset += 2 + view.getUint16(offset + 2, false);
    }

    return { found: false };
  }
}

// 使用示例
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const info = await JPEGParser.parse(file);
  console.log('JPEG信息:', info);
  /*
  {
    width: 1920,
    height: 1080,
    components: 3,
    precision: 8,
    markers: [...]
  }
  */
});

11.2 WAV音频文件解析

// WAV音频文件解析器
class WAVParser {
  /*
   * WAV文件结构(RIFF格式):
   * [0-3]   "RIFF" 标识
   * [4-7]   文件大小-8
   * [8-11]  "WAVE" 标识
   * [12-15] "fmt " 子块标识
   * [16-19] 子块大小
   * [20-21] 音频格式(1=PCM)
   * [22-23] 声道数
   * [24-27] 采样率
   * [28-31] 字节率
   * [32-33] 块对齐
   * [34-35] 位深度
   * ...
   * "data" 子块
   */

  static parse(arrayBuffer) {
    const view = new DataView(arrayBuffer);

    // 验证RIFF标识
    const riff = String.fromCharCode(
      view.getUint8(0),
      view.getUint8(1),
      view.getUint8(2),
      view.getUint8(3)
    );

    if (riff !== 'RIFF') {
      throw new Error('Not a valid WAV file');
    }

    // 验证WAVE标识
    const wave = String.fromCharCode(
      view.getUint8(8),
      view.getUint8(9),
      view.getUint8(10),
      view.getUint8(11)
    );

    if (wave !== 'WAVE') {
      throw new Error('Not a valid WAV file');
    }

    // 解析fmt子块
    const audioFormat = view.getUint16(20, true);
    const numChannels = view.getUint16(22, true);
    const sampleRate = view.getUint32(24, true);
    const byteRate = view.getUint32(28, true);
    const blockAlign = view.getUint16(32, true);
    const bitsPerSample = view.getUint16(34, true);

    // 查找data子块
    let offset = 36;
    let dataSize = 0;
    let dataOffset = 0;

    while (offset < arrayBuffer.byteLength) {
      const chunkId = String.fromCharCode(
        view.getUint8(offset),
        view.getUint8(offset + 1),
        view.getUint8(offset + 2),
        view.getUint8(offset + 3)
      );

      const chunkSize = view.getUint32(offset + 4, true);

      if (chunkId === 'data') {
        dataSize = chunkSize;
        dataOffset = offset + 8;
        break;
      }

      offset += 8 + chunkSize;
    }

    return {
      format: audioFormat === 1 ? 'PCM' : 'Compressed',
      channels: numChannels,
      sampleRate,
      byteRate,
      blockAlign,
      bitsPerSample,
      dataSize,
      dataOffset,
      duration: dataSize / byteRate
    };
  }

  // 提取音频样本
  static extractSamples(arrayBuffer, info) {
    const { dataOffset, dataSize, bitsPerSample, channels } = info;
    const sampleCount = dataSize / (bitsPerSample / 8) / channels;

    if (bitsPerSample === 16) {
      const samples = new Int16Array(arrayBuffer, dataOffset, sampleCount * channels);
      return samples;
    } else if (bitsPerSample === 8) {
      const samples = new Uint8Array(arrayBuffer, dataOffset, sampleCount * channels);
      return samples;
    } else if (bitsPerSample === 32) {
      const samples = new Float32Array(arrayBuffer, dataOffset, sampleCount * channels);
      return samples;
    }

    throw new Error(`Unsupported bit depth: ${bitsPerSample}`);
  }
}

// 使用示例
async function loadWAVFile(url) {
  const response = await fetch(url);
  const arrayBuffer = await response.arrayBuffer();

  const info = WAVParser.parse(arrayBuffer);
  console.log('WAV信息:', info);

  const samples = WAVParser.extractSamples(arrayBuffer, info);
  console.log('音频样本数:', samples.length);

  return { info, samples };
}

十二、加密与压缩

12.1 简单加密算法(XOR)

// XOR加密/解密
class SimpleEncryption {
  static xorEncrypt(data, key) {
    const encrypted = new Uint8Array(data.length);
    const keyBytes = new TextEncoder().encode(key);

    for (let i = 0; i < data.length; i++) {
      encrypted[i] = data[i] ^ keyBytes[i % keyBytes.length];
    }

    return encrypted;
  }

  static xorDecrypt(encrypted, key) {
    // XOR加密是对称的,解密使用相同函数
    return this.xorEncrypt(encrypted, key);
  }
}

// 使用示例
const message = new TextEncoder().encode('Hello, World!');
const key = 'secret';

const encrypted = SimpleEncryption.xorEncrypt(message, key);
console.log('加密后:', encrypted);

const decrypted = SimpleEncryption.xorDecrypt(encrypted, key);
console.log('解密后:', new TextDecoder().decode(decrypted)); // "Hello, World!"

12.2 RLE压缩算法

// Run-Length Encoding压缩
class RLECompression {
  // 压缩
  static compress(data) {
    const compressed = [];
    let i = 0;

    while (i < data.length) {
      const value = data[i];
      let count = 1;

      // 计算连续相同值的数量
      while (i + count < data.length && data[i + count] === value && count < 255) {
        count++;
      }

      compressed.push(count, value);
      i += count;
    }

    return new Uint8Array(compressed);
  }

  // 解压缩
  static decompress(compressed) {
    const decompressed = [];

    for (let i = 0; i < compressed.length; i += 2) {
      const count = compressed[i];
      const value = compressed[i + 1];

      for (let j = 0; j < count; j++) {
        decompressed.push(value);
      }
    }

    return new Uint8Array(decompressed);
  }

  // 计算压缩率
  static compressionRatio(original, compressed) {
    return (compressed.length / original.length * 100).toFixed(2) + '%';
  }
}

// 使用示例
const original = new Uint8Array([1, 1, 1, 1, 2, 2, 3, 3, 3, 3, 3]);
const compressed = RLECompression.compress(original);
console.log('原始数据:', original);
console.log('压缩数据:', compressed); // [4, 1, 2, 2, 5, 3]
console.log('压缩率:', RLECompression.compressionRatio(original, compressed));

const decompressed = RLECompression.decompress(compressed);
console.log('解压数据:', decompressed); // [1, 1, 1, 1, 2, 2, 3, 3, 3, 3, 3]

十三、性能分析与调试工具

13.1 性能分析器

// 类型化数组性能分析工具
class TypedArrayProfiler {
  constructor() {
    this.measurements = [];
  }

  // 测量函数执行时间
  measure(name, fn, iterations = 1000) {
    const start = performance.now();

    for (let i = 0; i < iterations; i++) {
      fn();
    }

    const end = performance.now();
    const duration = end - start;
    const average = duration / iterations;

    const result = {
      name,
      totalTime: duration,
      averageTime: average,
      iterations,
      opsPerSecond: 1000 / average
    };

    this.measurements.push(result);
    return result;
  }

  // 对比测试
  compare(tests) {
    console.table(
      tests.map(test => this.measure(test.name, test.fn))
    );
  }

  // 内存使用分析
  analyzeMemory(createFn) {
    if (!performance.memory) {
      console.warn('performance.memory不可用');
      return null;
    }

    const before = performance.memory.usedJSHeapSize;
    const obj = createFn();
    const after = performance.memory.usedJSHeapSize;

    return {
      memoryUsed: after - before,
      memoryUsedMB: ((after - before) / 1024 / 1024).toFixed(2)
    };
  }

  // 生成报告
  generateReport() {
    console.log('=== 性能分析报告 ===');
    console.table(this.measurements);

    // 找出最快和最慢的操作
    const sorted = [...this.measurements].sort((a, b) => a.averageTime - b.averageTime);
    console.log('最快:', sorted[0].name, sorted[0].averageTime.toFixed(4), 'ms');
    console.log('最慢:', sorted[sorted.length - 1].name, sorted[sorted.length - 1].averageTime.toFixed(4), 'ms');
  }
}

// 使用示例
const profiler = new TypedArrayProfiler();

profiler.compare([
  {
    name: '普通数组创建',
    fn: () => {
      const arr = new Array(10000);
      for (let i = 0; i < 10000; i++) arr[i] = i;
    }
  },
  {
    name: 'Float32Array创建',
    fn: () => {
      const arr = new Float32Array(10000);
      for (let i = 0; i < 10000; i++) arr[i] = i;
    }
  },
  {
    name: 'Float32Array.from',
    fn: () => {
      Float32Array.from({ length: 10000 }, (_, i) => i);
    }
  }
]);

profiler.generateReport();

13.2 内存泄漏检测器

// 内存泄漏检测器
class MemoryLeakDetector {
  constructor() {
    this.snapshots = [];
  }

  // 创建内存快照
  takeSnapshot(label) {
    if (!performance.memory) {
      console.warn('performance.memory不可用');
      return;
    }

    this.snapshots.push({
      label,
      timestamp: Date.now(),
      heapSize: performance.memory.usedJSHeapSize,
      totalHeapSize: performance.memory.totalJSHeapSize
    });
  }

  // 分析内存增长
  analyze() {
    if (this.snapshots.length < 2) {
      console.warn('需要至少2个快照进行分析');
      return;
    }

    console.log('=== 内存分析 ===');
    for (let i = 1; i < this.snapshots.length; i++) {
      const prev = this.snapshots[i - 1];
      const curr = this.snapshots[i];
      const growth = curr.heapSize - prev.heapSize;
      const growthMB = (growth / 1024 / 1024).toFixed(2);

      console.log(`${prev.label}${curr.label}:`);
      console.log(`  内存增长: ${growthMB} MB`);
      console.log(`  当前堆大小: ${(curr.heapSize / 1024 / 1024).toFixed(2)} MB`);
    }
  }

  // 检测可能的泄漏
  detectLeaks(threshold = 10) {
    const leaks = [];

    for (let i = 1; i < this.snapshots.length; i++) {
      const prev = this.snapshots[i - 1];
      const curr = this.snapshots[i];
      const growthMB = (curr.heapSize - prev.heapSize) / 1024 / 1024;

      if (growthMB > threshold) {
        leaks.push({
          from: prev.label,
          to: curr.label,
          growthMB: growthMB.toFixed(2)
        });
      }
    }

    if (leaks.length > 0) {
      console.warn('⚠️ 检测到可能的内存泄漏:');
      console.table(leaks);
    } else {
      console.log('✅ 未检测到明显的内存泄漏');
    }

    return leaks;
  }
}

// 使用示例
const detector = new MemoryLeakDetector();

detector.takeSnapshot('初始状态');

// 执行一些操作
const arrays = [];
for (let i = 0; i < 100; i++) {
  arrays.push(new Float32Array(100000));
}

detector.takeSnapshot('创建100个数组后');

// 清理
arrays.length = 0;
if (global.gc) global.gc(); // 需要--expose-gc标志

detector.takeSnapshot('清理后');

detector.analyze();
detector.detectLeaks();

13.3 调试辅助工具

// 类型化数组调试工具
class TypedArrayDebugger {
  // 十六进制转储
  static hexDump(typedArray, bytesPerLine = 16) {
    const bytes = new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength);
    let output = '';

    for (let i = 0; i < bytes.length; i += bytesPerLine) {
      // 地址
      output += i.toString(16).padStart(8, '0') + '  ';

      // 十六进制
      for (let j = 0; j < bytesPerLine; j++) {
        if (i + j < bytes.length) {
          output += bytes[i + j].toString(16).padStart(2, '0') + ' ';
        } else {
          output += '   ';
        }

        if (j === 7) output += ' ';
      }

      output += ' |';

      // ASCII
      for (let j = 0; j < bytesPerLine && i + j < bytes.length; j++) {
        const byte = bytes[i + j];
        output += (byte >= 32 && byte < 127) ? String.fromCharCode(byte) : '.';
      }

      output += '|\n';
    }

    return output;
  }

  // 比较两个类型化数组
  static compare(arr1, arr2) {
    if (arr1.length !== arr2.length) {
      return {
        equal: false,
        reason: `长度不同: ${arr1.length} vs ${arr2.length}`
      };
    }

    const differences = [];
    for (let i = 0; i < arr1.length; i++) {
      if (arr1[i] !== arr2[i]) {
        differences.push({
          index: i,
          value1: arr1[i],
          value2: arr2[i]
        });

        if (differences.length >= 10) {
          differences.push({ note: '... 还有更多差异 ...' });
          break;
        }
      }
    }

    if (differences.length === 0) {
      return { equal: true };
    } else {
      return {
        equal: false,
        differenceCount: differences.length,
        differences
      };
    }
  }

  // 统计信息
  static stats(typedArray) {
    let min = typedArray[0];
    let max = typedArray[0];
    let sum = 0;

    for (let i = 0; i < typedArray.length; i++) {
      const val = typedArray[i];
      if (val < min) min = val;
      if (val > max) max = val;
      sum += val;
    }

    const mean = sum / typedArray.length;

    // 计算标准差
    let variance = 0;
    for (let i = 0; i < typedArray.length; i++) {
      variance += Math.pow(typedArray[i] - mean, 2);
    }
    const stdDev = Math.sqrt(variance / typedArray.length);

    return {
      length: typedArray.length,
      min,
      max,
      sum,
      mean,
      stdDev,
      byteLength: typedArray.byteLength,
      type: typedArray.constructor.name
    };
  }
}

// 使用示例
const data = new Uint8Array([0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64]);
console.log(TypedArrayDebugger.hexDump(data));
/*
00000000  48 65 6c 6c 6f 20 57 6f  72 6c 64                 |Hello World|
*/

const arr1 = new Int32Array([1, 2, 3, 4, 5]);
const arr2 = new Int32Array([1, 2, 9, 4, 5]);
console.log(TypedArrayDebugger.compare(arr1, arr2));

const stats = TypedArrayDebugger.stats(new Float32Array([1.5, 2.3, 5.7, 8.1, 3.2]));
console.log('统计信息:', stats);

十四、浏览器兼容性与Polyfill

14.1 特性检测

// 类型化数组特性检测
class TypedArraySupport {
  static detect() {
    return {
      typedArrays: typeof Uint8Array !== 'undefined',
      arrayBuffer: typeof ArrayBuffer !== 'undefined',
      dataView: typeof DataView !== 'undefined',
      sharedArrayBuffer: typeof SharedArrayBuffer !== 'undefined',
      atomics: typeof Atomics !== 'undefined',
      bigInt64Array: typeof BigInt64Array !== 'undefined',
      textEncoder: typeof TextEncoder !== 'undefined',
      textDecoder: typeof TextDecoder !== 'undefined'
    };
  }

  static checkSupport() {
    const support = this.detect();
    const unsupported = Object.entries(support)
      .filter(([_, supported]) => !supported)
      .map(([feature]) => feature);

    if (unsupported.length > 0) {
      console.warn('以下特性不支持:', unsupported);
      return false;
    }

    console.log('✅ 所有类型化数组特性都支持');
    return true;
  }
}

// 使用
TypedArraySupport.checkSupport();

14.2 性能最佳实践总结

// 性能最佳实践检查清单
class BestPracticesChecker {
  static check(code) {
    const warnings = [];

    // 检查1: 避免频繁分配
    if (code.includes('new Float32Array') && code.includes('for')) {
      warnings.push({
        type: '性能警告',
        message: '检测到循环中创建类型化数组,考虑重用'
      });
    }

    // 检查2: 优先使用subarray
    if (code.includes('.slice(')) {
      warnings.push({
        type: '性能提示',
        message: '使用subarray代替slice可以避免数据复制'
      });
    }

    // 检查3: 批量操作
    if (code.match(/\[\d+\]\s*=/g) && code.match(/\[\d+\]\s*=/g).length > 5) {
      warnings.push({
        type: '性能提示',
        message: '考虑使用set()方法进行批量赋值'
      });
    }

    return warnings;
  }
}

// 使用示例
const codeSnippet = `
for (let i = 0; i < 1000; i++) {
  const temp = new Float32Array(100);
  temp[0] = i;
  temp[1] = i * 2;
  temp[2] = i * 3;
}
`;

const warnings = BestPracticesChecker.check(codeSnippet);
console.log('代码检查结果:', warnings);

十五、总结与展望

类型化数组作为JavaScript处理二进制数据的核心技术,在现代Web应用中扮演着越来越重要的角色。从基础的ArrayBuffer到高级的SharedArrayBuffer多线程编程,从简单的数据存储到复杂的图像处理算法,类型化数组为JavaScript带来了接近原生的性能。

关键技术要点

  1. 基础架构 - ArrayBuffer + TypedArray/DataView的分层设计
  2. 性能优势 - 2-5倍性能提升,精确的内存控制
  3. 多线程 - SharedArrayBuffer + Atomics实现真正的并行计算
  4. 实战应用 - WebGL、音视频、文件处理、网络协议
  5. 最佳实践 - 重用内存、零拷贝、批量操作

未来发展方向

  • WebGPU集成 - 更强大的GPU计算能力
  • WASM深度融合 - 零成本的JavaScript-WASM互操作
  • 更多原子操作 - 增强的并发原语
  • SIMD支持 - 显式的SIMD指令集
  • 更好的调试工具 - 浏览器DevTools增强

掌握类型化数组,不仅是性能优化的需要,更是构建现代高性能Web应用的基石。

昨天 — 2025年12月24日掘金 前端

请求 ID 跟踪模式:解决异步请求竞态条件

作者 鲫小鱼
2025年12月24日 18:36

📋 目录


问题背景

在搜索场景中,用户快速输入关键词时会触发多个并发请求:

// 用户快速输入:a → ac → acd
// 会触发 3 个请求,但返回顺序可能不同

问题表现:

  • 推荐商品列表有时会多出 5 个商品
  • 显示的商品与当前关键词不匹配
  • 旧请求的结果覆盖了新请求的结果

问题分析

竞态条件(Race Condition)

当多个异步请求并发执行时,由于网络延迟不同,返回顺序可能与发起顺序不一致:

时间线:
T1: 用户输入 "a"  → 触发请求1
T2: 用户输入 "ac" → 触发请求2
T3: 请求2返回     → 设置 productList = ["ac相关商品"]
T4: 请求1返回     → 设置 productList = ["a相关商品"] ❌ 错误!

根本原因:

  • React 的 setState 是异步的
  • 多个请求同时进行,无法保证哪个先返回
  • 旧请求的结果可能覆盖新请求的结果

解决方案

请求 ID 跟踪机制

使用一个全局递增的请求 ID 来跟踪每个请求,确保只处理最新请求的结果。

核心思路

  1. 每个请求分配唯一 ID:使用 useRef 保存一个递增的计数器
  2. 请求开始时保存 ID:在闭包中保存当前请求的 ID
  3. 请求返回时验证:比较保存的 ID 和最新的 ID,判断请求是否仍然有效

实现原理

1. 添加请求 ID 跟踪器

// 用于跟踪当前请求的 ID,确保只处理最新请求的结果
const requestIdRef = useRef<number>(0)

为什么使用 useRef

  • useRef 的值在组件重新渲染时保持不变
  • .current 属性是可变的,可以随时更新
  • 不会触发组件重新渲染

2. 请求开始时生成并保存 ID

useEffect(() => {
  const fetchProductList = async () => {
    // 生成新的请求 ID(先递增再取值)
    const currentRequestId = ++requestIdRef.current

    // 保存当前请求的关键词(双重验证)
    const currentKeyword = keyWord.trim()

    // ... 发起请求
  }

  fetchProductList()
}, [keyWord])

关键点:

  • ++requestIdRef.current 先递增再取值
  • currentRequestId 被闭包捕获,保存请求开始时的值
  • currentKeyword 也被闭包捕获,用于双重验证

3. 请求返回时验证 ID

const response = await getPublicSearchFilter(params)

// 检查是否是最新的请求
if (currentRequestId !== requestIdRef.current) {
  return  // 忽略旧请求的结果
}

// 双重验证:检查关键词是否仍然匹配
if (currentKeyword !== keyWord.trim()) {
  return  // 关键词已改变,忽略结果
}

// 只有通过所有检查才设置 state
setProductList([...proList])

完整代码示例

import { useState, useEffect, useRef } from 'react'

const SearchList = () => {
  const [productList, setProductList] = useState<any[]>([])
  const [productLoading, setProductLoading] = useState<boolean>(false)

  // 用于跟踪当前请求的 ID
  const requestIdRef = useRef<number>(0)

  // 获取推荐商品列表
  useEffect(() => {
    const fetchProductList = async () => {
      // 如果没有搜索关键字,不请求
      if (!keyWord || !keyWord.trim()) {
        setProductList([])
        return
      }

      // 生成新的请求 ID
      const currentRequestId = ++requestIdRef.current
      // 保存当前请求的关键词,用于验证结果是否仍然有效
      const currentKeyword = keyWord.trim()

      setProductLoading(true)
      try {
        const params: Search.SearchParams = {
          keyword: currentKeyword,
          size: 5,
          page: 1,
        }

 
        const response = await getPublicSearchFilter(params)

        // ✅ 检查1:是否是最新的请求
        // 如果不是则忽略结果,避免旧的请求结果覆盖新的结果
        if (currentRequestId !== requestIdRef.current) {
          return
        }

        // ✅ 检查2:关键词是否仍然匹配(双重保险)
        if (currentKeyword !== keyWord.trim()) {
          return
        }

        const items = response?.itemList?.data || []
        const proList = items.slice(0, 5)

        setProductList([...proList])
      } catch (error) {
        // 检查是否是最新的请求,如果不是则忽略错误
        if (currentRequestId !== requestIdRef.current) {
          return
        }
        console.error('获取推荐商品失败:', error)
        setProductList([])
      } finally {
        // 只有在是最新请求时才更新 loading 状态
        if (currentRequestId === requestIdRef.current) {
          setProductLoading(false)
        }
      }
    }

    fetchProductList()
  }, [keyWord])

  // ... 其他代码
}

闭包与 Ref 深入理解

关键概念

1. currentRequestId 被闭包捕获

const fetchProductList = async () => {
  // 这一行执行时,currentRequestId 被"冻结"在闭包中
  const currentRequestId = ++requestIdRef.current  // 假设此时 = 1

  // ... 发起异步请求 ...
  await getPublicSearchFilter(params)  // 这里等待,可能需要几秒钟

  // 当请求返回时,currentRequestId 仍然是 1(闭包保存的值)
  // 但 requestIdRef.current 可能已经是 2、3、4...(最新值)
  if (currentRequestId !== requestIdRef.current) {
    return
  }
}

要点:

  • currentRequestId 是局部常量,在函数执行时被赋值
  • 异步函数返回时,它仍然保持请求开始时的值
  • 这就是闭包:函数"记住"了创建时的变量值

2. requestIdRef.current 始终是最新的

// requestIdRef 是一个 ref 对象
const requestIdRef = useRef<number>(0)

// ref.current 是一个可变引用,每次读取都返回最新值
requestIdRef.current  // 读取时总是最新值

要点:

  • requestIdRef 是 React 的 ref 对象,.current 是可变的
  • 每次读取 requestIdRef.current 都会得到当前最新值
  • 不受闭包影响,因为它不是被捕获的变量,而是通过引用访问

时间线示例

// === 初始状态 ===
requestIdRef.current = 0

// === T1: 用户输入 "a",触发请求1 ===
const fetchProductList1 = async () => {
  const currentRequestId = ++requestIdRef.current
  // 执行后:currentRequestId = 1, requestIdRef.current = 1

  // 闭包捕获:currentRequestId = 1(被"冻结")
  // ref 引用:requestIdRef.current(随时可读取最新值)

  await getPublicSearchFilter(...)  // 等待响应...
}

// === T2: 用户输入 "ac",触发请求2(请求1还在等待中)===
const fetchProductList2 = async () => {
  const currentRequestId = ++requestIdRef.current
  // 执行后:currentRequestId = 2, requestIdRef.current = 2

  await getPublicSearchFilter(...)  // 等待响应...
}

// === T3: 请求1返回(此时 requestIdRef.current 已经是 2)===
// 在 fetchProductList1 的闭包中:
if (currentRequestId !== requestIdRef.current) {
  // currentRequestId = 1(闭包保存的旧值)
  // requestIdRef.current = 2(读取的最新值)
  // 1 !== 2 ✅ 返回,忽略结果
  return
}

// === T4: 请求2返回 ===
// 在 fetchProductList2 的闭包中:
if (currentRequestId !== requestIdRef.current) {
  // currentRequestId = 2(闭包保存的值)
  // requestIdRef.current = 2(如果此时没有新请求)
  // 2 === 2 ✅ 通过检查,设置 state
}

内存中的状态

// 内存布局示意:

// 全局 ref(所有函数共享)
requestIdRef = {
  current: 3  // ← 始终是最新值,随时可读取
}

// 请求1的闭包(已废弃)
fetchProductList1 闭包环境:
  currentRequestId: 1  // ← 被"冻结",不会改变

// 请求2的闭包(已废弃)
fetchProductList2 闭包环境:
  currentRequestId: 2  // ← 被"冻结",不会改变

// 请求3的闭包(当前有效)
fetchProductList3 闭包环境:
  currentRequestId: 3  // ← 被"冻结",不会改变

对比:闭包 vs Ref

特性 currentRequestId (闭包) requestIdRef.current (Ref)
值的变化 创建时赋值后不再改变 每次读取都是最新值
作用域 函数闭包内 全局可访问
用途 保存请求开始时的 ID 保存最新的请求 ID
类比 拍照(定格瞬间) 实时监控(动态更新)

为什么这样设计有效?

// 关键代码
const currentRequestId = ++requestIdRef.current  // 闭包捕获:保存"快照"
// ... 异步操作 ...
if (currentRequestId !== requestIdRef.current) {  // 比较"快照"和"实时值"
  return  // 如果不同,说明已有新请求
}

工作原理:

  1. 请求开始时currentRequestId 保存当前 ID(快照)
  2. 请求进行中requestIdRef.current 可能被新请求更新
  3. 请求返回时:比较快照和最新值
    • 相同 → 仍是最新请求,处理结果
    • 不同 → 已被新请求取代,忽略结果

最佳实践

1. 何时使用请求 ID 跟踪?

适用场景:

  • 用户输入触发的搜索请求
  • 下拉选择触发的数据加载
  • 任何可能快速连续触发的异步操作

不适用场景:

  • 一次性请求(如页面初始化)
  • 按钮点击触发的请求(用户不会快速点击)
  • 定时轮询请求(通常需要取消机制)

2. 双重验证的必要性

// 检查1:请求 ID(主要检查)
if (currentRequestId !== requestIdRef.current) {
  return
}

// 检查2:关键词匹配(双重保险)
if (currentKeyword !== keyWord.trim()) {
  return
}

为什么需要双重验证?

  • 请求 ID 检查:防止旧请求覆盖新请求
  • 关键词检查:防止边界情况(如请求 ID 相同但关键词已改变)

3. 错误处理

catch (error) {
  // 检查是否是最新的请求,如果不是则忽略错误
  if (currentRequestId !== requestIdRef.current) {
    return
  }
  console.error('获取推荐商品失败:', error)
  setProductList([])
}

要点:

  • 错误处理也要检查请求 ID
  • 避免旧请求的错误影响新请求的状态

4. Loading 状态管理

finally {
  // 只有在是最新请求时才更新 loading 状态
  if (currentRequestId === requestIdRef.current) {
    setProductLoading(false)
  }
}

要点:

  • Loading 状态也要检查请求 ID
  • 避免旧请求的 loading 状态影响 UI

其他解决方案对比

方案1:AbortController(推荐用于可取消的请求)

const abortControllerRef = useRef<AbortController | null>(null)

useEffect(() => {
  // 取消之前的请求
  if (abortControllerRef.current) {
    abortControllerRef.current.abort()
  }

  const abortController = new AbortController()
  abortControllerRef.current = abortController

  fetch(url, { signal: abortController.signal })
    .then(response => {
      if (abortController.signal.aborted) return
      // 处理响应
    })
}, [deps])

优点:

  • 可以真正取消网络请求
  • 节省带宽和服务器资源

缺点:

  • 需要 API 支持 AbortController
  • 某些旧的 API 可能不支持

方案2:请求 ID 跟踪(本文方案)

优点:

  • 适用于任何异步操作
  • 不依赖 API 支持
  • 实现简单

缺点:

  • 不能真正取消网络请求
  • 请求仍会占用带宽

方案3:防抖(Debounce)

const debouncedSearch = useMemo(
  () => debounce((keyword: string) => {
    fetchProductList(keyword)
  }, 300),
  []
)

优点:

  • 减少请求次数
  • 简单易用

缺点:

  • 延迟响应
  • 用户可能等待更长时间

总结

核心要点

  1. 问题根源:多个异步请求并发执行,返回顺序不确定
  2. 解决方案:使用请求 ID 跟踪,确保只处理最新请求
  3. 关键机制:闭包保存"快照",Ref 提供"实时值"
  4. 验证策略:双重验证(请求 ID + 业务参数)

适用场景

✅ 搜索输入框的联想词/推荐商品 ✅ 下拉选择的数据加载 ✅ 快速连续触发的异步操作

关键代码模式

// 1. 创建跟踪器
const requestIdRef = useRef<number>(0)

// 2. 请求开始时保存 ID
const currentRequestId = ++requestIdRef.current

// 3. 请求返回时验证
if (currentRequestId !== requestIdRef.current) {
  return  // 忽略旧请求
}

记忆口诀

"闭包保存快照,Ref 提供实时值,比较两者判断有效性"

最后感谢阅读!欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!!

Node.js的package.json

2025年12月24日 17:48

package.jsonNode.js 和前端项目的核心配置文件,它是一个 JSON 格式 的文件,用来描述项目的元数据、依赖、脚本等信息。

下面我给你一个 完整示例详细解析,方便你快速掌握。


1. 基本作用

  • 项目描述(名称、版本、作者等)
  • 依赖管理(生产依赖、开发依赖)
  • 脚本命令npm run xxx
  • 工具配置(如 ESLint、Babel、TypeScript 等)

2. 示例 package.json

Json
{
  "name": "my-node-app",                // 项目名称(必须小写、无空格)
  "version": "1.0.0",                   // 版本号(遵循 semver 语义化版本)
  "description": "A sample Node.js app",// 项目描述
  "main": "index.js",                   // 入口文件
  "type": "module",                     // 模块类型: "commonjs""module" (ESM)
  "scripts": {                          // npm 脚本命令
    "start": "node index.js",
    "dev": "nodemon index.js",
    "test": "node test.js"
  },
  "keywords": ["node", "example"],      // 关键词(方便 npm 搜索)
  "author": "Your Name",                // 作者
  "license": "MIT",                     // 许可证
  "dependencies": {                     // 生产依赖
    "express": "^4.18.2"
  },
  "devDependencies": {                  // 开发依赖
    "nodemon": "^3.0.1"
  },
  "engines": {                           // Node 版本要求
    "node": ">=18.0.0"
  }
}

3. 常用字段说明

字段 作用
name 包名(npm 发布时使用)
version 版本号(语义化版本:主.次.补丁)
description 项目描述
main 入口文件(require() 默认加载)
type 模块类型(commonjsmodule
scripts 自定义命令(npm run xxx
dependencies 生产环境依赖
devDependencies 开发环境依赖
peerDependencies 对等依赖(插件/库常用)
engines 指定 Node/npm 版本
license 开源协议

4. 常用命令

Bash
# 初始化 package.json
npm init -y

# 安装生产依赖
npm install express

# 安装开发依赖
npm install nodemon --save-dev

# 运行脚本
npm run start
npm run dev

5. 版本号规则(SemVer)

  • ^1.2.3:允许 次版本补丁版本 更新(1.x.x
  • ~1.2.3:允许 补丁版本 更新(1.2.x
  • 1.2.3:固定版本
  • *:任意版本(不推荐)

从零构建一个待办事项应用:一次关于组件化与状态管理的深度思考

作者 烟袅破辰
2025年12月24日 17:09
我们今天要做的,是一个再普通不过的小项目——待办事项(Todo List)应用。 它看起来简单:输入任务、添加、勾选完成、删除、清空已完成项。但正是这种“简单”,恰恰是理解 React 核心思想的最佳

Tauri (22)——让 `Esc` 快捷键一层层退出的分层关闭方案

2025年12月23日 10:15

背景:为什么 Esc 会变得“不可控”

在 Coco 里,Esc 同时承担了很多“退出/关闭”的职责:

  • 退出输入(Input/Textarea)编辑态
  • 关闭弹层(Popover / 菜单)
  • 关闭历史面板(History Panel)
  • 最后隐藏窗口(Tauri hideWindow

问题在于:这些层级的 UI 往往同时存在。如果不做事件隔离,Esc 可能 “一键关闭所有”,或者被某一层吞掉导致 “该关的不关”。

本次改动的核心,就是把 Esc 做成可预期的优先级链路,并通过 stopPropagation + DOM 状态判定让各层各司其职。


设计目标:Esc 的层级优先级(从近到远)

我们把 Esc 定义为 “从用户当前操作点开始逐层退出”,优先级如下:

  1. 如果正在输入:先 blur(退出输入态),不做其它关闭
  2. 如果在 Popover 里:关闭当前 Popover(但第一下 Esc 若在输入框里,仍先 blur)
  3. 如果上下文菜单打开:关闭上下文菜单
  4. 如果 History Panel 打开:关闭 History Panel
  5. 如果 Extension View 打开:不隐藏窗口(交给 Extension 自己处理或保持现状)
  6. 否则:隐藏窗口(hideWindow()

这条链路的关键点是:“内层 UI 必须能拦截并消费 Esc,避免冒泡到全局导致直接 hideWindow”


全局 Esc:统一入口与“最后兜底”

全局 EscLayoutOutlet 初始化(src/routes/outlet.tsx 调用 useEscape()),核心逻辑在 src/hooks/useEscape.ts

  • 先做 event.preventDefault() + event.stopPropagation()src/hooks/useEscape.ts
  • 再按优先级处理:
    • 输入 blur(src/hooks/useEscape.ts
    • 关闭右键菜单(src/hooks/useEscape.ts
    • 关闭 History Panel(src/hooks/useEscape.ts
    • Extension 打开时直接 return,避免误隐藏(src/hooks/useEscape.ts
    • 最后隐藏窗口(src/hooks/useEscape.ts

这里有个重要的小优化:回调里用 useSearchStore.getState() 取最新状态(src/hooks/useEscape.ts),避免快捷键回调捕获旧状态导致 “按了没反应”。


Popover 的 Esc:把“关闭弹层”从全局剥离出来

真正决定 “Esc 是否一层层退出” 的关键,是 Popover 对 Esc 的消费。

在 Radix Popover 中,PopoverContent 提供了 onEscapeKeyDown。本次在组件封装层统一加入:

  • 文件:src/components/ui/popover.tsx
  • 逻辑:src/components/ui/popover.tsx

行为是:

  1. stopPropagation + preventDefault(避免 Esc 冒泡到全局 useEscape,也避免浏览器默认行为)
  2. 如果焦点在输入框/文本域:只 blur(src/components/ui/popover.tsx
    • 这保证了“第一下 Esc 退出输入”,而不是直接关掉 popover
  3. 否则:找到当前打开的 popover trigger 并 click(),从而走 Radix 的正常关闭流程(src/components/ui/popover.tsx

为了找到 “当前打开的 trigger”,新增了选择器常量:

  • OPENED_POPOVER_TRIGGER_SELECTORsrc/constants/index.ts

为什么要改选择器:从“自定义标记”到 Radix 的真实 DOM

这次对 Popover 的识别方式也做了统一调整:

  • POPOVER_PANEL_SELECTOR 改为 Radix wrapper:src/constants/index.ts
    • 变成 "[data-radix-popper-content-wrapper]",用于更可靠地判断“现在是否存在 popover”
  • HISTORY_PANEL_ID / CONTEXT_MENU_PANEL_IDheadlessui-... 命名迁移到 popover-panel:...src/constants/index.ts
    • 配合当前实际渲染结构,避免 ID 不一致导致关闭逻辑失效

对应的 History 关闭动作走的是点击 trigger:

  • closeHistoryPanel()src/utils/index.ts
  • 它通过 [aria-controls="${HISTORY_PANEL_ID}"] 找到按钮并点击(src/utils/index.ts
  • useEscapedocument.getElementById(HISTORY_PANEL_ID) 判断历史面板是否存在(src/hooks/useEscape.ts

VisibleKey 与 “在 Popover 里显示快捷键提示”

VisibleKey 需要知道 “当前快捷键提示是否应该显示”,尤其是在 popover 打开时,只在 popover 内的元素上显示。

它通过:

  • POPOVER_PANEL_SELECTOR 获取 popover 面板 wrapper(src/components/Common/VisibleKey.tsx
  • OPENED_POPOVER_TRIGGER_SELECTOR 获取打开的 trigger(src/components/Common/VisibleKey.tsx
  • 判断当前组件是否在 panel 或 trigger 内(src/components/Common/VisibleKey.tsx

这样一来,“打开 popover 后,快捷键提示只在当前 popover 的交互区域出现”,不会污染外层 UI。


输入框组件收敛:删除 PopoverInput,统一用 shadcn Input

src/components/Common/PopoverInput.tsx 被删除,相关位置改为直接使用 src/components/ui/input

  • SearchPopoversrc/components/Search/SearchPopover.tsx
  • MCPPopoversrc/components/Search/MCPPopover.tsx
  • AssistantListsrc/components/Assistant/AssistantList.tsx

同时统一加了 autoCorrect="off",减少输入联想带来的干扰(例如英文关键字/ID 搜索场景)。


小结

本次改动将 Esc 从“不可控的一键退出”重构为有明确优先级的逐层退出机制

输入态 → Popover → 菜单 / History → Extension → 窗口隐藏

核心做法是:

  • 全局 useEscape 只作为最后兜底,按优先级处理关闭逻辑
  • Popover 内部消费 Esc(优先 blur 输入,其次关闭弹层),防止事件冒泡到全局
  • 基于 Radix 实际 DOM 统一判断 popover / history 是否打开
  • VisibleKey 只在当前 popover 交互区域内显示
  • 统一输入组件,减少干扰

结果是:Esc 行为稳定、可预期,不再误关、不再漏关。

开源

如果你对我们的技术细节感兴趣,或者想体验一下这款全能的生产力工具,欢迎访问我们的开源仓库和官网:

不只是作品集:用 Next.js 打造我的数字作品库

2025年12月23日 09:31

前言

Hi,大家好,我是白雾茫茫丶!

很久没和大家见面了,说来惭愧,自从AI成了“全能助手”,我这个“码字工”的笔就有点锈了——感觉很多技术问题,还没等我写完文章,AI三言两语就解释清楚了。少了点输出深度内容的动力,手也就慢慢懒了。

不过最近,我又找到了一点不一样的感觉。工作上接触不到,我就自己动手——这段时间一直在折腾 Next.js + Shadcn UI 这套组合。不得不说,确实很火,用起来也很顺手,从头搭一个项目的过程,反而让我找回了那种边踩坑边学习的踏实感。

今天想和大家分享的,是我在这个过程中做出来的一个小成果:一个个人作品信息展示的模板。我觉得它设计得挺干净,结构也清晰,不管是拿来即用,还是当作学习参考,应该都还不错。如果你也在找类似的灵感或模版,不妨看看,希望能帮到你。

灵感来源

在我探索 Shadcn UI 的过程中,偶然发现了一个设计非常出色的模板:

magicui.design/docs/templa…

最初我只是把它集成在自己正在捣鼓的项目里试试水,但很快发现,这个页面本身就足够完整和优雅——即使独立出来,也完全能作为一个专业的作品展示站点。

于是,我决定把它单独抽离出来,搭建成了一个专注的作品展示项目。在保留其原设计精髓的基础上,我根据自己的偏好做了一些调整,加入了一些个性化的交互细节和动画效果,让整个界面在简洁之中多了一点灵动的气息。

最终呈现出来的,就是现在这个版本——整体保持干净利落,又不失细节处的巧思。如果你也在寻找一个轻量、现代且易于定制的作品集模板,或许这个实现能给你带来一些灵感。

为什么每个开发者都需要一个作品站点?

作为开作为开发者,我们每天都在创造价值:在 GitHub 提交代码、在掘金写技术文章、在开源社区贡献方案……

但这些成果往往散落在不同的平台,像一座座信息孤岛。面试时,我们需要反复解释这些分散的内容;求职时,简历上的短短几行描述,难以承载我们真实的技术思考与项目深度。

构建一个统一的技术身份,将分散的项目、文章、数据可视化整合在一个专业、可访问的空间。它不仅仅是一个作品集,更是:

面试的隐形加分项——当面试官通过一个精心设计的站点看到你的完整技术路径、真实的项目思考过程,这种体验远胜过千篇一律的简历

技术能力的系统证明——可视化你的 GitHub 贡献、技能雷达图、项目迭代历史,让抽象的能力变得具体可见

技术栈

- 框架:Next.js 16、React 19、TypeScript 5
- 样式:Tailwind CSS v4、tw-animate-css
- 可视化:Recharts
- 其他:ahooks、enum-plus、lucide-react

特性

- 基于 `Next.js App Router` 的现代架构
- 使用 `Tailwind CSS v4` 与自定义主题变量,支持暗色模式
- GitHub 仓库与贡献统计 API
- Halo 文章列表聚合 API
- Recharts 数据图表可视化
- 完整 SEO 文件:`robots``sitemap``manifest`
- 集成 Umami、Microsoft Clarity、Google Analytics(生产环境自动启用)

环境变量

在项目根目录创建 .env,示例:

# 站点信息
NEXT_PUBLIC_NAME="你的名字"
NEXT_PUBLIC_APP_NAME="Portfolio"
NEXT_PUBLIC_DESC="一句话简介/站点描述"
NEXT_PUBLIC_APP_DOMAIN="https://your-domain.com"
NEXT_PUBLIC_THEME="light" # 可选:light | dark | system

# 分析统计(生产环境生效)
NEXT_PUBLIC_UMAMI_ID=""
NEXT_PUBLIC_CLARITY_ID=""
NEXT_PUBLIC_GA_ID=""

# GitHub API
GITHUB_TOKEN="" # 只读 Token
NEXT_PUBLIC_GITHUB_USERNAME="your-github-username"

# Halo API
HALO_TOKEN="" # 只读 Token

效果预览

PixPin_2025-12-23_09-27-56.png

总结

这个基于 Next.js + Shadcn UI 构建的个人作品展示模板,是我在探索现代前端技术栈过程中的一次实践与沉淀。

技术本身是工具,但如何用它更好地表达自己、呈现价值,才是更有意义的探索。

如果你也在构建个人项目、整理作品集,或单纯想学习 Next.js 全栈实践,这个项目或许能给你一些参考。

在线预览:portfolio.baiwumm.com

Github:github.com/baiwumm/por…

草稿

作者 三只萌新
2025年12月24日 17:27
使用 基本界面 需求文档 设计文档 任务拆解 执行任务 最终产物包含文档记录和代码以及静态资源文件 效果对比 kiro 完成效果 对比 trae 参照官方文档资料使用 spec kit 完成效果。 代
❌
❌