普通视图

发现新文章,点击刷新页面。
昨天 — 2025年12月13日首页

斐波那契数列:从递归到缓存优化的极致拆解

作者 闲云ing
2025年12月13日 16:57

斐波那契数列:从递归到缓存优化的极致拆解

斐波那契数列是算法入门的经典案例,也是理解「递归」「缓存优化」「闭包」核心思想的绝佳载体。本文会从最基础的递归解法入手,逐步拆解重复计算的痛点,再通过哈希缓存、闭包缓存等方式优化,带你吃透斐波那契数列的解题思路。

一、斐波那契数列的定义

先明确斐波那契数列的核心规则:

  • 起始项:f(0) = 0f(1) = 1
  • 递推公式:f(n) = f(n-1) + f(n-2)(n ≥ 2);
  • 数列示例:0, 1, 1, 2, 3, 5, 8, 13, 21...

简单来说,从0和1开始,后续每一项都等于前两项之和。

二、基础递归解法:思路简单但效率拉胯

1. 递归核心思想

递归的本质是「大问题拆解为小问题」:计算 f(n) 时,先拆解为计算 f(n-1)f(n-2),直到拆解到 f(0)f(1) 这个「递归终止条件」,再逐层返回结果。

2. 代码实现

// 基础递归版斐波那契
function fib(n) {
  // 递归退出条件:触底到0或1,直接返回
  if (n <= 1) return n;
  // 递推公式:拆分为两个子问题
  return fib(n - 1) + fib(n - 2);
}

console.log(fib(10)); // 55(小数值正常)
console.log(fib(100)); // 卡死(重复计算导致超时)

3. 核心问题分析

(1)重复计算严重

fib(5) 为例,拆解过程如下:

fib(5) = fib(4) + fib(3)
fib(4) = fib(3) + fib(2)
fib(3) = fib(2) + fib(1)
fib(2) = fib(1) + fib(0)

可以看到:fib(3) 被计算了2次,fib(2) 被计算了3次,fib(1) 被计算了5次。随着n增大,重复计算呈指数级增长。

(2)时间复杂度爆炸
  • 时间复杂度:O(2ⁿ),指数级复杂度,n=40时计算时间就会明显增加,n=100直接卡死;
  • 空间复杂度:O(n),递归调用栈的深度等于n,极端情况下会触发「栈溢出」。
(3)调用栈溢出风险

递归依赖函数调用栈存储上下文,当n过大时(比如n=10000),会超出JS引擎的调用栈限制,抛出 Maximum call stack size exceeded 错误。

三、优化1:哈希缓存(空间换时间)

1. 优化思路

既然重复计算是核心问题,我们可以用「哈希表(对象)」缓存已经计算过的结果:

  • 计算前先查缓存,存在则直接返回;
  • 计算后将结果存入缓存,避免重复计算。

这是典型的「空间换时间」策略,用少量内存开销换取时间复杂度的大幅降低。

2. 代码实现

// 缓存对象:存储已计算的斐波那契值
const cache = {};

function fib(n) {
  // 1. 优先查缓存,存在则直接返回
  if (n in cache) {
    return cache[n];
  }
  // 2. 递归终止条件
  if (n <= 1) {
    cache[n] = n; // 存入缓存
    return n;
  }
  // 3. 计算并缓存结果
  const result = fib(n - 1) + fib(n - 2);
  cache[n] = result;
  return result;
}

console.log(fib(100)); // 顺利输出:354224848179261915075

3. 优化效果分析

  • 时间复杂度:O(n),每个n只计算一次,后续直接取缓存;
  • 空间复杂度:O(n),缓存对象存储n个值 + 递归调用栈深度n;
  • 核心改进:彻底解决重复计算问题,n=100也能快速计算。

4. 小问题

缓存对象 cache 暴露在全局作用域中,容易被意外修改,破坏了函数逻辑的独立性。

四、优化2:闭包封装缓存(更优雅的空间换时间)

1. 优化思路

用「立即执行函数(IIFE)」创建闭包,将缓存对象封装在函数内部,避免全局污染:

  • IIFE 立即执行,创建独立的作用域;
  • 内部定义缓存对象(自由变量),返回一个计算斐波那契的函数;
  • 返回的函数可以访问闭包中的缓存对象,且外部无法修改。

2. 代码实现

// IIFE 创建闭包,封装缓存
const fib = (function() {
  // 闭包中的缓存:仅内部可访问,避免全局污染
  const cache = {};
  
  // 返回实际的计算函数
  return function(n) {
    if (n in cache) {
      return cache[n];
    }
    if (n <= 1) {
      cache[n] = n;
      return n;
    }
    // 注意:此处调用的是外部的fib(即返回的这个函数)
    cache[n] = fib(n - 1) + fib(n - 2);
    return cache[n];
  }
})();

console.log(fib(100)); // 依然快速输出结果
console.log(cache); // undefined(外部无法访问缓存,更安全)

3. 核心优势

  • 缓存私有化:闭包中的 cache 仅被返回的 fib 函数访问,避免全局污染和意外修改;
  • 代码更优雅:把缓存和计算的逻辑打包在一起,就像把相关工具放进同一个工具箱,用起来方便还不杂乱;
  • 性能不变:时间复杂度仍为O(n),空间复杂度仍为O(n)。

五、补充:递归 vs 迭代(拓展思路)

除了缓存优化递归,还可以用「迭代」彻底避免递归调用栈问题:

// 迭代版斐波那契(空间复杂度可优化至O(1))
function fib(n) {
  if (n <= 1) return n;
  let prev = 0, curr = 1;
  for (let i = 2; i <= n; i++) {
    const next = prev + curr;
    prev = curr;
    curr = next;
  }
  return curr;
}
  • 时间复杂度:O(n);
  • 空间复杂度:O(1),仅用三个变量存储状态,无递归栈和缓存开销。

六、核心知识点总结

1. 递归的适用场景

递归适合解决「可拆分为相似子问题、有明确终止条件、符合树形结构」的问题,但必须注意:

  • 避免重复计算(用缓存优化);
  • 防止栈溢出(n过大时优先用迭代)。

2. 缓存优化的核心思想

「空间换时间」是算法优化的常用策略,核心是存储已计算的结果,避免重复劳动,常见载体包括:

  • 哈希表(对象/Map);
  • 数组;
  • 闭包私有化缓存。

3. IIFE + 闭包的价值

  • IIFE:立即执行函数,创建独立作用域,避免全局污染;
  • 闭包:让内部函数访问外部作用域的变量(如cache),且变量不会被垃圾回收,持续有效。

4. 各版本对比

版本 时间复杂度 空间复杂度 优点 缺点
基础递归 O(2ⁿ) O(n) 思路简单 重复计算、易栈溢出
哈希缓存 O(n) O(n) 解决重复计算 缓存全局暴露
闭包缓存 O(n) O(n) 缓存私有化、代码优雅 仍有递归栈开销
迭代 O(n) O(1) 性能最优、无栈溢出 思路稍绕

七、总结

斐波那契数列的优化过程,是算法思维从「简单实现」到「高效优雅」的典型体现:

  1. 基础递归:满足「能跑」,但存在重复计算和栈溢出问题;
  2. 哈希缓存:解决重复计算,时间复杂度从O(2ⁿ)降到O(n);
  3. 闭包缓存:在缓存的基础上优化代码结构,实现缓存私有化;
  4. 迭代优化:彻底摆脱递归栈,空间复杂度降到O(1)。

斐波那契看似简单,却是理解算法优化的绝佳入口。从朴素递归的指数爆炸,到缓存记忆化的时间换空间,再到闭包封装的工程优雅,最后迭代实现极致效率——每一步都体现了“用合适工具解决合适问题”的编程智慧。

异步并行任务执行工具

作者 NuLL
2025年12月13日 16:30

📖 概述

runParallelTasks 是一个生产级的并行异步任务执行工具,它提供了一种优雅的方式来并行执行多个异步任务,同时支持丰富的功能如重试机制、超时控制、进度追踪和任务取消。

🎯 设计哲学

为什么这样设计?

传统异步并行处理(如 Promise.all())存在以下局限性:

  1. 错误处理粗糙:一个任务失败会导致整个批次失败
  2. 缺乏进度反馈:无法知道任务执行进度
  3. 无取消机制:无法中途停止任务执行
  4. 缺乏重试能力:网络波动时无法自动恢复
  5. 资源管理困难:无法清理超时任务和监听器

本工具的设计目标是解决这些问题,提供:

  • ✅ 细粒度错误处理:每个任务独立处理成功/失败
  • ✅ 实时进度追踪:精确掌握执行进度
  • ✅ 完善的取消机制:支持随时取消所有任务
  • ✅ 智能重试策略:自动重试失败任务
  • ✅ 资源自动管理:避免内存泄漏

🆚 与传统方案对比

特性 Promise.all() Promise.allSettled() runParallelTasks
错误处理 一个失败全部失败 收集所有结果,无后续处理 每个任务独立错误处理 + 全局兜底
进度追踪 ❌ 不支持 ❌ 不支持 ✅ 实时进度回调
取消机制 ❌ 不支持 ❌ 不支持 ✅ 支持取消所有任务
重试机制 ❌ 不支持 ❌ 不支持 ✅ 支持配置化重试
超时控制 ❌ 不支持 ❌ 不支持 ✅ 支持任务级超时
资源清理 ❌ 无 ❌ 无 ✅ 自动清理定时器/监听器
错误调试 简单错误信息 简单状态信息 ✅ 完整错误历史记录

🏗️ 架构设计

核心执行流程

// 执行流程:重试 → 超时 → 取消
const executeTask = () => withRetry(asyncTask, retryCount, retryDelay, signal, taskIndex, taskName);
const taskPromise = Promise.resolve()
  .then(() => withTimeout(executeTask, timeout, taskIndex, taskName))
  // 后续处理...

设计说明

  • 执行顺序:超时包裹重试,确保总超时包含所有重试尝试
  • 取消检查:每次重试前检查取消状态,避免无效执行
  • 错误传播:重试用尽后向上抛出最终错误

重试机制 (withRetry)

/**
 * 带重试的任务执行器
 * 设计特点:
 * 1. 迭代实现:避免递归导致的堆栈溢出
 * 2. 取消检查:每次重试前检查取消信号
 * 3. 错误记录:记录所有重试错误的历史记录
 * 4. 延迟响应:重试延迟期间可立即响应取消
 */
const withRetry = async (asyncTask, retryCount = 0, retryDelay = 0, signal, taskIndex, taskName) => {
  const retryErrors = []; // 记录所有重试错误
  let currentRetry = 0;

  while (currentRetry <= retryCount) {
    // 检查取消(第一道防线)
    if (signal?.aborted) {
      const abortError = new Error(`任务[${taskIndex}](${taskName || '未知'})已取消,终止重试`);
      abortError.name = 'AbortError';
      abortError.retryErrors = retryErrors;
      abortError.retryCount = currentRetry;
      abortError.totalRetry = retryCount;
      throw abortError;
    }

    try {
      const result = await asyncTask(signal);
      return {
        data: result,
        retryCount: currentRetry,
        totalRetry: retryCount,
        retryErrors
      };
    } catch (error) {
      // 记录错误历史
      retryErrors.push({
        retry: currentRetry,
        error: error.message,
        timestamp: new Date().toISOString()
      });

      // 重试用尽
      if (currentRetry >= retryCount) {
        error.retryErrors = retryErrors;
        error.retryCount = currentRetry;
        error.totalRetry = retryCount;
        throw error;
      }

      // 延迟重试(支持取消)
      await delayWithCancel(retryDelay, signal, taskIndex, taskName);
      currentRetry++;
    }
  }
};

延迟函数 (delayWithCancel)

/**
 * 带取消响应的延迟函数
 * 设计特点:
 * 1. 取消响应:延迟期间监听取消信号,立即中断
 * 2. 资源清理:自动清理定时器和事件监听器
 * 3. 原子操作:确保清理操作只执行一次
 */
const delayWithCancel = (delay, signal, taskIndex, taskName) => {
  return new Promise((resolve, reject) => {
    if (delay <= 0) return resolve();
    
    // 立即检查取消状态
    if (signal?.aborted) {
      const abortError = new Error(`任务[${taskIndex}](${taskName || '未知'})已取消,终止延迟`);
      abortError.name = 'AbortError';
      return reject(abortError);
    }

    let timeoutId;
    let abortHandler;
    
    // 统一的清理函数
    const cleanup = () => {
      clearTimeout(timeoutId);
      if (abortHandler) {
        signal?.removeEventListener('abort', abortHandler);
      }
    };

    // 延迟成功结束
    const onFinish = () => {
      cleanup();
      resolve();
    };

    // 取消处理函数
    abortHandler = () => {
      cleanup();
      const abortError = new Error(`任务[${taskIndex}](${taskName || '未知'})取消,中断重试延迟`);
      abortError.name = 'AbortError';
      reject(abortError);
    };

    // 设置延迟
    timeoutId = setTimeout(onFinish, delay);
    
    // 监听取消信号
    signal?.addEventListener('abort', abortHandler);
  });
};

超时控制 (withTimeout)

/**
 * 带超时的任务执行器
 * 设计特点:
 * 1. 总超时:超时时间包含所有重试尝试
 * 2. 竞态执行:任务执行与超时竞态,先完成者生效
 * 3. 自动清理:任务完成后自动清理超时定时器
 */
const withTimeout = (taskFn, timeout, taskIndex, taskName) => {
  if (!timeout || timeout <= 0) return taskFn();

  return new Promise((resolve, reject) => {
    let timeoutId;
    
    // 超时Promise
    const timeoutPromise = new Promise((_, reject) => {
      timeoutId = setTimeout(() => {
        const timeoutError = new Error(`任务[${taskIndex}](${taskName || '未知'})超时(${timeout}ms,含所有重试)`);
        timeoutError.name = 'TaskTimeoutError';
        timeoutError.taskIndex = taskIndex;
        timeoutError.taskName = taskName;
        reject(timeoutError);
      }, timeout);
    });

    // 竞态执行
    Promise.race([taskFn(), timeoutPromise])
      .then(resolve)
      .catch(reject)
      .finally(() => {
        clearTimeout(timeoutId); // 关键:清理超时定时器
      });
  });
};

结果聚合 (allDone)

/**
 * 聚合所有任务结果
 * 设计特点:
 * 1. 统一格式:将所有任务结果格式化为统一结构
 * 2. 错误兜底:处理意料之外的错误
 * 3. 完整信息:包含任务索引、名称、重试信息等
 */
const allDone = Promise.allSettled(taskPromises).then((settledResults) => {
  return settledResults.map((item) => {
    if (item.status === 'fulfilled') return item.value;
    
    // 兜底处理:理论上不会执行到这里(内部已catch所有错误)
    return {
      success: false,
      error: item.reason,
      taskIndex: -1,
      taskName: '未知任务',
      isAborted: false,
      reason: 'UNHANDLED_ERROR',
      retryCount: 0,
      totalRetry: 0,
      retryErrors: []
    };
  });
});

📚 使用方法

基本安装

// 1. 复制 runParallelTasks 函数到你的项目
// 2. 导入函数
import { runParallelTasks } from './utils/asyncTask';

// 或者作为独立模块使用
// import runParallelTasks from 'parallel-task-runner';

任务队列配置

每个任务可以配置以下属性:

const task = {
  // 必需:异步任务函数,可接收 AbortSignal
  asyncTask: (signal) => fetch('/api/data', { signal }).then(r => r.json()),
  
  // 可选:任务成功回调(支持异步)
  onSuccess: (data, index) => {
    console.log(`任务${index}成功:`, data);
    updateUI(data);
  },
  
  // 可选:任务失败回调(支持异步)
  onError: (error, index) => {
    console.error(`任务${index}失败:`, error);
    showError(error);
  },
  
  // 可选:任务名称(用于日志和调试)
  taskName: '获取用户数据',
  
  // 可选:总超时时间(毫秒,包含所有重试)
  timeout: 10000,
  
  // 可选:重试次数(默认0,不重试)
  retryCount: 3,
  
  // 可选:重试延迟(毫秒,默认0)
  retryDelay: 1000
};

执行配置

const options = {
  // 必需:任务队列数组
  taskQueue: [...],
  
  // 可选:全局进度回调
  onProgress: (completed, total, taskIndex, taskName) => {
    console.log(`进度: ${completed}/${total}`);
    updateProgressBar(completed / total);
  },
  
  // 可选:全局错误兜底
  onGlobalError: (error, taskIndex, taskName) => {
    console.error(`任务${taskIndex}(${taskName})未处理错误:`, error);
    sendToErrorTracking(error);
  },
  
  // 可选:是否启用取消功能(默认true)
  enableAbort: true
};

执行和结果处理

// 执行任务
const runner = runParallelTasks(options);

// 1. 使用 allDone 等待所有任务完成
runner.allDone.then(results => {
  const successCount = results.filter(r => r.success).length;
  const failedCount = results.filter(r => !r.success).length;
  
  console.log(`完成: ${successCount}成功, ${failedCount}失败`);
  
  // 处理成功结果
  results.filter(r => r.success).forEach(result => {
    console.log(`任务${result.taskIndex}结果:`, result.result);
  });
  
  // 处理失败结果
  results.filter(r => !r.success).forEach(result => {
    console.error(`任务${result.taskIndex}失败原因:`, result.error.message);
    if (result.retryCount > 0) {
      console.error(`已重试${result.retryCount}次`, result.retryErrors);
    }
  });
});

// 2. 随时取消任务(如页面卸载时)
// runner.abort();

// 3. 访问单个任务的Promise(高级用法)
// runner.promises[0].then(result => console.log('第一个任务结果:', result));

📋 使用案例

案例1:页面数据加载

/**
 * 场景:页面初始化时需要并行加载多个API数据
 * 需求:需要进度显示,支持取消,关键数据需要重试
 */
const loadPageData = () => {
  const taskQueue = [
    {
      taskName: '用户信息',
      asyncTask: (signal) => api.getUserInfo({ signal }),
      timeout: 5000,
      retryCount: 1,
      retryDelay: 1000,
      onSuccess: (data) => store.commit('SET_USER', data),
      onError: (error) => {
        console.error('用户信息加载失败');
        showFallbackUserInfo();
      }
    },
    {
      taskName: '配置信息',
      asyncTask: (signal) => api.getConfig({ signal }),
      timeout: 3000,
      onSuccess: (data) => store.commit('SET_CONFIG', data)
    },
    {
      taskName: '推荐内容',
      asyncTask: (signal) => api.getRecommendations({ signal }),
      timeout: 8000,
      onSuccess: (data) => store.commit('SET_RECOMMENDATIONS', data)
    }
  ];

  const runner = runParallelTasks({
    taskQueue,
    onProgress: (completed, total) => {
      showLoadingProgress(completed / total * 100);
    },
    onGlobalError: (error, index, name) => {
      logToMonitoring('页面数据加载失败', { taskIndex: index, taskName: name, error });
    }
  });

  // 返回runner,以便在组件卸载时取消
  return runner;
};

// 使用
const pageDataLoader = loadPageData();

// 等待所有数据加载完成
pageDataLoader.allDone.then(results => {
  const allSuccess = results.every(r => r.success);
  if (allSuccess) {
    showPageContent();
  } else {
    showPartialContent(results);
  }
});

// 页面卸载时取消未完成的任务
onBeforeUnmount(() => {
  pageDataLoader.abort();
});

案例2:批量文件上传

/**
 * 场景:批量上传多个文件
 * 需求:显示总进度,单个文件可重试,支持取消上传
 */
const uploadFiles = (files) => {
  const taskQueue = files.map((file, index) => ({
    taskName: `文件: ${file.name}`,
    asyncTask: async (signal) => {
      // 使用FormData上传
      const formData = new FormData();
      formData.append('file', file);
      
      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData,
        signal // 支持取消
      });
      
      if (!response.ok) {
        throw new Error(`上传失败: ${response.status}`);
      }
      
      return await response.json();
    },
    timeout: 30000, // 30秒超时
    retryCount: 2,  // 重试2次
    retryDelay: 2000, // 2秒后重试
    onSuccess: (result, index) => {
      updateFileStatus(index, 'success');
      console.log(`文件${file.name}上传成功:`, result);
    },
    onError: (error, index) => {
      updateFileStatus(index, 'error');
      console.error(`文件${file.name}上传失败:`, error);
      
      // 根据重试情况显示不同提示
      if (error.retryCount > 0) {
        showToast(`${file.name}上传失败,已重试${error.retryCount}次`);
      }
    }
  }));

  const runner = runParallelTasks({
    taskQueue,
    onProgress: (completed, total) => {
      updateTotalProgress(completed / total * 100);
    },
    onGlobalError: (error, index) => {
      console.error(`文件${files[index]?.name}上传异常:`, error);
    }
  });

  return runner;
};

// 使用
const files = [...]; // 文件列表
const uploadRunner = uploadFiles(files);

// 监控上传结果
uploadRunner.allDone.then(results => {
  const successCount = results.filter(r => r.success).length;
  showToast(`上传完成: ${successCount}/${files.length}个文件成功`);
  
  // 处理失败的文件
  results.filter(r => !r.success).forEach(result => {
    logUploadFailure(result);
  });
});

// 用户取消上传
cancelButton.onclick = () => {
  uploadRunner.abort();
  showToast('上传已取消');
};

案例3:健康检查监控

/**
 * 场景:监控多个微服务的健康状态
 * 需求:并行检查,快速失败,记录检查历史
 */
const checkServiceHealth = (services) => {
  const taskQueue = services.map((service, index) => ({
    taskName: service.name,
    asyncTask: async (signal) => {
      const response = await fetch(`${service.url}/health`, {
        signal,
        timeout: 3000
      });
      
      const data = await response.json();
      
      if (data.status !== 'healthy') {
        throw new Error(`服务状态异常: ${data.status}`);
      }
      
      return data;
    },
    timeout: 5000, // 5秒超时
    retryCount: 1, // 快速重试1次
    retryDelay: 1000,
    onSuccess: (data, index) => {
      markServiceHealthy(services[index].id);
      console.log(`${services[index].name}健康检查通过`);
    },
    onError: (error, index) => {
      const service = services[index];
      markServiceUnhealthy(service.id);
      
      // 记录详细的健康检查失败信息
      logHealthCheckFailure({
        service: service.name,
        error: error.message,
        retryCount: error.retryCount || 0,
        retryErrors: error.retryErrors || []
      });
    }
  }));

  const runner = runParallelTasks({
    taskQueue,
    onProgress: (completed, total) => {
      updateDashboardHealthStatus(completed, total);
    },
    onGlobalError: (error, index, name) => {
      // 发送到监控系统
      sendToMonitoringSystem({
        type: 'HEALTH_CHECK_ERROR',
        service: name,
        error: error.message
      });
    },
    enableAbort: false // 健康检查不需要取消
  });

  return runner;
};

// 定时执行健康检查
setInterval(() => {
  const services = [
    { id: 'auth', name: '认证服务', url: 'https://auth.example.com' },
    { id: 'payment', name: '支付服务', url: 'https://payment.example.com' },
    { id: 'notification', name: '通知服务', url: 'https://notification.example.com' }
  ];
  
  const healthChecker = checkServiceHealth(services);
  
  healthChecker.allDone.then(results => {
    const healthyCount = results.filter(r => r.success).length;
    updateSystemHealthIndicator(healthyCount / results.length * 100);
    
    // 如果有服务不健康,发送警报
    const unhealthy = results.filter(r => !r.success);
    if (unhealthy.length > 0) {
      sendAlert(`有${unhealthy.length}个服务不健康`);
    }
  });
}, 60000); // 每分钟检查一次

案例4:API请求合并优化

/**
 * 场景:页面需要多个API数据,传统方案是串行请求
 * 优化:使用并行请求减少总加载时间
 */
const fetchDashboardData = () => {
  const taskQueue = [
    {
      taskName: '用户统计',
      asyncTask: () => api.getUserStats(),
      timeout: 3000,
      onSuccess: (data) => store.commit('SET_USER_STATS', data)
    },
    {
      taskName: '销售数据',
      asyncTask: () => api.getSalesData(),
      timeout: 5000,
      retryCount: 1,
      onSuccess: (data) => store.commit('SET_SALES_DATA', data)
    },
    {
      taskName: '库存状态',
      asyncTask: () => api.getInventoryStatus(),
      timeout: 4000,
      onSuccess: (data) => store.commit('SET_INVENTORY', data)
    },
    {
      taskName: '活动列表',
      asyncTask: () => api.getActivities(),
      timeout: 6000,
      onSuccess: (data) => store.commit('SET_ACTIVITIES', data)
    }
  ];

  const runner = runParallelTasks({
    taskQueue,
    onProgress: (completed, total) => {
      // 显示加载进度
      const progress = Math.min(completed / total * 100, 99); // 最大99%,留1%给最终处理
      updateLoadingProgress(progress);
    },
    onGlobalError: (error, index, name) => {
      console.error(`仪表板数据加载失败: ${name}`, error);
    }
  });

  return runner;
};

// 使用 - 相比串行请求,时间从 sum(time) 减少到 max(time)
const dashboardLoader = fetchDashboardData();

// 传统串行方式大约需要 3+5+4+6 = 18秒
// 并行方式最多只需要 max(3,5,4,6) = 6秒

dashboardLoader.allDone.then(results => {
  const allLoaded = results.every(r => r.success);
  
  if (allLoaded) {
    showDashboard();
  } else {
    // 部分数据加载失败,显示降级内容
    showDegradedDashboard(results);
  }
});

🔧 高级配置

自定义重试策略

// 基于错误类型的重试策略
const createRetryConfig = (error) => {
  // 网络错误:重试3次
  if (error.name === 'NetworkError' || error.name === 'TypeError') {
    return { retryCount: 3, retryDelay: 1000 };
  }
  
  // 服务器5xx错误:重试2次
  if (error.status >= 500 && error.status < 600) {
    return { retryCount: 2, retryDelay: 2000 };
  }
  
  // 其他错误:不重试
  return { retryCount: 0 };
};

// 在任务配置中使用
const task = {
  asyncTask: async (signal) => {
    try {
      return await fetch('/api/data', { signal }).then(r => r.json());
    } catch (error) {
      // 根据错误类型动态决定重试策略
      const retryConfig = createRetryConfig(error);
      error.retryConfig = retryConfig;
      throw error;
    }
  },
  // 动态重试配置
  retryCount: (task) => task.error?.retryConfig?.retryCount || 0,
  retryDelay: (task) => task.error?.retryConfig?.retryDelay || 0
};

性能监控集成

// 添加性能监控
const monitoredRunParallelTasks = (options) => {
  const startTime = performance.now();
  const taskCount = options.taskQueue.length;
  
  const runner = runParallelTasks({
    ...options,
    onProgress: (completed, total, taskIndex, taskName) => {
      // 调用原始进度回调
      options.onProgress?.(completed, total, taskIndex, taskName);
      
      // 性能监控
      if (completed === total) {
        const endTime = performance.now();
        const duration = endTime - startTime;
        
        sendToAnalytics({
          event: 'PARALLEL_TASKS_COMPLETED',
          taskCount,
          duration,
          successRate: completed / total
        });
      }
    }
  });
  
  return runner;
};

📊 性能建议

最佳实践

  1. 合理设置超时时间
    • 关键任务:5-10秒
    • 非关键任务:3-5秒
    • 后台任务:10-30秒
  1. 重试策略建议
    • 网络请求:重试2-3次,延迟1-2秒
    • 支付操作:重试1-2次,延迟2-3秒
    • 文件上传:重试1次,延迟3秒
  1. 并发控制
    • 虽然工具支持无限并发,但建议根据实际情况控制任务数量
    • 大量任务(>50)建议分批执行
  1. 内存管理
    • 页面卸载时务必调用 abort() 取消未完成任务
    • 监控长时间运行的任务,避免内存泄漏

🐛 常见问题

Q1: 任务取消后,allDone 还会返回结果吗?

A: 会的。取消的任务会返回一个特殊的结果对象,其中 isAborted: truereason: 'USER_CANCELLED'allDone 会等待所有任务(包括被取消的)完成。

Q2: 重试期间超时如何计算?

A: 超时时间是从任务开始到结束的总时间,包含所有重试尝试。例如:设置 timeout: 10000,重试3次,那么从第一次尝试开始计时,10秒后如果还没成功则超时。

Q3: 任务函数必须接收 signal 参数吗?

A: 不需要。工具总是传递 signal 参数,但如果你的任务函数不需要取消功能,可以忽略这个参数。

Q4: 如何实现并发控制?

A: 当前版本不内置并发控制,因为设计目标是真正的并行执行。如果需要并发控制,建议在外部实现任务分批。

Q5: 错误对象中的 retryErrors 包含什么?

A: 包含所有重试尝试的错误记录数组,每个记录包含:

  • retry: 第几次重试(从0开始)
  • error: 错误信息
  • timestamp: 错误发生时间

📈 扩展建议

如果未来需要扩展功能,可以考虑:

  1. 优先级调度:为任务添加优先级,高优先级先执行
  2. 依赖关系:支持任务间的依赖关系
  3. 并发限制:限制同时执行的任务数量
  4. 断点续传:对于长时间任务支持暂停/恢复
  5. 更复杂的重试策略:指数退避、抖动等算法

📝 总结

runParallelTasks 是一个功能全面、设计优雅的并行任务执行工具,它解决了传统异步并行处理的诸多痛点,特别适合以下场景:

  • ✅ 复杂页面初始化:需要加载多个API
  • ✅ 批量操作:文件上传、数据导入导出
  • ✅ 监控检查:服务健康检查、心跳检测
  • ✅ 实时数据处理:并行处理多个数据流
  • ✅ 用户交互响应:多个后台任务并行执行

通过合理使用这个工具,可以显著提升应用的用户体验和代码的可维护性。


📄 完整代码

最后,这是完整的 runParallelTasks 函数代码:

/**
 * @file utils/asyncTask.js
 * @description 并行执行异步任务队列(重试机制终极优化版)
 * 核心特性:
 * 1. 重试延迟期间可立即响应取消(无需等待延迟结束)
 * 2. 所有定时器(重试延迟/超时)自动清理,无内存泄漏
 * 3. 每次重试前检查取消状态,避免无效重试
 * 4. 记录所有重试错误(保留最后一次错误为主,附带错误列表)
 * 5. 总超时包裹整个重试过程(符合需求),重试次数/延迟可配置
 * 6. 取消/超时/重试逻辑解耦,代码结构清晰
 */

export function runParallelTasks({
  taskQueue,
  onProgress,
  onGlobalError,
  enableAbort = true
}) {
  // 初始化取消控制器
  const controller = enableAbort ? new AbortController() : null;
  const { signal } = controller || {};
  const total = taskQueue.length;
  let completed = 0;
  const taskPromises = [];

  // 空队列兜底
  if (total === 0) {
    console.warn('runParallelTasks: 任务队列为空');
    return {
      promises: taskPromises,
      abort: () => {},
      allDone: Promise.resolve([])
    };
  }

  /**
   * 带取消响应的延迟函数(核心改进:延迟期间可取消,清理定时器)
   * @param {number} delay 延迟毫秒数
   * @param {AbortSignal} signal 取消信号
   * @param {number} taskIndex 任务索引
   * @param {string} taskName 任务名称
   * @returns {Promise<void>} 延迟Promise,取消时立即reject
   */
  const delayWithCancel = (delay, signal, taskIndex, taskName) => {
    return new Promise((resolve, reject) => {
      if (delay <= 0) return resolve();
      
      // 检查是否已取消
      if (signal?.aborted) {
        const abortError = new Error(`任务[${taskIndex}](${taskName || '未知'})已取消,终止延迟`);
        abortError.name = 'AbortError';
        return reject(abortError);
      }

      let timeoutId;
      let abortHandler;
      
      // 清理函数
      const cleanup = () => {
        clearTimeout(timeoutId);
        if (abortHandler) {
          signal?.removeEventListener('abort', abortHandler);
        }
      };

      // 延迟成功结束
      const onFinish = () => {
        cleanup();
        resolve();
      };

      // 取消处理
      abortHandler = () => {
        cleanup();
        const abortError = new Error(`任务[${taskIndex}](${taskName || '未知'})取消,中断重试延迟`);
        abortError.name = 'AbortError';
        reject(abortError);
      };

      // 设置延迟定时器
      timeoutId = setTimeout(onFinish, delay);
      
      // 监听取消信号
      signal?.addEventListener('abort', abortHandler);
    });
  };

  /**
   * 带重试的任务执行器(核心改进:延迟响应取消、记录所有错误、清理定时器)
   * @param {Function} asyncTask 异步任务函数
   * @param {number} retryCount 重试次数
   * @param {number} retryDelay 重试间隔
   * @param {AbortSignal} signal 取消信号
   * @param {number} taskIndex 任务索引
   * @param {string} taskName 任务名称
   * @returns {Promise<any>} 任务执行结果
   */
  const withRetry = async (asyncTask, retryCount = 0, retryDelay = 0, signal, taskIndex, taskName) => {
    const retryErrors = []; // 记录所有重试错误
    let currentRetry = 0;

    while (currentRetry <= retryCount) {
      // 每次重试前检查是否已取消(第一道防线)
      if (signal?.aborted) {
        const abortError = new Error(`任务[${taskIndex}](${taskName || '未知'})已取消,终止重试`);
        abortError.name = 'AbortError';
        abortError.retryErrors = retryErrors;
        abortError.retryCount = currentRetry;
        abortError.totalRetry = retryCount;
        throw abortError;
      }

      try {
        // 执行单次任务
        const result = await asyncTask(signal);
        // 成功则返回结果,附带重试信息
        return {
          data: result,
          retryCount: currentRetry,
          totalRetry: retryCount,
          retryErrors
        };
      } catch (error) {
        // 记录当前错误
        retryErrors.push({
          retry: currentRetry,
          error: error.message || String(error),
          timestamp: new Date().toISOString()
        });

        // 重试用尽,抛出最终错误(附带所有重试错误)
        if (currentRetry >= retryCount) {
          if (!error.retryErrors) error.retryErrors = retryErrors;
          if (!error.retryCount) error.retryCount = currentRetry;
          if (!error.totalRetry) error.totalRetry = retryCount;
          throw error;
        }

        console.log(`任务[${taskIndex}](${taskName || '未知'})执行失败,将在${retryDelay}ms后重试(第${currentRetry + 1}/${retryCount}次)`, error.message);
        
        try {
          // 重试延迟(支持取消)
          await delayWithCancel(retryDelay, signal, taskIndex, taskName);
        } catch (delayError) {
          // 延迟期间被取消,传播取消错误
          delayError.retryErrors = retryErrors;
          delayError.retryCount = currentRetry;
          delayError.totalRetry = retryCount;
          throw delayError;
        }
        
        currentRetry++;
      }
    }
  };

  /**
   * 带超时的任务执行器(总超时包裹整个重试过程)
   * @param {Function} taskFn 任务函数(含重试逻辑)
   * @param {number} timeout 总超时时间
   * @param {number} taskIndex 任务索引
   * @param {string} taskName 任务名称
   * @returns {Promise<any>} 任务执行结果
   */
  const withTimeout = (taskFn, timeout, taskIndex, taskName) => {
    if (!timeout || timeout <= 0) return taskFn();

    return new Promise((resolve, reject) => {
      let timeoutId;
      // 总超时Promise
      const timeoutPromise = new Promise((_, reject) => {
        timeoutId = setTimeout(() => {
          const timeoutError = new Error(`任务[${taskIndex}](${taskName || '未知'})超时(${timeout}ms,含所有重试)`);
          timeoutError.name = 'TaskTimeoutError';
          timeoutError.taskIndex = taskIndex;
          timeoutError.taskName = taskName;
          reject(timeoutError);
        }, timeout);
      });

      // 竞态执行:任务(含重试) vs 总超时
      Promise.race([taskFn(), timeoutPromise])
        .then(resolve)
        .catch(reject)
        .finally(() => {
          clearTimeout(timeoutId); // 清理超时定时器
        });
    });
  };

  // 遍历执行每个任务
  taskQueue.forEach((task, taskIndex) => {
    const {
      asyncTask,
      onSuccess,
      onError,
      taskName,
      timeout,
      retryCount = 0,
      retryDelay = 0
    } = task;

    // 执行任务:重试(带取消/延迟清理) → 总超时 → 取消
    const executeTask = () => withRetry(asyncTask, retryCount, retryDelay, signal, taskIndex, taskName);

    const taskPromise = Promise.resolve()
      .then(() => withTimeout(executeTask, timeout, taskIndex, taskName))
      // 成功处理
      .then((result) => {
        // 解构重试结果(兼容无重试的情况)
        const { data, retryCount: actualRetry, totalRetry, retryErrors } = result || {};
        return Promise.resolve(onSuccess?.(data, taskIndex))
          .then(() => ({
            success: true,
            result: data,
            taskIndex,
            taskName,
            isAborted: false,
            retryCount: actualRetry || 0,
            totalRetry: totalRetry || 0,
            retryErrors: retryErrors || []
          }));
      })
      // 失败处理
      .catch((error) => {
        // 处理主动取消(含重试延迟中取消)
        if (error.name === 'AbortError') {
          console.log(`runParallelTasks: 任务[${taskIndex}](${taskName || '未知'})已取消`, error.message);
          return {
            success: false,
            error,
            taskIndex,
            taskName,
            isAborted: true,
            reason: 'USER_CANCELLED',
            retryCount: error.retryCount || 0,
            totalRetry: error.totalRetry || 0,
            retryErrors: error.retryErrors || []
          };
        }

        // 处理超时/最终执行失败(重试用尽)
        return Promise.resolve()
          .then(() => {
            // 优先执行专属错误回调
            if (onError) {
              return onError(error, taskIndex);
            }
            // 全局错误处理(try-catch兜底)
            try {
              onGlobalError?.(error, taskIndex, taskName);
            } catch (globalErr) {
              console.error(`runParallelTasks: 全局错误处理函数执行失败`, globalErr);
            }
            console.error(
              `runParallelTasks: 任务[${taskIndex}](${taskName || '未知'})最终执行失败`,
              `已重试${error.retryCount || 0}/${error.totalRetry || 0}次`,
              `错误列表:${JSON.stringify(error.retryErrors || [])}`,
              error
            );
          })
          .then(() => ({
            success: false,
            error,
            taskIndex,
            taskName,
            isAborted: false,
            reason: error.name === 'TaskTimeoutError' ? 'TIMEOUT' : 'EXECUTION_FAILED',
            retryCount: error.retryCount || 0,
            totalRetry: error.totalRetry || 0,
            retryErrors: error.retryErrors || []
          }));
      })
      // 进度更新(原子操作)
      .finally(() => {
        const currentCompleted = ++completed;
        onProgress?.(currentCompleted, total, taskIndex, taskName);
      });

    taskPromises.push(taskPromise);
  });

  // 聚合Promise:格式化所有任务结果
  const allDone = Promise.allSettled(taskPromises).then((settledResults) => {
    return settledResults.map((item) => {
      if (item.status === 'fulfilled') return item.value;
      // 兜底处理
      return {
        success: false,
        error: item.reason,
        taskIndex: -1,
        taskName: '未知任务',
        isAborted: false,
        reason: 'UNHANDLED_ERROR',
        retryCount: 0,
        totalRetry: 0,
        retryErrors: []
      };
    });
  });

  // 取消方法
  const abort = () => {
    if (controller) {
      controller.abort();
      console.log('runParallelTasks: 已触发取消所有任务');
    } else {
      console.warn('runParallelTasks: 未开启取消功能(enableAbort=false)');
    }
  };

  return {
    promises: taskPromises,
    abort,
    allDone
  };
}

异步互斥锁

作者 NuLL
2025年12月13日 16:27

异步任务互斥锁工具 (Async Lock Manager)

📖 概述

LockManager 是一个生产级的异步任务互斥锁管理工具,专为现代 Web 应用中的并发控制设计。它通过互斥锁机制防止异步任务重复执行,提供队列管理、智能重试、超时控制和资源自动清理等功能。

🎯 设计哲学

为什么需要异步任务互斥锁?

传统的防抖节流方案存在以下局限性:

  1. 无法防止长时间异步操作:防抖节流只能控制函数调用频率,但无法防止 API 接口长时间未返回时的重复调用
  2. 缺乏队列管理:多个并发请求无法有序排队执行
  3. 缺少取消机制:无法中断已发起的异步任务
  4. 资源管理困难:无法自动清理过期锁和等待任务
  5. 缺乏智能重试:简单的重试策略无法适应复杂错误场景

本工具的设计目标是解决这些问题,提供:

  • ✅ 原子性操作:确保锁的获取和释放是原子操作
  • ✅ 智能队列管理:支持 FIFO 队列,可配置队列大小和超时
  • ✅ 可中断执行:支持任务取消和超时中断
  • ✅ 指数退避重试:支持自定义重试条件和退避策略
  • ✅ 资源自动管理:自动清理过期锁和队列项
  • ✅ 完整监控统计:提供执行统计和状态监控

🆚 与传统方案对比

特性 防抖 (Debounce) 节流 (Throttle) 简单互斥锁 LockManager
防止重复调用 ✅ 时间窗口内 ✅ 固定频率 ✅ 直到完成 ✅ 直到完成 + 队列
异步任务支持 ❌ 有限 ❌ 有限 ✅ 基础 ✅ 完整(重试、超时、取消)
队列管理 ❌ 不支持 ❌ 不支持 ❌ 不支持 ✅ 支持 FIFO 队列
取消机制 ❌ 不支持 ❌ 不支持 ❌ 不支持 ✅ 支持主动取消
重试策略 ❌ 不支持 ❌ 不支持 ❌ 不支持 ✅ 指数退避 + 自定义条件
超时控制 ❌ 不支持 ❌ 不支持 ❌ 不支持 ✅ 支持任务和队列超时
资源清理 ❌ 无 ❌ 无 ❌ 无 ✅ 自动清理过期锁
状态监控 ❌ 无 ❌ 无 ❌ 无 ✅ 完整统计信息

🏗️ 架构设计

核心执行流程

// 执行流程:检查锁 → 加入队列(可选) → 获取锁 → 执行任务 → 释放锁 → 处理队列
async execute(options) {
  // 1. 检查锁状态
  // 2. 如果已锁定且启用队列,加入队列等待
  // 3. 获取锁(原子操作)
  // 4. 执行任务(支持重试)
  // 5. 清理锁资源
  // 6. 处理队列中的下一个任务
}

锁管理机制 (_acquireLock)

/**
 * 原子性地获取锁
 * 设计特点:
 * 1. 三重检查:确保锁获取的原子性
 * 2. 唯一标识:为每个锁尝试生成唯一ID
 * 3. 资源预分配:提前创建取消控制器
 * 4. 验证机制:设置后验证确保原子性
 */
_acquireLock(name) {
  const attemptId = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
  
  // 第一重检查
  const existing = this._lockMap.get(name);
  if (existing?.locked) {
    return null;
  }
  
  // 创建锁对象
  const lockItem = {
    locked: true,
    abortController: new AbortController(),
    timeoutTimer: null,
    createdAt: Date.now(),
    taskId: `${name}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
    attemptId: attemptId
  };
  
  // 第二重检查(原子性保障)
  const current = this._lockMap.get(name);
  if (current?.locked) {
    return null;
  }
  
  // 设置锁
  this._lockMap.set(name, lockItem);
  
  // 最终验证(确保原子性)
  const afterSet = this._lockMap.get(name);
  if (afterSet?.attemptId !== attemptId) {
    lockItem.abortController.abort();
    return null;
  }
  
  return lockItem;
}

队列管理机制 (_addToQueue_processNextInQueue)

/**
 * 将任务加入等待队列
 * 设计特点:
 * 1. 容量控制:可配置最大队列大小
 * 2. 超时管理:队列等待也有超时控制
 * 3. 有序执行:FIFO(先进先出)原则
 * 4. 资源清理:超时自动清理队列项
 */
_addToQueue(options) {
  const { name, maxQueueSize } = options;
  
  let queue = this._queueMap.get(name);
  if (!queue) {
    queue = [];
    this._queueMap.set(name, queue);
  }
  
  // 队列容量检查
  if (queue.length >= maxQueueSize) {
    const error = new Error(`任务队列【${name}】已满(最大${maxQueueSize})`);
    error.type = 'queue_full';
    error.code = 'QUEUE_FULL';
    return Promise.reject(error);
  }
  
  return new Promise((resolve, reject) => {
    const queueItem = {
      options,
      resolve,
      reject,
      enqueuedAt: Date.now()
    };
    
    queue.push(queueItem);
    
    // 队列等待超时
    if (options.timeout > 0) {
      queueItem.timeoutTimer = setTimeout(() => {
        const index = queue.indexOf(queueItem);
        if (index > -1) {
          queue.splice(index, 1);
          const error = new Error(`任务【${name}】在队列中等待超时`);
          error.type = 'queue_timeout';
          error.code = 'QUEUE_TIMEOUT';
          reject(error);
        }
      }, options.timeout);
    }
  });
}

/**
 * 处理队列中的下一个任务
 * 设计特点:
 * 1. 微任务调度:使用 Promise.resolve() 避免 setTimeout 延迟
 * 2. 递归处理:自动处理队列中的所有任务
 * 3. 资源清理:处理完成后清理空队列
 * 4. 错误传播:正确处理任务成功和失败
 */
async _processNextInQueue(name) {
  const queue = this._queueMap.get(name);
  if (!queue || queue.length === 0) {
    this._queueMap.delete(name);
    return;
  }
  
  // 使用微任务处理,避免 setTimeout 的延迟
  await Promise.resolve();
  
  const queueItem = queue.shift();
  
  // 清理队列项的超时定时器
  if (queueItem.timeoutTimer) {
    clearTimeout(queueItem.timeoutTimer);
  }
  
  try {
    const result = await this._executeTask(queueItem.options);
    queueItem.resolve(result);
  } catch (error) {
    queueItem.reject(error);
  } finally {
    // 递归处理下一个任务
    if (queue.length > 0) {
      Promise.resolve().then(() => this._processNextInQueue(name));
    } else {
      this._queueMap.delete(name);
    }
  }
}

智能重试机制 (_executeWithExponentialBackoff)

/**
 * 指数退避重试执行
 * 设计特点:
 * 1. 取消检查:每次重试前检查取消信号
 * 2. 退避算法:指数退避 + 随机抖动
 * 3. 自定义条件:支持根据错误类型决定是否重试
 * 4. 安全延迟:可中断的延时函数
 */
async _executeWithExponentialBackoff(fn, maxRetries, baseDelay, maxDelay, abortController, retryCondition) {
  let lastError;
  
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      // 检查是否已取消
      if (abortController.signal.aborted) {
        const cancelError = new Error('任务已被取消');
        cancelError.type = 'cancel';
        cancelError.code = 'CANCELLED';
        throw cancelError;
      }
      
      // 非首次尝试时延迟
      if (attempt > 0) {
        const delay = this._calculateExponentialBackoffDelay(attempt, baseDelay, maxDelay);
        await this._sleep(delay, abortController.signal);
      }
      
      return await fn();
      
    } catch (error) {
      lastError = error;
      
      // 判断是否应该重试
      if (!this._shouldRetry(error, retryCondition)) {
        throw error;
      }
      
      // 重试用尽
      if (attempt === maxRetries) {
        error.retryAttempts = attempt;
        throw error;
      }
    }
  }
  
  throw lastError;
}

/**
 * 判断是否应该重试(支持自定义重试条件)
 */
_shouldRetry(error, retryCondition) {
  // 不重试的错误类型
  const noRetryTypes = ['cancel', 'timeout', 'queue_full', 'queue_timeout', 'lock_failed'];
  if (noRetryTypes.includes(error.type)) {
    return false;
  }
  
  // 如果提供了自定义重试条件函数,使用它
  if (typeof retryCondition === 'function') {
    return retryCondition(error);
  }
  
  // 默认重试条件:非特定错误都重试
  return true;
}

资源自动清理 (_cleanupExpiredLocks)

/**
 * 清理过期锁和队列
 * 设计特点:
 * 1. 定期执行:每60秒自动清理一次
 * 2. 双重清理:同时清理过期锁和队列项
 * 3. 优雅终止:清理时发送取消信号
 * 4. 统计记录:记录清理操作便于监控
 */
_cleanupExpiredLocks() {
  const now = Date.now();
  const maxAge = this._defaults.maxLockAge;
  
  // 清理过期锁
  for (const [name, lockItem] of this._lockMap.entries()) {
    if (lockItem.locked && (now - lockItem.createdAt) > maxAge) {
      console.warn(`清理过期锁【${name}】,已锁定${now - lockItem.createdAt}ms`);
      
      const error = new Error('锁过期自动清理');
      error.type = 'timeout';
      error.code = 'LOCK_EXPIRED';
      
      if (lockItem.abortController) {
        lockItem.abortController.abort(error);
      }
      
      this._lockMap.delete(name);
    }
  }
  
  // 清理过期队列项
  for (const [name, queue] of this._queueMap.entries()) {
    // ... 清理逻辑
  }
}

📚 使用方法

基本安装

// 方式1:使用默认单例(无控制台警告)
import { asyncLock, releaseLock } from './asyncLock';

// 方式2:创建自定义实例
import { createLockManager } from './asyncLock';
const myLockManager = createLockManager({
  timeout: 10000,
  maxQueueSize: 10,
  tipHandler: (msg) => console.warn(msg)
});

// 方式3:使用带控制台警告的单例
import { verboseLockManager } from './asyncLock';

基础配置选项

const options = {
  // 必需:锁名称(用于标识任务类型)
  name: 'submitForm',
  
  // 必需:异步任务函数
  asyncFn: async (signal) => {
    // signal 是 AbortSignal,用于取消任务
    if (signal.aborted) throw new Error('任务已取消');
    return await fetch('/api/submit', { signal }).then(r => r.json());
  },
  
  // 可选:任务超时时间(毫秒)
  timeout: 8000,
  
  // 可选:重试次数(默认0)
  retryCount: 2,
  
  // 可选:基础重试延迟(毫秒)
  baseRetryDelay: 1000,
  
  // 可选:最大重试延迟(毫秒)
  maxRetryDelay: 10000,
  
  // 可选:自定义重试条件函数
  retryCondition: (error) => {
    // 只对网络错误重试
    return error.message.includes('Network') || error.message.includes('timeout');
  },
  
  // 可选:重复执行时的提示信息
  repeatTip: '操作中,请稍后...',
  
  // 可选:重复执行时是否抛出错误(默认true)
  throwRepeatError: true,
  
  // 可选:是否启用队列(默认false)
  enableQueue: true,
  
  // 可选:队列最大长度(默认100)
  maxQueueSize: 5,
  
  // 可选:成功回调
  onSuccess: (result) => {
    console.log('任务成功:', result);
  },
  
  // 可选:失败回调
  onFail: (error) => {
    console.error('任务失败:', error.message);
  },
  
  // 可选:提示处理器(用于显示重复提示)
  tipHandler: (message) => {
    Toast.warning(message);
  }
};

执行任务

// 使用默认单例
try {
  const result = await asyncLock(options);
  console.log('执行结果:', result);
} catch (error) {
  if (error.code === 'LOCKED') {
    // 重复执行被拒绝
    console.warn('请勿重复操作');
  } else if (error.code === 'QUEUE_FULL') {
    // 队列已满
    console.error('系统繁忙,请稍后重试');
  } else {
    // 其他错误
    console.error('执行失败:', error);
  }
}

// 使用自定义实例
try {
  const result = await myLockManager.execute(options);
  console.log('执行结果:', result);
} catch (error) {
  // 错误处理
}

锁管理操作

import { 
  asyncLock, 
  releaseLock, 
  releaseAllLocks, 
  cancelLockTask,
  getLockStatus,
  getStats,
  resetStats 
} from './asyncLock';

// 1. 手动释放指定锁
releaseLock('submitForm');

// 2. 释放所有锁
releaseAllLocks();

// 3. 取消正在执行的任务
const cancelled = cancelLockTask('submitForm', '用户主动取消');
if (cancelled) {
  console.log('任务已取消');
}

// 4. 获取锁状态
const status = getLockStatus('submitForm');
console.log('锁状态:', {
  是否锁定: status.locked,
  锁定时长: `${status.age}ms`,
  队列长度: status.queueLength
});

// 5. 获取统计信息
const stats = getStats();
console.log('执行统计:', {
  总执行次数: stats.totalExecutions,
  成功次数: stats.successCount,
  超时次数: stats.timeoutCount,
  当前活跃锁: stats.activeLocks.length
});

// 6. 重置统计
resetStats();

📋 使用案例

案例1:表单提交防重复

/**
 * 场景:表单提交按钮防止用户重复点击
 * 需求:提交期间禁用按钮,防止重复提交,支持取消
 */
class FormSubmitService {
  constructor() {
    this.isSubmitting = false;
  }
  
  async submitForm(formData) {
    if (this.isSubmitting) {
      Toast.warning('正在提交,请稍候...');
      return;
    }
    
    this.isSubmitting = true;
    
    try {
      const result = await asyncLock({
        name: 'formSubmit',
        asyncFn: async (signal) => {
          // 模拟API调用
          const response = await fetch('/api/submit', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(formData),
            signal
          });
          
          if (!response.ok) {
            throw new Error(`提交失败: ${response.status}`);
          }
          
          return await response.json();
        },
        timeout: 10000,
        retryCount: 1,
        baseRetryDelay: 2000,
        repeatTip: '正在提交中,请勿重复点击',
        tipHandler: (msg) => Toast.warning(msg),
        onSuccess: (result) => {
          Toast.success('提交成功!');
          console.log('提交结果:', result);
        },
        onFail: (error) => {
          if (error.code !== 'LOCKED') {
            Toast.error(`提交失败: ${error.message}`);
          }
        }
      });
      
      return result;
    } finally {
      this.isSubmitting = false;
    }
  }
  
  // 用户离开页面时取消提交
  cancelSubmit() {
    cancelLockTask('formSubmit', '用户离开页面');
  }
}

// 使用
const formService = new FormSubmitService();

// 提交表单
submitButton.addEventListener('click', async () => {
  const formData = collectFormData();
  await formService.submitForm(formData);
});

// 页面离开时取消
window.addEventListener('beforeunload', () => {
  formService.cancelSubmit();
});

案例2:支付订单防重复

/**
 * 场景:支付订单防止重复支付
 * 需求:支付期间锁定订单,防止重复支付,支持队列
 */
class PaymentService {
  constructor(orderId) {
    this.orderId = orderId;
    this.lockName = `payment_${orderId}`;
  }
  
  async processPayment(paymentData) {
    try {
      return await asyncLock({
        name: this.lockName,
        asyncFn: async (signal) => {
          // 调用支付接口
          const paymentResult = await this.callPaymentApi(paymentData, signal);
          
          // 更新订单状态
          await this.updateOrderStatus(paymentResult, signal);
          
          return paymentResult;
        },
        timeout: 30000, // 支付操作需要更长时间
        retryCount: 2,
        baseRetryDelay: 3000,
        maxRetryDelay: 15000,
        // 只对网络错误和服务器5xx错误重试
        retryCondition: (error) => {
          const isNetworkError = error.message.includes('Network') || 
                                 error.message.includes('fetch');
          const isServerError = error.message.includes('50') || 
                                error.message.includes('服务不可用');
          return isNetworkError || isServerError;
        },
        enableQueue: true,
        maxQueueSize: 1, // 同一订单只允许一个排队
        repeatTip: '订单支付处理中,请稍候...',
        tipHandler: (msg) => {
          showPaymentStatus(msg);
        },
        onSuccess: (result) => {
          showPaymentSuccess(result);
          trackPaymentEvent('success', this.orderId);
        },
        onFail: (error) => {
          if (error.code === 'LOCKED') {
            // 重复支付被阻止
            trackPaymentEvent('prevented_duplicate', this.orderId);
          } else if (error.code === 'QUEUE_FULL') {
            showPaymentError('订单正在处理,请勿重复操作');
          } else {
            showPaymentError(`支付失败: ${error.message}`);
            trackPaymentEvent('failed', this.orderId, error);
          }
        }
      });
    } catch (error) {
      console.error('支付处理异常:', error);
      throw error;
    }
  }
  
  async callPaymentApi(paymentData, signal) {
    // 模拟支付API调用
    const response = await fetch('/api/payment/process', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        orderId: this.orderId,
        ...paymentData
      }),
      signal
    });
    
    if (!response.ok) {
      throw new Error(`支付API错误: ${response.status}`);
    }
    
    return await response.json();
  }
  
  async updateOrderStatus(paymentResult, signal) {
    // 更新订单状态
    const response = await fetch(`/api/orders/${this.orderId}/status`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        status: 'paid',
        paymentId: paymentResult.paymentId,
        paidAt: new Date().toISOString()
      }),
      signal
    });
    
    if (!response.ok) {
      throw new Error(`订单状态更新失败: ${response.status}`);
    }
  }
  
  // 取消支付
  cancelPayment() {
    const cancelled = cancelLockTask(this.lockName, '用户取消支付');
    if (cancelled) {
      showPaymentStatus('支付已取消');
      trackPaymentEvent('cancelled', this.orderId);
    }
    return cancelled;
  }
}

// 使用
const paymentService = new PaymentService('ORDER_123456');

// 开始支付
paymentButton.addEventListener('click', async () => {
  const paymentData = {
    amount: 100.00,
    method: 'credit_card',
    cardToken: 'tok_123456'
  };
  
  try {
    await paymentService.processPayment(paymentData);
  } catch (error) {
    console.error('支付失败:', error);
  }
});

// 取消支付
cancelButton.addEventListener('click', () => {
  paymentService.cancelPayment();
});

案例3:文件上传队列管理

/**
 * 场景:批量文件上传,需要控制并发和防止重复上传
 * 需求:同一文件不能重复上传,上传任务需要排队
 */
class FileUploadManager {
  constructor() {
    this.uploadQueue = new Map(); // fileId -> upload promise
  }
  
  async uploadFile(file, options = {}) {
    const fileId = this.generateFileId(file);
    const lockName = `upload_${fileId}`;
    
    // 如果已经在队列中,返回已有的Promise
    if (this.uploadQueue.has(fileId)) {
      return this.uploadQueue.get(fileId);
    }
    
    const uploadPromise = asyncLock({
      name: lockName,
      asyncFn: async (signal) => {
        try {
          // 更新UI状态
          this.updateFileStatus(fileId, 'uploading');
          
          // 执行上传
          const result = await this.doUpload(file, signal, options);
          
          // 上传成功
          this.updateFileStatus(fileId, 'success');
          return result;
        } catch (error) {
          // 上传失败
          this.updateFileStatus(fileId, 'error');
          throw error;
        }
      },
      timeout: 5 * 60 * 1000, // 5分钟超时
      retryCount: 3,
      baseRetryDelay: 5000,
      maxRetryDelay: 60000,
      retryCondition: (error) => {
        // 只对网络错误重试
        return error.message.includes('network') || 
               error.message.includes('timeout') ||
               error.message.includes('Network');
      },
      enableQueue: true,
      maxQueueSize: 0, // 同一文件不上传队列
      repeatTip: '文件正在上传中...',
      tipHandler: (msg) => {
        console.log(`文件 ${file.name}: ${msg}`);
      },
      onSuccess: (result) => {
        console.log(`文件 ${file.name} 上传成功:`, result);
        this.uploadQueue.delete(fileId);
      },
      onFail: (error) => {
        console.error(`文件 ${file.name} 上传失败:`, error);
        this.uploadQueue.delete(fileId);
      },
      autoCleanup: false // 手动清理,避免上传完成前锁被清理
    });
    
    // 保存到队列
    this.uploadQueue.set(fileId, uploadPromise);
    
    return uploadPromise;
  }
  
  async doUpload(file, signal, options) {
    const formData = new FormData();
    formData.append('file', file);
    
    // 添加上传进度回调
    const xhr = new XMLHttpRequest();
    
    return new Promise((resolve, reject) => {
      // 监听取消信号
      if (signal.aborted) {
        reject(new Error('上传被取消'));
        return;
      }
      
      const onAbort = () => {
        xhr.abort();
        reject(new Error('上传被取消'));
      };
      
      signal.addEventListener('abort', onAbort);
      
      // 设置上传进度
      xhr.upload.addEventListener('progress', (event) => {
        if (event.lengthComputable) {
          const percent = Math.round((event.loaded / event.total) * 100);
          this.updateUploadProgress(fileId, percent);
        }
      });
      
      // 完成处理
      xhr.addEventListener('load', () => {
        signal.removeEventListener('abort', onAbort);
        
        if (xhr.status >= 200 && xhr.status < 300) {
          resolve(JSON.parse(xhr.responseText));
        } else {
          reject(new Error(`上传失败: ${xhr.status} ${xhr.statusText}`));
        }
      });
      
      xhr.addEventListener('error', () => {
        signal.removeEventListener('abort', onAbort);
        reject(new Error('网络错误,上传失败'));
      });
      
      xhr.addEventListener('abort', () => {
        signal.removeEventListener('abort', onAbort);
        reject(new Error('上传被取消'));
      });
      
      // 开始上传
      xhr.open('POST', '/api/upload');
      xhr.send(formData);
    });
  }
  
  generateFileId(file) {
    // 生成文件唯一ID(实际项目中可能需要更复杂的逻辑)
    return `${file.name}_${file.size}_${file.lastModified}`;
  }
  
  updateFileStatus(fileId, status) {
    // 更新UI显示
    console.log(`文件 ${fileId} 状态: ${status}`);
  }
  
  updateUploadProgress(fileId, percent) {
    // 更新上传进度
    console.log(`文件 ${fileId} 上传进度: ${percent}%`);
  }
  
  // 取消文件上传
  cancelUpload(file) {
    const fileId = this.generateFileId(file);
    const lockName = `upload_${fileId}`;
    
    const cancelled = cancelLockTask(lockName, '用户取消上传');
    if (cancelled) {
      this.uploadQueue.delete(fileId);
      this.updateFileStatus(fileId, 'cancelled');
      console.log(`文件 ${file.name} 上传已取消`);
    }
    
    return cancelled;
  }
  
  // 批量取消所有上传
  cancelAllUploads() {
    releaseAllLocks();
    this.uploadQueue.clear();
    console.log('所有文件上传已取消');
  }
}

// 使用
const uploadManager = new FileUploadManager();

// 上传文件
fileInput.addEventListener('change', async (event) => {
  const files = Array.from(event.target.files);
  
  for (const file of files) {
    try {
      await uploadManager.uploadFile(file);
    } catch (error) {
      console.error(`文件 ${file.name} 上传失败:`, error);
    }
  }
});

// 取消上传
cancelButton.addEventListener('click', () => {
  const file = getSelectedFile();
  uploadManager.cancelUpload(file);
});

案例4:全局配置管理

/**
 * 场景:应用全局配置需要防止并发修改
 * 需求:配置更新需要互斥,多个更新请求需要排队
 */
class ConfigManager {
  constructor() {
    this.config = {};
    this.lockManager = createLockManager({
      timeout: 15000,
      maxLockAge: 2 * 60 * 1000, // 2分钟
      maxQueueSize: 5,
      tipHandler: (msg) => console.log('[ConfigLock]', msg),
      enableStats: true
    });
  }
  
  async updateConfig(key, value, options = {}) {
    const lockName = `config_${key}`;
    
    try {
      const result = await this.lockManager.execute({
        name: lockName,
        asyncFn: async (signal) => {
          // 获取当前配置
          const currentConfig = await this.fetchConfig(key, signal);
          
          // 验证配置
          if (options.validate) {
            const isValid = await options.validate(value, currentConfig, signal);
            if (!isValid) {
              throw new Error('配置验证失败');
            }
          }
          
          // 更新配置
          const updateResult = await this.doUpdateConfig(key, value, signal);
          
          // 更新本地缓存
          this.config[key] = value;
          
          // 触发配置变更事件
          this.emitConfigChange(key, value, currentConfig);
          
          return updateResult;
        },
        timeout: options.timeout || 10000,
        retryCount: options.retryCount || 1,
        baseRetryDelay: 2000,
        retryCondition: (error) => {
          // 只对网络错误重试
          return error.message.includes('network') || 
                 error.message.includes('timeout') ||
                 error.name === 'TypeError'; // fetch错误
        },
        enableQueue: true,
        onSuccess: (result) => {
          console.log(`配置 ${key} 更新成功:`, result);
        },
        onFail: (error) => {
          if (error.code !== 'LOCKED') {
            console.error(`配置 ${key} 更新失败:`, error);
          }
        }
      });
      
      return result;
    } catch (error) {
      console.error(`配置 ${key} 更新异常:`, error);
      throw error;
    }
  }
  
  async fetchConfig(key, signal) {
    const response = await fetch(`/api/config/${key}`, { signal });
    if (!response.ok) {
      throw new Error(`获取配置失败: ${response.status}`);
    }
    return await response.json();
  }
  
  async doUpdateConfig(key, value, signal) {
    const response = await fetch(`/api/config/${key}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ value }),
      signal
    });
    
    if (!response.ok) {
      throw new Error(`更新配置失败: ${response.status}`);
    }
    
    return await response.json();
  }
  
  emitConfigChange(key, newValue, oldValue) {
    // 触发配置变更事件
    const event = new CustomEvent('configChange', {
      detail: { key, newValue, oldValue }
    });
    window.dispatchEvent(event);
  }
  
  // 批量更新配置(多个配置项原子更新)
  async batchUpdateConfig(updates, options = {}) {
    const lockName = 'config_batch_update';
    
    return await this.lockManager.execute({
      name: lockName,
      asyncFn: async (signal) => {
        // 开始事务
        const transactionId = await this.beginTransaction(signal);
        
        try {
          const results = {};
          
          // 依次更新每个配置
          for (const [key, value] of Object.entries(updates)) {
            const result = await this.doUpdateConfig(key, value, signal);
            results[key] = result;
            this.config[key] = value;
          }
          
          // 提交事务
          await this.commitTransaction(transactionId, signal);
          
          // 触发批量变更事件
          this.emitBatchConfigChange(updates);
          
          return results;
        } catch (error) {
          // 回滚事务
          await this.rollbackTransaction(transactionId, signal);
          throw error;
        }
      },
      timeout: 30000, // 批量操作需要更长时间
      retryCount: 0, // 批量操作不重试
      enableQueue: true,
      maxQueueSize: 1 // 批量操作只允许一个排队
    });
  }
  
  async beginTransaction(signal) {
    const response = await fetch('/api/config/transaction/begin', { signal });
    if (!response.ok) {
      throw new Error('开始事务失败');
    }
    const data = await response.json();
    return data.transactionId;
  }
  
  async commitTransaction(transactionId, signal) {
    const response = await fetch('/api/config/transaction/commit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ transactionId }),
      signal
    });
    
    if (!response.ok) {
      throw new Error('提交事务失败');
    }
  }
  
  async rollbackTransaction(transactionId, signal) {
    await fetch('/api/config/transaction/rollback', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ transactionId }),
      signal
    }).catch(() => {
      // 回滚失败也继续,不影响主流程
    });
  }
  
  emitBatchConfigChange(updates) {
    const event = new CustomEvent('configBatchChange', {
      detail: { updates }
    });
    window.dispatchEvent(event);
  }
  
  // 获取锁管理器统计信息(用于监控)
  getLockStats() {
    return this.lockManager.getStats();
  }
  
  // 清理所有配置锁
  cleanupConfigLocks() {
    this.lockManager.releaseAllLocks();
  }
}

// 使用
const configManager = new ConfigManager();

// 更新单个配置
async function updateTheme(theme) {
  try {
    await configManager.updateConfig('theme', theme, {
      validate: async (value, current) => {
        // 验证主题是否有效
        const validThemes = ['light', 'dark', 'auto'];
        return validThemes.includes(value);
      },
      retryCount: 2
    });
  } catch (error) {
    if (error.code === 'LOCKED') {
      console.log('配置正在更新中,请稍后');
    } else {
      console.error('更新主题失败:', error);
    }
  }
}

// 批量更新配置
async function updateUserPreferences(prefs) {
  try {
    const results = await configManager.batchUpdateConfig(prefs);
    console.log('偏好设置更新成功:', results);
  } catch (error) {
    console.error('批量更新失败:', error);
  }
}

// 监控锁状态
setInterval(() => {
  const stats = configManager.getLockStats();
  if (stats.activeLocks.length > 0) {
    console.log('活跃的配置锁:', stats.activeLocks);
  }
}, 60000);

🔧 高级配置

自定义重试策略

// 基于错误类型的智能重试策略
const smartRetryCondition = (error) => {
  // 网络错误:重试
  if (error.name === 'NetworkError' || 
      error.name === 'TypeError' || 
      error.message.includes('network')) {
    return true;
  }
  
  // 服务器5xx错误:重试
  if (error.status >= 500 && error.status < 600) {
    return true;
  }
  
  // 服务器4xx错误(除429外):不重试
  if (error.status >= 400 && error.status < 500 && error.status !== 429) {
    return false;
  }
  
  // 429 Too Many Requests:使用退避重试
  if (error.status === 429) {
    return true;
  }
  
  // 默认情况:不重试
  return false;
};

// 使用自定义重试条件
await asyncLock({
  name: 'apiCall',
  asyncFn: apiCallFunction,
  retryCount: 3,
  retryCondition: smartRetryCondition
});

性能监控集成

// 创建带监控的锁管理器
class MonitoredLockManager extends LockManager {
  constructor(options = {}) {
    super(options);
    this.metrics = {
      lockAcquisitionTime: [],
      taskExecutionTime: [],
      queueWaitTime: []
    };
  }
  
  async execute(options) {
    const startTime = performance.now();
    
    try {
      const result = await super.execute(options);
      
      // 记录执行时间
      const endTime = performance.now();
      const executionTime = endTime - startTime;
      this.metrics.taskExecutionTime.push(executionTime);
      
      // 发送性能指标
      this.sendMetrics({
        name: options.name,
        executionTime,
        success: true
      });
      
      return result;
    } catch (error) {
      const endTime = performance.now();
      const executionTime = endTime - startTime;
      
      // 发送错误指标
      this.sendMetrics({
        name: options.name,
        executionTime,
        success: false,
        errorType: error.type,
        errorCode: error.code
      });
      
      throw error;
    }
  }
  
  sendMetrics(metric) {
    // 发送到监控系统
    console.log('[LockMetrics]', metric);
    
    // 实际项目中可以发送到 APM 系统
    // sendToAPM('lock_execution', metric);
  }
  
  getPerformanceMetrics() {
    const calculateStats = (array) => {
      if (array.length === 0) return null;
      
      const sum = array.reduce((a, b) => a + b, 0);
      const avg = sum / array.length;
      const max = Math.max(...array);
      const min = Math.min(...array);
      
      return { count: array.length, avg, min, max, sum };
    };
    
    return {
      taskExecution: calculateStats(this.metrics.taskExecutionTime),
      lockAcquisition: calculateStats(this.metrics.lockAcquisitionTime),
      queueWait: calculateStats(this.metrics.queueWaitTime)
    };
  }
}

// 使用带监控的锁管理器
const monitoredManager = new MonitoredLockManager();

// 定期打印性能指标
setInterval(() => {
  const metrics = monitoredManager.getPerformanceMetrics();
  console.log('锁管理器性能指标:', metrics);
}, 60000);

📊 性能建议

最佳实践

  1. 合理设置超时时间
    • 快速操作:1-5秒
    • 普通操作:5-10秒
    • 长时间操作:10-30秒
    • 文件上传等:1-5分钟
  1. 队列配置建议
    • 关键操作:队列大小 1(确保严格顺序)
    • 普通操作:队列大小 3-5
    • 批量操作:队列大小 10-20
    • 注意:队列越大,内存占用越高
  1. 重试策略建议
    • 网络请求:重试2-3次,基础延迟1-3秒
    • 支付操作:重试1-2次,基础延迟2-5秒
    • 文件操作:重试0-1次,基础延迟5-10秒
  1. 内存管理
    • 定期检查锁状态,避免内存泄漏
    • 页面卸载时调用 destroy() 清理资源
    • 监控队列长度,避免无限增长
  1. 错误处理
    • 区分用户取消和系统错误
    • 对不同的错误类型采取不同的处理策略
    • 记录详细的错误日志以便排查

🐛 常见问题

Q1: 锁会自动释放吗?

A: 是的。锁会在以下情况下自动释放:

  • 任务执行完成(成功或失败)
  • 任务超时
  • 锁过期(超过 maxLockAge 配置)
  • 手动调用 releaseLock() 或 releaseAllLocks()

Q2: 队列中的任务会按顺序执行吗?

A: 是的。队列采用 FIFO(先进先出)原则,任务会按照加入队列的顺序依次执行。

Q3: 如何防止内存泄漏?

A: 锁管理器内置以下防护措施:

  1. 定期清理过期锁(默认60秒一次)
  2. 队列项超时自动清理
  3. 页面卸载时可以调用 destroy() 方法
  4. 所有定时器和事件监听器都有清理逻辑

Q4: 支持分布式环境吗?

A: 当前版本是单机内存锁,适用于单页面应用或单服务器环境。如果需要分布式锁,可以基于此模式扩展,使用 Redis 或其他分布式存储作为锁存储后端。

Q5: 如何监控锁管理器的状态?

A: 可以通过以下方式监控:

  1. 使用 getLockStatus(name) 获取特定锁状态
  2. 使用 getStats() 获取全局统计信息
  3. 继承 LockManager 类添加自定义监控
  4. 监听相关事件(需要自行扩展事件系统)

📈 扩展建议

如果未来需要扩展功能,可以考虑:

  1. 分布式锁支持:集成 Redis 或其他分布式存储
  2. 锁优先级:为队列中的任务添加优先级
  3. 锁续期机制:长时间任务自动续期
  4. 事件系统:锁状态变化时触发事件
  5. 浏览器存储持久化:页面刷新后恢复锁状态
  6. 更复杂的队列算法:支持优先级队列、延迟队列等

📝 总结

LockManager 是一个功能全面、设计优雅的异步任务互斥锁工具,它解决了传统防抖节流方案的诸多痛点,特别适合以下场景:

  • ✅ 表单提交:防止重复提交
  • ✅ 支付操作:防止重复支付
  • ✅ 文件上传:同一文件不上传多次
  • ✅ 配置更新:防止并发修改配置
  • ✅ 关键操作:需要严格顺序执行的操作
  • ✅ 资源竞争:多组件共享资源时的并发控制

通过合理使用这个工具,可以显著提升应用的数据一致性和用户体验,避免因并发操作导致的业务逻辑错误。


📄 完整代码

  1. 默认单例 (asyncLock):适合大多数场景
  2. 自定义实例 (createLockManager):需要不同配置时使用
  3. 类直接使用 (LockManager):需要继承扩展时使用

工具已经过精心设计和测试,可以直接在生产环境中使用。

/**
 * 异步任务互斥锁工具
 * 需求:防抖节流不能防止api接口长时间未返回。如果用户等待一小段时候后重新点击提交,会导致重新触发请求;
 * 解决思路:用互斥锁思路处理异步任务锁定,通过name进行异步任务锁定,防止重入。
 * 核心能力:防止异步任务未完成时重复执行、超时控制、任务取消、资源自动清理
 * 支持:队列机制、指数退避重试、原子操作、错误分类、性能监控
 */
class LockManager {
  constructor(options = {}) {
    // 存储所有锁状态
    this._lockMap = new Map();
    
    // 等待队列
    this._queueMap = new Map();
    
    // 默认配置
    this._defaults = {
      timeout: 10000,
      repeatTip: '操作中,请稍后...',
      throwRepeatError: true,
      autoCleanup: true,
      maxLockAge: 5 * 60 * 1000,
      maxQueueSize: 100,
      enableStats: true,
      tipHandler: () => {}, 
      ...options
    };
    
    // 统计信息
    this._stats = {
      totalExecutions: 0,
      successCount: 0,
      timeoutCount: 0,
      cancelCount: 0,
      repeatRejectCount: 0,
      queueFullCount: 0,
      retryCount: 0
    };
    
    // 定期清理过期锁和队列
    this._cleanupInterval = setInterval(() => this._cleanupExpiredLocks(), 60000);
    
    // 绑定方法,确保在回调中使用正确的this
    this._processNextInQueue = this._processNextInQueue.bind(this);
  }

  /**
   * 原子性地获取锁
   */
  _acquireLock(name) {
    const attemptId = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
    const now = Date.now();
    
    // 第一重检查
    const existing = this._lockMap.get(name);
    if (existing?.locked) {
      return null;
    }
    
    // 创建新的锁对象
    const lockItem = {
      locked: true,
      abortController: new AbortController(),
      timeoutTimer: null,
      createdAt: now,
      taskId: `${name}_${now}_${Math.random().toString(36).slice(2, 10)}`,
      attemptId: attemptId,
      waitingQueue: this._queueMap.get(name) || []
    };
    
    // 第二重检查(原子性保障)
    const current = this._lockMap.get(name);
    if (current?.locked) {
      return null;
    }
    
    // 设置锁(原子操作)
    this._lockMap.set(name, lockItem);
    
    // 最终验证
    const afterSet = this._lockMap.get(name);
    if (afterSet?.attemptId !== attemptId) {
      lockItem.abortController.abort();
      return null;
    }
    
    return lockItem;
  }

  /**
   * 将任务加入等待队列
   */
  _addToQueue(options) {
    const { name, maxQueueSize = this._defaults.maxQueueSize } = options;
    
    let queue = this._queueMap.get(name);
    if (!queue) {
      queue = [];
      this._queueMap.set(name, queue);
    }
    
    if (queue.length >= maxQueueSize) {
      this._stats.queueFullCount++;
      const error = new Error(`任务队列【${name}】已满(最大${maxQueueSize})`);
      error.type = 'queue_full';
      error.code = 'QUEUE_FULL';
      return Promise.reject(error);
    }
    
    return new Promise((resolve, reject) => {
      const queueItem = {
        options,
        resolve,
        reject,
        enqueuedAt: Date.now()
      };
      
      queue.push(queueItem);
      
      if (options.timeout > 0) {
        queueItem.timeoutTimer = setTimeout(() => {
          const index = queue.indexOf(queueItem);
          if (index > -1) {
            queue.splice(index, 1);
            const error = new Error(`任务【${name}】在队列中等待超时`);
            error.type = 'queue_timeout';
            error.code = 'QUEUE_TIMEOUT';
            reject(error);
            
            if (queue.length === 0) {
              this._queueMap.delete(name);
            }
          }
        }, options.timeout);
      }
    });
  }

  /**
   * 处理队列中的下一个任务(使用微任务)
   */
  async _processNextInQueue(name) {
    const queue = this._queueMap.get(name);
    if (!queue || queue.length === 0) {
      this._queueMap.delete(name);
      return;
    }
    
    // 使用微任务处理,避免 setTimeout 的延迟
    await Promise.resolve();
    
    const queueItem = queue.shift();
    
    if (queueItem.timeoutTimer) {
      clearTimeout(queueItem.timeoutTimer);
    }
    
    try {
      const result = await this._executeTask(queueItem.options);
      queueItem.resolve(result);
    } catch (error) {
      queueItem.reject(error);
    } finally {
      // 继续处理下一个(递归)
      if (queue.length > 0) {
        // 再次使用微任务
        Promise.resolve().then(() => this._processNextInQueue(name));
      } else {
        this._queueMap.delete(name);
      }
    }
  }

  /**
   * 执行任务核心逻辑
   */
  async _executeTask(options) {
    const {
      name,
      asyncFn,
      timeout = this._defaults.timeout,
      retryCount = 0,
      baseRetryDelay = 1000,
      maxRetryDelay = 30000,
      retryCondition = null // 自定义重试条件函数
    } = options;
    
    const lockItem = this._acquireLock(name);
    if (!lockItem) {
      const error = new Error(`无法获取锁【${name}】`);
      error.type = 'lock_failed';
      error.code = 'LOCK_FAILED';
      throw error;
    }
    
    let result;
    try {
      if (timeout > 0) {
        lockItem.timeoutTimer = setTimeout(() => {
          const timeoutError = new Error(`任务【${name}】超时(${timeout}ms)`);
          timeoutError.type = 'timeout';
          timeoutError.code = 'TIMEOUT';
          lockItem.abortController.abort(timeoutError);
        }, timeout);
      }
      
      result = await this._executeWithExponentialBackoff(
        () => asyncFn(lockItem.abortController.signal),
        retryCount,
        baseRetryDelay,
        maxRetryDelay,
        lockItem.abortController,
        retryCondition // 传递重试条件
      );
      
      return result;
      
    } catch (error) {
      error.lockName = name;
      error.taskId = lockItem.taskId;
      throw error;
      
    } finally {
      this._cleanupLock(name, lockItem, options.autoCleanup ?? this._defaults.autoCleanup);
      
      // 使用微任务处理下一个队列任务
      Promise.resolve().then(() => this._processNextInQueue(name));
    }
  }

  /**
   * 判断是否应该重试(支持自定义重试条件)
   */
  _shouldRetry(error, retryCondition) {
    // 不重试的错误类型
    const noRetryTypes = ['cancel', 'timeout', 'queue_full', 'queue_timeout', 'lock_failed'];
    if (noRetryTypes.includes(error.type)) {
      return false;
    }
    
    // 如果提供了自定义重试条件函数,使用它
    if (typeof retryCondition === 'function') {
      return retryCondition(error);
    }
    
    // 默认重试条件:非特定错误都重试
    return true;
  }

  /**
   * 指数退避重试执行(支持自定义重试条件)
   */
  async _executeWithExponentialBackoff(fn, maxRetries, baseDelay, maxDelay, abortController, retryCondition) {
    let lastError;
    
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        if (abortController.signal.aborted) {
          const cancelError = new Error('任务已被取消');
          cancelError.type = 'cancel';
          cancelError.code = 'CANCELLED';
          throw cancelError;
        }
        
        if (attempt > 0) {
          const delay = this._calculateExponentialBackoffDelay(
            attempt,
            baseDelay,
            maxDelay
          );
          
          this._stats.retryCount++;
          console.log(`任务重试第${attempt}次,延迟${delay}ms`);
          
          await this._sleep(delay, abortController.signal);
        }
        
        return await fn();
        
      } catch (error) {
        lastError = error;
        
        // 使用统一的判断逻辑决定是否重试
        if (!this._shouldRetry(error, retryCondition)) {
          throw error;
        }
        
        if (attempt === maxRetries) {
          error.retryAttempts = attempt;
          throw error;
        }
      }
    }
    
    throw lastError;
  }

  /**
   * 计算指数退避延迟
   */
  _calculateExponentialBackoffDelay(attempt, baseDelay, maxDelay) {
    const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
    const jitter = exponentialDelay * 0.1 * Math.random();
    return Math.min(exponentialDelay + jitter, maxDelay);
  }

  /**
   * 可中断的延时(安全的事件监听清理)
   */
  _sleep(ms, signal) {
    return new Promise((resolve, reject) => {
      if (signal.aborted) {
        reject(new Error('等待被中断'));
        return;
      }
      
      const timer = setTimeout(() => {
        // 清理事件监听
        signal.removeEventListener('abort', abortHandler);
        resolve();
      }, ms);
      
      const abortHandler = () => {
        clearTimeout(timer);
        const error = new Error('等待被中断');
        error.type = 'cancel';
        error.code = 'SLEEP_CANCELLED';
        reject(error);
      };
      
      signal.addEventListener('abort', abortHandler);
      
      // 确保在Promise settled后清理
      const cleanup = () => {
        clearTimeout(timer);
        signal.removeEventListener('abort', abortHandler);
      };
      
      // 无论成功还是失败都执行清理
      this._safeFinally(() => {
        cleanup();
      }, resolve, reject);
    });
  }

  /**
   * 安全的finally执行,避免影响原始Promise
   */
  _safeFinally(cleanupFn, resolve, reject) {
    const wrappedResolve = (value) => {
      try {
        cleanupFn();
      } finally {
        resolve(value);
      }
    };
    
    const wrappedReject = (error) => {
      try {
        cleanupFn();
      } finally {
        reject(error);
      }
    };
    
    return { resolve: wrappedResolve, reject: wrappedReject };
  }

  /**
   * 清理锁资源
   */
  _cleanupLock(name, lockItem, autoCleanup) {
    if (lockItem.timeoutTimer) {
      clearTimeout(lockItem.timeoutTimer);
      lockItem.timeoutTimer = null;
    }
    
    if (lockItem.abortController) {
      lockItem.abortController = null;
    }
    
    if (autoCleanup) {
      this._lockMap.delete(name);
    } else {
      lockItem.locked = false;
      lockItem.abortController = null;
      lockItem.timeoutTimer = null;
    }
  }

  /**
   * 清理过期锁和队列
   */
  _cleanupExpiredLocks() {
    const now = Date.now();
    const maxAge = this._defaults.maxLockAge;
    
    // 清理过期锁
    for (const [name, lockItem] of this._lockMap.entries()) {
      if (lockItem.locked && (now - lockItem.createdAt) > maxAge) {
        console.warn(`清理过期锁【${name}】,已锁定${now - lockItem.createdAt}ms`);
        
        const error = new Error('锁过期自动清理');
        error.type = 'timeout';
        error.code = 'LOCK_EXPIRED';
        
        if (lockItem.abortController) {
          lockItem.abortController.abort(error);
        }
        
        this._lockMap.delete(name);
      }
    }
    
    // 清理过期队列项
    for (const [name, queue] of this._queueMap.entries()) {
      const expiredItems = [];
      
      for (let i = 0; i < queue.length; i++) {
        const item = queue[i];
        const queueAge = now - item.enqueuedAt;
        const timeout = item.options?.timeout || 30000;
        if (queueAge > timeout) {
          expiredItems.push(i);
        }
      }
      
      for (let i = expiredItems.length - 1; i >= 0; i--) {
        const index = expiredItems[i];
        const item = queue[index];
        
        if (item.timeoutTimer) {
          clearTimeout(item.timeoutTimer);
        }
        
        const error = new Error(`任务【${name}】在队列中过期`);
        error.type = 'queue_timeout';
        error.code = 'QUEUE_TIMEOUT';
        item.reject(error);
        
        queue.splice(index, 1);
      }
      
      if (queue.length === 0) {
        this._queueMap.delete(name);
      }
    }
  }

  /**
   * 执行带锁的异步任务
   */
  async execute(options) {
    const {
      name,
      asyncFn,
      onSuccess,
      onFail,
      repeatTip = this._defaults.repeatTip,
      timeout = this._defaults.timeout,
      throwRepeatError = this._defaults.throwRepeatError,
      tipHandler = this._defaults.tipHandler, // 使用配置的默认值
      enableQueue = false,
      maxQueueSize = this._defaults.maxQueueSize,
      retryCount = 0,
      baseRetryDelay = 1000,
      maxRetryDelay = 30000,
      retryCondition = null, // 自定义重试条件
      autoCleanup = this._defaults.autoCleanup
    } = options;

    this._stats.totalExecutions++;

    try {
      const existingLock = this._lockMap.get(name);
      if (existingLock?.locked) {
        this._stats.repeatRejectCount++;
        
        const repeatError = new Error(repeatTip);
        repeatError.type = 'repeat';
        repeatError.code = 'LOCKED';
        repeatError.lockName = name;
        
        tipHandler(repeatTip);
        
        if (enableQueue) {
          console.log(`任务【${name}】加入等待队列,当前队列长度:${this._queueMap.get(name)?.length || 0}`);
          
          const queueOptions = {
            ...options,
            enableQueue: false,
            maxQueueSize: undefined
          };
          
          const queueResult = await this._addToQueue({
            ...queueOptions,
            name,
            maxQueueSize
          });
          
          onSuccess?.(queueResult);
          return queueResult;
        } else {
          onFail?.(repeatError);
          if (throwRepeatError) throw repeatError;
          return Promise.reject(repeatError);
        }
      }

      const result = await this._executeTask({
        name,
        asyncFn,
        timeout,
        retryCount,
        baseRetryDelay,
        maxRetryDelay,
        retryCondition, // 传递重试条件
        autoCleanup
      });

      this._stats.successCount++;
      onSuccess?.(result);
      return result;
      
    } catch (error) {
      switch (error.type) {
        case 'timeout':
          this._stats.timeoutCount++;
          break;
        case 'cancel':
          this._stats.cancelCount++;
          break;
        case 'queue_full':
          this._stats.queueFullCount++;
          break;
      }
      
      onFail?.(error);
      throw error;
    }
  }

  /**
   * 手动释放指定锁
   */
  releaseLock(name) {
    const lockItem = this._lockMap.get(name);
    if (lockItem) {
      this._cleanupLock(name, lockItem, true);
    }
    
    const queue = this._queueMap.get(name);
    if (queue) {
      queue.forEach(item => {
        if (item.timeoutTimer) {
          clearTimeout(item.timeoutTimer);
        }
        const error = new Error('锁被手动释放,队列任务取消');
        error.type = 'cancel';
        error.code = 'MANUAL_RELEASE';
        item.reject(error);
      });
      this._queueMap.delete(name);
    }
  }

  /**
   * 批量释放所有锁
   */
  releaseAllLocks() {
    this._lockMap.forEach((lockItem, name) => {
      this._cleanupLock(name, lockItem, true);
    });
    this._lockMap.clear();
    
    this._queueMap.forEach((queue, name) => {
      queue.forEach(item => {
        if (item.timeoutTimer) {
          clearTimeout(item.timeoutTimer);
        }
        const error = new Error('所有锁被释放,队列任务取消');
        error.type = 'cancel';
        error.code = 'ALL_RELEASED';
        item.reject(error);
      });
    });
    this._queueMap.clear();
  }

  /**
   * 取消正在执行的任务
   */
  cancelLockTask(name, reason = "用户主动取消") {
    const lockItem = this._lockMap.get(name);
    if (lockItem?.locked && lockItem.abortController) {
      const error = new Error(reason);
      error.type = 'cancel';
      error.code = 'USER_CANCEL';
      lockItem.abortController.abort(error);
      this._cleanupLock(name, lockItem, true);
      return true;
    }
    return false;
  }

  /**
   * 获取指定任务的锁状态
   */
  getLockStatus(name) {
    const lockItem = this._lockMap.get(name);
    const queue = this._queueMap.get(name);
    
    return {
      locked: lockItem?.locked ?? false,
      taskId: lockItem?.taskId,
      createdAt: lockItem?.createdAt,
      age: lockItem ? Date.now() - lockItem.createdAt : 0,
      hasAbortController: !!lockItem?.abortController,
      queueLength: queue?.length || 0,
      queueWaitTimes: queue?.map(item => Date.now() - item.enqueuedAt) || []
    };
  }

  /**
   * 获取统计信息
   */
  getStats() {
    return {
      ...this._stats,
      activeLocks: Array.from(this._lockMap.entries())
        .filter(([_, lock]) => lock.locked)
        .map(([name, lock]) => ({
          name,
          age: Date.now() - lock.createdAt,
          taskId: lock.taskId
        })),
      waitingQueues: Array.from(this._queueMap.entries())
        .map(([name, queue]) => ({
          name,
          length: queue.length,
          oldestWait: queue.length > 0 ? Date.now() - queue[0].enqueuedAt : 0
        }))
    };
  }

  /**
   * 重置统计信息
   */
  resetStats() {
    this._stats = {
      totalExecutions: 0,
      successCount: 0,
      timeoutCount: 0,
      cancelCount: 0,
      repeatRejectCount: 0,
      queueFullCount: 0,
      retryCount: 0
    };
  }

  /**
   * 销毁实例
   */
  destroy() {
    clearInterval(this._cleanupInterval);
    this.releaseAllLocks();
    this._queueMap.clear();
    this._lockMap.clear();
  }
}

// 创建锁管理器的工厂函数
export const createLockManager = (options) => new LockManager(options);

// 默认单例(无默认控制台警告)
export const defaultLockManager = new LockManager({
  tipHandler: () => {} // 明确指定空函数
});

// 带控制台警告的单例(如果需要)
export const verboseLockManager = new LockManager({
  tipHandler: console.warn
});

// 核心方法导出(使用默认单例)
export const asyncLock = (options) => defaultLockManager.execute(options);
export const releaseLock = (name) => defaultLockManager.releaseLock(name);
export const releaseAllLocks = () => defaultLockManager.releaseAllLocks();
export const cancelLockTask = (name, reason) => defaultLockManager.cancelLockTask(name, reason);
export const getLockStatus = (name) => defaultLockManager.getLockStatus(name);
export const getStats = () => defaultLockManager.getStats();
export const resetStats = () => defaultLockManager.resetStats();
export const destroyLockManager = () => defaultLockManager.destroy();

// 导出类本身
export { LockManager };

/*********************************************************************
 * 使用示例
 *********************************************************************/

/*
// 示例1:基础使用(无控制台警告)
import { asyncLock } from './asyncLock';

const submitForm = async () => {
  try {
    const result = await asyncLock({
      name: 'formSubmit',
      asyncFn: async (signal) => {
        if (signal.aborted) throw new Error('任务已被取消');
        return await api.submit(data);
      },
      timeout: 8000,
      retryCount: 2,
      baseRetryDelay: 1000,
      maxRetryDelay: 10000,
      onSuccess: (res) => console.log('提交成功:', res),
      tipHandler: (msg) => console.warn(msg) // 需要时才传入
    });
  } catch (err) {
    console.error('捕获到错误:', err);
  }
};

// 示例2:自定义重试条件
const fetchWithRetry = async () => {
  try {
    const result = await asyncLock({
      name: 'fetchData',
      asyncFn: async (signal) => {
        const response = await fetch('/api/data', { signal });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return await response.json();
      },
      retryCount: 3,
      retryCondition: (error) => {
        // 只对网络错误和5xx错误重试
        return error.message.includes('Failed to fetch') || 
               error.message.includes('HTTP 5');
      },
      onFail: (error) => {
        if (!error.message.includes('HTTP 4')) {
          console.error('需要重试的错误:', error);
        }
      }
    });
  } catch (err) {
    console.error('最终失败:', err);
  }
};

// 示例3:队列处理
const processWithQueue = async () => {
  try {
    const result = await asyncLock({
      name: 'heavyProcess',
      asyncFn: async (signal) => {
        // 耗时处理
        return await heavyProcessing();
      },
      enableQueue: true,
      maxQueueSize: 10,
      timeout: 30000,
      onSuccess: (res) => {
        console.log('处理完成,结果:', res);
      }
    });
  } catch (err) {
    if (err.code === 'QUEUE_FULL') {
      alert('系统繁忙,请稍后重试');
    }
  }
};

// 示例4:使用verbose版本(需要控制台警告)
import { verboseLockManager } from './asyncLock';

const verboseTask = async () => {
  const result = await verboseLockManager.execute({
    name: 'verboseTask',
    asyncFn: async () => {
      // 任务逻辑
    },
    // 会自动输出控制台警告
  });
};

// 示例5:多个锁管理器实例(隔离环境)
import { createLockManager } from './asyncLock';

const userLockManager = createLockManager({
  maxQueueSize: 5,
  tipHandler: (msg) => Toast.warning(msg)
});

const systemLockManager = createLockManager({
  timeout: 30000,
  tipHandler: console.error
});

// 分别使用
const userTask = async () => {
  await userLockManager.execute({
    name: 'userAction',
    asyncFn: userAction
  });
};

const systemTask = async () => {
  await systemLockManager.execute({
    name: 'systemTask',
    asyncFn: systemTask
  });
};
*/

【基础篇007】GeoGebra工具系列_多边形(Polygon)

2025年12月13日 13:18

@心有矩,行有方;不逾界,自成章。——八荒启

GeoGebra工具系列_多边形(Polygon) @TOC

一. 🚀 引言

1. 背景

在传统教学中,画多边形只是个绘图动作。但在 GeoGebra 中,使用 “多边形”工具(或“向量多边形”工具)的瞬间,你创造的是一个活的数学对象。比如,如果你用的是两点定义多边形,那么这两个点就决定了多边形的长、宽、面积、周长;多边形绘制好后会立即在“代数区”同步生成其方程、所有顶点坐标、几何属性,方便关联其他元素;你可以随时拖动任何一个顶点或边,整个多边形及其所有关联数据(面积、对角线长度…)都会实时、连续地变化。那么本章,我们将从交互的角度一起研究一下多边形工具。

文章路径 公众号:八荒启-交互动画 / 创作中心 / 系列教程 / Geogebra从入门到编程全集 / 基础篇
作者 酷酷的脸脸
官方网址 八荒启-交互动画
更新日期 2025.12.13
资源下载 文章配套文件包,公众号内回复“GGB007B”(注意不要换行)

2. 场景

八荒启专精于制作交互动画,比如GGB、Canvas、H5、Unity,本套GGB系列文章主要是以交互动画为大背景,逐步展开具体知识点的讲解。(官网:八荒启-交互动画) 在这里插入图片描述

二、🛠️GeoGebra工具系列_多边形(Polygon)

1. 基石—重新认识“多边形”工具

核心问题:传统的画多边形与GeoGebra里的“画多边形”,本质区别是什么?

(1)多边形不只是图形

对于传统意义上的多边形绘制,主要突出的特点是:静态图形、纯视觉,比如在纸上画一个多边形,更多是一个描绘动作:

  • 它的边看似平行,但不一定真的平行。
  • 它的角看似直角,但精确度依赖画图工具。
  • 画完之后,它就凝固在那里,无法改变。
  • 学生看到的只是“像个多边形的图形”,而不是一个真正带数学性质的对象。

换句话来说:

传统课堂里,多边形是“图像”。你只能看,不能动,也无法从中提取更多数学信息。

但GeoGebra多边形:一个具有“数学生命”的动态对象

GeoGebra中绘制多边形非常简单,只需要激活多边形工具,然后在画布上点击点即可: polygon工具的使用

GGB的基础操作这里就不过多赘述了,摸索一下就好,都非常简单。

所以在这里,多边形就不只是一个图形了: 它自动具备:

  • 顶点坐标(可实时更新)
  • 边长(随拖动自动计算)
  • 角度(保持直角关系)
  • 平行、垂直等约束(软件自动维持)
  • 面积、周长等属性(动态显示)
  • 拖动后仍保持多边形本质(软件保证性质不被破坏)
在 GeoGebra 里,多边形是“数学对象”。你可以拖、可以测、可以变,它始终保持多边形的数学定义。

这里我汇总一下:

维度 传统画法(纸笔) GeoGebra多边形
本质 静态图形 动态数学对象
精确度 依赖手、尺子、绘图技巧 自动保证精确(平行、垂直、直角)
可操作性 画完即固定,无法改变 可拖动、可变形但保持多边形性质
信息可见性 只能看到轮廓 坐标、边长、角度、面积等实时显示
数学关系 需要人为推理或标注 系统自动维护内部数学关系
探索性 很弱,无法实验 很强,可用于观察、猜想、验证定理
教学价值 展示为主 探究为主,可视化数学思想
(2)动态特性展示

如果我们有一个多边形,然后已经显示出了它的周长、面积、内角和,当我们拖动某个点或者某个边的时候当前多边形的性质是这样的: 动态四边形(V1.0) 注意:

Parameter(q1)  // 获取多边形的周长
Area(q1)  // 获取多边形的面积

看到这里是不是已经可以重新认识了多边形,对的,我们可以让学生逐步理解:几何图形是可以“呼吸”的数学对象,而不是死板的图。

如果说前面讲的点、线等都是一些基础操作,那么从这里开始,互动性就真正开始跃然纸上,比如当前的这个动画,学生通过拖动能看到:

  • 改动一个点,为什么好像整个形状都被“牵动”?
  • 面积为何突然变大?
  • 哪些角的变化与哪些边有关?
  • 周长和面积的变化有没有同步关系?

这种体验,比静止的图形更能帮助学生理解:

图形是由一系列约束与关系共同构成的系统,不只是几条线围起来的“形状”。(配套资源见文章头部表格)

(3)数学对象思维

在 GeoGebra 中,用工具创建的多边形(比如刚才提到的多边形)不只是由几条线围成的图,而是一个具有结构、约束和属性的数学对象。这种“对象思维”是传统纸笔几何较难培养的,但在动态几何环境里可以自然生长。 黑板上的多边形 比如在传统几何课中,学生画一个四边形,往往关注的是:边画直了吗?看起来像不像?有没有闭合?这是典型的“图像思维”。

但在 GeoGebra 中,多边形是由一系列点与点之间的关系定义的,顶点是可以移动的对象,边是由顶点实时决定的线段 ,内角与面积是可计算的量,形状随拖动而变动,但结构关系保持,学生逐渐不再“画图”,而是在操控一个由数学约束构成的系统。

为了方便观察,我这里把动态几何下的“观察 → 猜想 → 验证”科学思维与实验性数学整理成一个表格:

阶段 学生活动 示例问题 目标与体验
观察(Observe) 拖动四边形顶点,测量边长、角度、面积 “四边形的内角和是不是总是 360°?” 直接得到现象,理解图形属性随操作变化
猜想(Hypothesize) 对观察结果提出规律或假设 “如果我把一个角拉成凹角,面积为什么变小?”
“对边的关系会不会一直保持?”
培养预测能力与逻辑思维,形成数学假设
验证(Verify) 通过动态拖动、测量、观察来检验猜想 “是不是所有四边形都能分成两个三角形?” 通过实验验证规律,理解数学对象的稳定性与约束关系

重新认识了一下“多边形”工具,你是否有什么收获呢?这里我没有大篇幅讲解如何操作,但是从交互的角度讲解了一下GeoGebra中“多边形”工具的独特含义。最后,我汇总一下数学作为可实验的对象的表现与意义

特性 表现与意义
可试探 学生可以自由改变图形顶点位置,探索不同形状的性质
可调整 图形属性随操作变化,帮助学生发现规律的条件
可实验 通过动态拖动和测量,进行“数学实验”,验证或反驳假设
可验证 数学性质可以被重复测试和观察,不依赖记忆
可反驳 错误假设可以通过操作立即发现,培养批判性思维
数学不是死知识,而是一套可操控、可验证、可推理的结构系统。多边形是培养“数学对象思维”的最佳入口。
——八荒启

2. 解密—工具背后的代数世界

核心问题:当我拖动多边形时,代数区里发生了什么?

在 GeoGebra 中,拖动并不是“随便动一动图形”,而是一次几何操作驱动代数系统实时重算的过程。理解这一点,是从“会用工具”走向“理解工具”的关键一步。

(1)坐标与约束关系

当你用“多边形工具”创建一个多边形时,GeoGebra 首先做的不是画边,而是定义点。比如我们重新打开一个GGB界面,然后用多边形工具创建一个多边形:

在这里插入图片描述 左边代数区可以发现不是只有一句指令,而是有七行:

A=(-6.0965,6.74676)
B=(-10.84531,2.42577)
C=(-3.35845,1.69848)
t1=Polygon(A,B,C)
a=Segment(B,C,t1)
b=Segment(C,A,t1)
c=Segment(A,B,t1)

仔细观察发现,前三行是点的定义,接着是多边形的定义,最后是三个线段的定义,这个就很好理解了:

当你用“多边形工具”创建一个多边形时,GeoGebra 首先做的不是画边,而是定义点。每一个顶点本质上都是一个有序数对:

A(x1,y1),B(x2,y2),C(x3,y3),A(x1,y1),B(x2,y2),C(x3,y3),…

如果我们拖动其中一个顶点,会发现: 请添加图片描述

对应点的坐标在不停的改变,多边形的面积数值在变化(Polygon指令默认返回的是多边形的面积),多边形对应的边也在发生变化。

汇总一下,当你拖动某一个顶点时:

  • 该点的坐标立即发生变化
  • 与它相连的边随之重算
  • 整个多边形的结构被重新计算

虽然学生看到的只是“图形在动”,而代数区中发生的是:一组变量正在被实时更新,这正是GGB创作的灵魂思路。比如我们可以将这些变化的量作为参数去创建其他图形,就可以实现联动效果,这也正是代数约束在后台持续起作用的结果。

(2)即时反馈机制

GeoGebra 的强大之处在于:代数不是事后计算,而是实时反馈。

当我们拖动多边形的一个顶点时:

  • 每条边的长度自动更新(多边形形成的时候软件就会自动创建好)
  • 各个内角大小重新计算(需要使用Angle指令提取)
  • 周长与面积立即刷新(面积已经有了,周长需要用Perimeter指令提取)
  • 对角线同步变化(需要构造,然后提取长度等信息)

慢慢的你会发现,GGB真正强大的功能不是工具栏,恰恰相反,工具栏只占GGB全部功能的2%,而代数区占了全部功能的28%(剩下的70%是GGB的指令脚本系统)

经过前面几篇的学习和了解,恭喜你逐渐入门GGB,那么这里我总结一下GGB创作的核心思路:

代数区是动画“看不见的引擎”,代数区中不断变化的数值,实际上是多边形内部属性的可视化呈现。
主要体现为:
1. 数值的变化 ↔ 图形形状的变化
2. 数值的稳定 ↔ 图形性质的不变

这也是几何画板根本无法和GGB比较的原因,从软件的设计初衷来看,GGB领先于几何画板好几个时代。

当然,学生也可以意识到一点:几何图形的每一次变化,都有代数层面的对应。

(3)从操作到理解

拖动如果只是“好玩”,意义有限,但当拖动与思考结合,就变成了理解的通道。 多边形工具绘制松树(无约束) 多边形工具的使用,已经可以实现很多教育效果,比如通过拖动观察“哪些变,哪些不变”,教师可以引导学生反复拖动多边形,并引导思考:

  • 顶点坐标变了吗?(变)
  • 边长和角度变了吗?(通常变)
  • 多边形的边数变了吗?(不变)
  • 内角和是否保持不变?(多边形内角和的计算)

这种对比帮助学生区分,变量 vs 不变量,是数学思维中极其重要的一步。(当然,创作GGB的话数学思维必不可少)

当然这里只是一个简单的例子,如果我们对这个松树进行一定的关系约束,比如这种:

多边形工具绘制松树(有约束) 那就可以从几何中的“拖动”,逐渐理解到代数中的“变量变化”,再到几何中的“性质保持”,以及代数中的“关系约束”。(当前演示作品配套文件见文章首部表格,制作不复杂,可以参考)

当你能想到图形之所以没变形,是因为某些代数关系一直成立,说明就已经开始用数学对象的语言在思考问题了。

3. 融合—跨领域的连接与应用

核心问题:多边形工具能用在数学之外的场景吗?

在现实世界中,我们几乎看不到“完美的圆”,却到处都是由直线围成的区域。从设计图纸到工程结构,从城市街区到生活空间,多边形是描述现实世界最自然、最常用的数学语言之一。GeoGebra 的多边形工具,正好为这种“现实—数学”的连接提供了桥梁。

比如这个踏板,就是多边形工具最经典的使用: 多边形工具的使用—动画组件 这里我汇总一下多边形工具在整个交互动画领域的常见使用方式:

专题名称 多边形在交互动画中的核心作用 典型制作场景
画面构图 作为背景块面与画面结构单元 场景搭建、画面比例调整
区域划分 划定功能区与交互区 操作区 / 显示区分离
遮罩与限制 限制显示或操作范围 区域内有效交互
角色轮廓 作为物体或角色的简化外形 轻量级动画角色
结构骨架 作为动画的几何骨架 框架、机构演示
变形结构 拖动顶点引发整体变形 拉伸、压缩动画
运动边界 限制对象的运动范围 防止越界
路径依附 为运动对象提供参考边 沿边移动、贴边动画
参数承载 面积、周长作为动态参数 数值驱动动画变化
间接控制 通过形状变化控制动画状态 减少滑块依赖
状态反馈 颜色、透明度变化提示状态 即时反馈
条件触发 作为几何判断条件 简单交互逻辑
场景建模 构建完整交互场景轮廓 情境化动画
教学演示 可被拖动的演示对象 探索式学习
实验操作 支持反复操作与验证 “试一试”型动画
对象组织 作为多个对象的参考基准 动画结构管理
依赖关系 构建对象间的依赖网络 保持系统稳定
动画系统 作为底层建模单元 完整交互动画

本篇讲解的内容稍微倾向于底层逻辑的梳理与交互动画创作思路的培养,当然,也是因为多边形工具非常重要,且是众多动画根基的缘故。动画工具本身非常简单,但是背后涉及到的理论和思路,值得我们深究。

三. ✨结尾

本文配套文件已上传,资料获取方式见文章首部表格。


本文收录于微信公众号:八荒启-交互动画,可点击扫码关注,更多技术咨询与服务,可直接访问官方网站获取:bahuangqi.com/(电脑打开)

前端常用模式:提升代码质量的四大核心模式

作者 1024肥宅
2025年12月13日 13:00

引言

在前端开发中,随着应用复杂度不断增加,代码的组织和管理变得越来越重要。本文将深入探讨前端开发中四种极其有用的模式:中间件模式、管道模式、链式调用和惰性加载。这些模式不仅能提高代码的可读性和可维护性,还能显著优化应用性能。

一、中间件模式(Middleware Pattern)

中间件模式允许我们在请求和响应的处理流程中插入多个处理阶段, 这种模式在Node.js框架(例如Koa、Express)中广泛应用, 但在前端同样有其用武之地。

1.1 核心概念

中间件本质上是一个函数, 它可以:

  • 访问请求(request)和响应(response)对象
  • 执行任何代码
  • 修改请求和响应对象
  • 结束请求-响应周期
  • 调用下一个中间件
1.2 基础实现
// 简单中间件系统实现
class MiddlewareSystem {
  constructor() {
    this.middlewares = [];
    this.context = {};
  }

  // 添加中间件
  use(middleware) {
    if (typeof middleware !== 'function') {
      throw new Error('Middleware must be a function');
    }
    this.middlewares.push(middleware);
    return this; // 支持链式调用
  }

  // 执行中间件
  async run(input) {
    this.context = { ...input };
    
    // 创建next函数
    let index = 0;
    const next = async () => {
      if (index < this.middlewares.length) {
        const middleware = this.middlewares[index++];
        await middleware(this.context, next);
      }
    };
    
    try {
      await next();
      return this.context;
    } catch (error) {
      console.error('Middleware execution error:', error);
      throw error;
    }
  }
}

// 使用示例
const system = new MiddlewareSystem();

// 添加中间件
system
  .use(async (ctx, next) => {
    console.log('Middleware 1: Start');
    ctx.timestamp = Date.now();
    await next();
    console.log('Middleware 1: End');
  })
  .use(async (ctx, next) => {
    console.log('Middleware 2: Start');
    ctx.user = { id: 1, name: 'John' };
    // 模拟异步操作
    await new Promise(resolve => setTimeout(resolve, 100));
    await next();
    console.log('Middleware 2: End');
  })
  .use(async (ctx, next) => {
    console.log('Middleware 3: Start');
    ctx.data = { message: 'Hello World' };
    // 不再调用next(),结束执行
    console.log('Middleware 3: End (no next call)');
  });

// 运行中间件
system.run({ requestId: '123' }).then(result => {
  console.log('Final context:', result);
});
1.3 错误处理中间件
class ErrorHandlingMiddleware {
  constructor() {
    this.middlewares = [];
    this.errorMiddlewares = [];
  }

  use(middleware) {
    this.middlewares.push(middleware);
    return this;
  }

  useError(errorMiddleware) {
    this.errorMiddlewares.push(errorMiddleware);
    return this;
  }

  async run(input) {
    const context = { ...input };
    let index = 0;
    let errorIndex = 0;
    let error = null;

    const next = async (err) => {
      if (err) {
        error = err;
        errorIndex = 0;
        return await nextError();
      }

      if (index < this.middlewares.length) {
        const middleware = this.middlewares[index++];
        try {
          await middleware(context, next);
        } catch (err) {
          await next(err);
        }
      }
    };

    const nextError = async () => {
      if (errorIndex < this.errorMiddlewares.length) {
        const errorMiddleware = this.errorMiddlewares[errorIndex++];
        try {
          await errorMiddleware(error, context, nextError);
        } catch (err) {
          error = err;
          await nextError();
        }
      }
    };

    await next();
    return { context, error };
  }
}

// 使用示例
const errorSystem = new ErrorHandlingMiddleware();

errorSystem
  .use(async (ctx, next) => {
    console.log('Processing...');
    // 模拟错误
    if (!ctx.user) {
      throw new Error('User not found');
    }
    await next();
  })
  .useError(async (err, ctx, next) => {
    console.error('Error caught:', err.message);
    ctx.error = err.message;
    ctx.status = 'error';
    await next();
  });

errorSystem.run({}).then(result => {
  console.log('Result with error handling:', result);
});
1.4 前端应用场景
// 前端请求拦截中间件
class RequestInterceptor {
  constructor() {
    this.interceptors = [];
    this.defaultConfig = {
      timeout: 5000,
      headers: {}
    };
  }

  use(interceptor) {
    this.interceptors.push(interceptor);
    return this;
  }

  async request(url, config = {}) {
    const context = {
      url,
      config: { ...this.defaultConfig, ...config },
      response: null,
      error: null
    };

    let index = 0;
    const next = async () => {
      if (index < this.interceptors.length) {
        const interceptor = this.interceptors[index++];
        await interceptor(context, next);
      } else {
        // 执行实际请求
        await this.executeRequest(context);
      }
    };

    await next();
    return context;
  }

  async executeRequest(context) {
    try {
      const response = await fetch(context.url, context.config);
      context.response = await response.json();
    } catch (error) {
      context.error = error;
    }
  }
}

// 创建请求拦截器
const api = new RequestInterceptor();

api
  .use(async (ctx, next) => {
    console.log('Auth interceptor');
    ctx.config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`;
    await next();
  })
  .use(async (ctx, next) => {
    console.log('Logging interceptor');
    console.log(`Request to ${ctx.url}`, ctx.config);
    const start = Date.now();
    await next();
    const duration = Date.now() - start;
    console.log(`Request completed in ${duration}ms`);
  })
  .use(async (ctx, next) => {
    console.log('Cache interceptor');
    const cacheKey = `cache_${ctx.url}`;
    const cached = localStorage.getItem(cacheKey);
    
    if (cached && !ctx.config.noCache) {
      ctx.response = JSON.parse(cached);
      console.log('Using cached response');
    } else {
      await next();
      if (ctx.response && !ctx.error) {
        localStorage.setItem(cacheKey, JSON.stringify(ctx.response));
      }
    }
  });

// 使用拦截器
api.request('https://api.example.com/data', { method: 'GET' })
  .then(result => console.log('Response:', result.response));

二、管道模式(Pipeline Pattern)

管道模式将多个处理函数连接起来, 数据像流水一样经过这些函数进行处理和转换。

2.1 基本实现
// 同步管道
const pipeline = (...fns) => (initialValue) => {
  return fns.reduce((value, fn) => {
    if (typeof fn !== 'function') {
      throw new Error(`Pipeline expects functions, got ${typeof fn}`);
    }
    return fn(value);
  }, initialValue);
};

// 异步管道
const asyncPipeline = (...fns) => async (initialValue) => {
  let result = initialValue;
  for (const fn of fns) {
    if (typeof fn !== 'function') {
      throw new Error(`Pipeline expects functions, got ${typeof fn}`);
    }
    result = await fn(result);
  }
  return result;
};

// 可中断的管道
const breakablePipeline = (...fns) => (initialValue) => {
  let shouldBreak = false;
  let breakValue = null;
  
  const breakFn = (value) => {
    shouldBreak = true;
    breakValue = value;
  };
  
  const result = fns.reduce((value, fn) => {
    if (shouldBreak) return breakValue;
    return fn(value, breakFn);
  }, initialValue);
  
  return shouldBreak ? breakValue : result;
};
2.2 数据处理管道示例
// 数据清洗管道
const dataCleaningPipeline = pipeline(
  // 1. 移除空值
  (data) => data.filter(item => item != null),
  
  // 2. 标准化字段
  (data) => data.map(item => ({
    ...item,
    name: item.name?.trim().toLowerCase() || 'unknown',
    value: Number(item.value) || 0
  })),
  
  // 3. 去重
  (data) => {
    const seen = new Set();
    return data.filter(item => {
      const key = `${item.name}-${item.value}`;
      if (seen.has(key)) return false;
      seen.add(key);
      return true;
    });
  },
  
  // 4. 排序
  (data) => data.sort((a, b) => b.value - a.value),
  
  // 5. 限制数量
  (data) => data.slice(0, 10)
);

// 使用管道
const rawData = [
  { name: '  John  ', value: '100' },
  { name: 'Jane', value: 200 },
  null,
  { name: 'John', value: '100' }, // 重复
  { name: '', value: 'invalid' }
];

const cleanedData = dataCleaningPipeline(rawData);
console.log('Cleaned data:', cleanedData);
2.3 表单验证管道
// 验证规则
const validators = {
  required: (value) => ({
    isValid: value != null && value.toString().trim() !== '',
    message: 'This field is required'
  }),
  
  minLength: (min) => (value) => ({
    isValid: value?.toString().length >= min,
    message: `Minimum length is ${min} characters`
  }),
  
  maxLength: (max) => (value) => ({
    isValid: value?.toString().length <= max,
    message: `Maximum length is ${max} characters`
  }),
  
  email: (value) => ({
    isValid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
    message: 'Invalid email format'
  }),
  
  numeric: (value) => ({
    isValid: !isNaN(parseFloat(value)) && isFinite(value),
    message: 'Must be a number'
  })
};

// 表单验证管道
class FormValidator {
  constructor(rules) {
    this.rules = rules;
  }

  validate(data) {
    const errors = {};
    const results = {};

    for (const [field, fieldRules] of Object.entries(this.rules)) {
      const value = data[field];
      const fieldErrors = [];

      for (const rule of fieldRules) {
        const result = rule(value);
        if (!result.isValid) {
          fieldErrors.push(result.message);
        }
      }

      if (fieldErrors.length > 0) {
        errors[field] = fieldErrors;
      } else {
        results[field] = value;
      }
    }

    return {
      isValid: Object.keys(errors).length === 0,
      errors,
      results
    };
  }
}

// 使用示例
const registrationRules = {
  username: [
    validators.required,
    validators.minLength(3),
    validators.maxLength(20)
  ],
  email: [
    validators.required,
    validators.email
  ],
  age: [
    validators.required,
    validators.numeric,
    (value) => ({
      isValid: value >= 18 && value <= 100,
      message: 'Age must be between 18 and 100'
    })
  ]
};

const validator = new FormValidator(registrationRules);
const userData = {
  username: 'johndoe',
  email: 'john@example.com',
  age: 25
};

const validationResult = validator.validate(userData);
console.log('Validation result:', validationResult);

三、链式调用(Chaining Pattern)

链式调用通过返回对象实例本身(this), 允许连续调用多个方法, 使代码更加流畅易读。

3.1 基础链式调用
// jQuery风格的链式调用
class QueryBuilder {
  constructor() {
    this.query = {
      select: [],
      from: null,
      where: [],
      orderBy: [],
      limit: null,
      offset: null
    };
  }

  select(...fields) {
    this.query.select.push(...fields);
    return this;
  }

  from(table) {
    this.query.from = table;
    return this;
  }

  where(condition) {
    this.query.where.push(condition);
    return this;
  }

  orderBy(field, direction = 'ASC') {
    this.query.orderBy.push({ field, direction });
    return this;
  }

  limit(count) {
    this.query.limit = count;
    return this;
  }

  offset(count) {
    this.query.offset = count;
    return this;
  }

  build() {
    const { select, from, where, orderBy, limit, offset } = this.query;
    
    if (!from) {
      throw new Error('FROM clause is required');
    }

    let sql = `SELECT ${select.length > 0 ? select.join(', ') : '*'} FROM ${from}`;
    
    if (where.length > 0) {
      sql += ` WHERE ${where.join(' AND ')}`;
    }
    
    if (orderBy.length > 0) {
      const orderClauses = orderBy.map(({ field, direction }) => `${field} ${direction}`);
      sql += ` ORDER BY ${orderClauses.join(', ')}`;
    }
    
    if (limit !== null) {
      sql += ` LIMIT ${limit}`;
    }
    
    if (offset !== null) {
      sql += ` OFFSET ${offset}`;
    }
    
    return sql + ';';
  }

  reset() {
    this.query = {
      select: [],
      from: null,
      where: [],
      orderBy: [],
      limit: null,
      offset: null
    };
    return this;
  }
}

// 使用示例
const sql = new QueryBuilder()
  .select('id', 'name', 'email')
  .from('users')
  .where('active = true')
  .where('age >= 18')
  .orderBy('name', 'ASC')
  .limit(10)
  .offset(0)
  .build();

console.log('Generated SQL:', sql);
3.2 DOM操作链式调用
class DOMElement {
  constructor(selector) {
    if (typeof selector === 'string') {
      this.elements = Array.from(document.querySelectorAll(selector));
    } else if (selector instanceof Element) {
      this.elements = [selector];
    } else if (Array.isArray(selector)) {
      this.elements = selector;
    } else {
      this.elements = [];
    }
  }

  // CSS相关方法
  css(property, value) {
    if (typeof property === 'object') {
      this.elements.forEach(el => {
        Object.assign(el.style, property);
      });
    } else if (value !== undefined) {
      this.elements.forEach(el => {
        el.style[property] = value;
      });
    }
    return this;
  }

  addClass(className) {
    this.elements.forEach(el => {
      el.classList.add(className);
    });
    return this;
  }

  removeClass(className) {
    this.elements.forEach(el => {
      el.classList.remove(className);
    });
    return this;
  }

  toggleClass(className) {
    this.elements.forEach(el => {
      el.classList.toggle(className);
    });
    return this;
  }

  // 内容操作
  text(content) {
    if (content !== undefined) {
      this.elements.forEach(el => {
        el.textContent = content;
      });
      return this;
    }
    return this.elements[0]?.textContent || '';
  }

  html(content) {
    if (content !== undefined) {
      this.elements.forEach(el => {
        el.innerHTML = content;
      });
      return this;
    }
    return this.elements[0]?.innerHTML || '';
  }

  // 属性操作
  attr(name, value) {
    if (value !== undefined) {
      this.elements.forEach(el => {
        el.setAttribute(name, value);
      });
      return this;
    }
    return this.elements[0]?.getAttribute(name) || null;
  }

  // 事件处理
  on(event, handler, options = {}) {
    this.elements.forEach(el => {
      el.addEventListener(event, handler, options);
    });
    return this;
  }

  off(event, handler, options = {}) {
    this.elements.forEach(el => {
      el.removeEventListener(event, handler, options);
    });
    return this;
  }

  // 遍历
  each(callback) {
    this.elements.forEach((el, index) => {
      callback.call(el, index, el);
    });
    return this;
  }

  // 查找子元素
  find(selector) {
    const found = [];
    this.elements.forEach(el => {
      found.push(...Array.from(el.querySelectorAll(selector)));
    });
    return new DOMElement(found);
  }

  // 获取父元素
  parent() {
    const parents = this.elements.map(el => el.parentElement);
    return new DOMElement(parents.filter(Boolean));
  }

  // 显示/隐藏
  show() {
    return this.css('display', '');
  }

  hide() {
    return this.css('display', 'none');
  }

  // 动画
  animate(properties, duration = 300, easing = 'ease') {
    this.elements.forEach(el => {
      el.style.transition = `all ${duration}ms ${easing}`;
      Object.assign(el.style, properties);
      
      setTimeout(() => {
        el.style.transition = '';
      }, duration);
    });
    return this;
  }
}

// 使用示例
// 假设HTML中有: <div id="myDiv">Hello</div>
const $ = (selector) => new DOMElement(selector);

$('#myDiv')
  .css({
    color: 'white',
    backgroundColor: 'blue',
    padding: '10px'
  })
  .addClass('highlight')
  .text('Hello, World!')
  .on('click', function() {
    $(this).toggleClass('active');
  })
  .animate({
    opacity: 0.8,
    transform: 'scale(1.1)'
  }, 300);
3.3 构建器模式与链式调用
// 配置对象构建器
class ConfigurationBuilder {
  constructor() {
    this.config = {
      api: {},
      ui: {},
      features: {},
      performance: {}
    };
  }

  // API配置
  withApi(baseUrl) {
    this.config.api.baseUrl = baseUrl;
    return this;
  }

  withApiVersion(version) {
    this.config.api.version = version;
    return this;
  }

  withTimeout(ms) {
    this.config.api.timeout = ms;
    return this;
  }

  // UI配置
  withTheme(theme) {
    this.config.ui.theme = theme;
    return this;
  }

  withLanguage(lang) {
    this.config.ui.language = lang;
    return this;
  }

  withDarkMode(enabled) {
    this.config.ui.darkMode = enabled;
    return this;
  }

  // 功能配置
  enableFeature(feature) {
    this.config.features[feature] = true;
    return this;
  }

  disableFeature(feature) {
    this.config.features[feature] = false;
    return this;
  }

  // 性能配置
  withCache(enabled) {
    this.config.performance.cache = enabled;
    return this;
  }

  withLazyLoad(enabled) {
    this.config.performance.lazyLoad = enabled;
    return this;
  }

  withCompression(enabled) {
    this.config.performance.compression = enabled;
    return this;
  }

  // 构建方法
  build() {
    // 验证配置
    this.validate();
    // 返回不可变配置
    return Object.freeze(JSON.parse(JSON.stringify(this.config)));
  }

  validate() {
    const { api } = this.config;
    if (!api.baseUrl) {
      throw new Error('API base URL is required');
    }
    if (api.timeout && api.timeout < 100) {
      throw new Error('Timeout must be at least 100ms');
    }
  }
}

// 使用示例
const config = new ConfigurationBuilder()
  .withApi('https://api.example.com')
  .withApiVersion('v1')
  .withTimeout(5000)
  .withTheme('dark')
  .withLanguage('en')
  .withDarkMode(true)
  .enableFeature('analytics')
  .enableFeature('notifications')
  .disableFeature('debug')
  .withCache(true)
  .withLazyLoad(true)
  .build();

console.log('App configuration:', config);

四、惰性加载/求值(Lazy Loading/Evaluation)

惰性加载延迟计算或初始化, 直到真正需要时才执行, 可以显著提高应用性能。

4.1 惰性求值实现
// 惰性函数
function lazy(fn) {
  let result;
  let evaluated = false;
  
  return function(...args) {
    if (!evaluated) {
      result = fn.apply(this, args);
      evaluated = true;
    }
    return result;
  };
}

// 惰性属性
function lazyProperty(target, propertyName, getter) {
  let value;
  let evaluated = false;
  
  Object.defineProperty(target, propertyName, {
    get() {
      if (!evaluated) {
        value = getter.call(this);
        evaluated = true;
      }
      return value;
    },
    enumerable: true,
    configurable: true
  });
}

// 惰性类属性
class LazyClass {
  constructor() {
    this._expensiveData = null;
  }

  get expensiveData() {
    if (this._expensiveData === null) {
      console.log('Computing expensive data...');
      // 模拟耗时计算
      this._expensiveData = this.computeExpensiveData();
    }
    return this._expensiveData;
  }

  computeExpensiveData() {
    // 复杂的计算逻辑
    const start = Date.now();
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
      result += Math.sqrt(i);
    }
    console.log(`Computed in ${Date.now() - start}ms`);
    return result;
  }

  // 重置惰性值
  reset() {
    this._expensiveData = null;
  }
}
4.2 图片懒加载
class LazyImageLoader {
  constructor(options = {}) {
    this.options = {
      root: null,
      rootMargin: '0px',
      threshold: 0.1,
      placeholder: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
      ...options
    };
    
    this.images = new Map();
    this.observer = null;
    this.initObserver();
  }

  initObserver() {
    if ('IntersectionObserver' in window) {
      this.observer = new IntersectionObserver(
        this.handleIntersection.bind(this),
        this.options
      );
    }
  }

  registerImage(imgElement, src) {
    if (!imgElement || !src) return;

    // 保存原始src
    const originalSrc = imgElement.getAttribute('data-src') || src;
    imgElement.setAttribute('data-src', originalSrc);
    
    // 设置占位符
    imgElement.src = this.options.placeholder;
    imgElement.classList.add('lazy-load');
    
    // 添加到观察列表
    this.images.set(imgElement, {
      src: originalSrc,
      loaded: false
    });
    
    if (this.observer) {
      this.observer.observe(imgElement);
    } else {
      // 降级方案:立即加载
      this.loadImage(imgElement);
    }
  }

  handleIntersection(entries) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        this.loadImage(img);
        this.observer?.unobserve(img);
      }
    });
  }

  async loadImage(imgElement) {
    const imageData = this.images.get(imgElement);
    if (!imageData || imageData.loaded) return;

    try {
      // 预加载图片
      await this.preloadImage(imageData.src);
      
      // 应用实际图片
      imgElement.src = imageData.src;
      imgElement.classList.remove('lazy-load');
      imgElement.classList.add('lazy-loaded');
      
      imageData.loaded = true;
      
      // 触发加载完成事件
      imgElement.dispatchEvent(new CustomEvent('lazyload', {
        detail: { src: imageData.src }
      }));
    } catch (error) {
      console.error('Failed to load image:', imageData.src, error);
      imgElement.classList.add('lazy-error');
      
      imgElement.dispatchEvent(new CustomEvent('lazyloaderror', {
        detail: { src: imageData.src, error }
      }));
    }
  }

  preloadImage(src) {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.onerror = reject;
      img.src = src;
    });
  }

  // 批量注册图片
  registerAll(selector = 'img[data-src]') {
    const images = document.querySelectorAll(selector);
    images.forEach(img => {
      const src = img.getAttribute('data-src');
      if (src) {
        this.registerImage(img, src);
      }
    });
  }

  // 强制加载特定图片
  forceLoad(imgElement) {
    this.loadImage(imgElement);
  }

  // 清理
  destroy() {
    this.observer?.disconnect();
    this.images.clear();
  }
}

// 使用示例
document.addEventListener('DOMContentLoaded', () => {
  const lazyLoader = new LazyImageLoader({
    threshold: 0.5,
    placeholder: '/images/placeholder.png'
  });
  
  // 注册现有图片
  lazyLoader.registerAll();
  
  // 动态添加的图片
  document.addEventListener('newImagesAdded', (event) => {
    const newImages = event.detail.images;
    newImages.forEach(img => {
      lazyLoader.registerImage(img, img.dataset.src);
    });
  });
  
  // 页面离开时清理
  window.addEventListener('beforeunload', () => {
    lazyLoader.destroy();
  });
});
4.3 组件懒加载(Vue/React示例)
// React组件懒加载
import React, { Suspense, lazy } from 'react';

// 懒加载组件
const LazyComponent = lazy(() => import('./ExpensiveComponent'));

// 使用Suspense包裹
function App() {
  return (
    <div>
      <h1>My App</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
      </Suspense>
    </div>
  );
}

// Vue组件懒加载
const LazyComponent = () => import('./ExpensiveComponent.vue');

// 路由配置中使用懒加载
const routes = [
  {
    path: '/dashboard',
    component: () => import('./views/Dashboard.vue')
  },
  {
    path: '/settings',
    component: () => import('./views/Settings.vue')
  }
];

// 自定义懒加载封装
function createLazyLoader(importFn, loadingComponent, errorComponent) {
  return {
    data() {
      return {
        component: null,
        error: null,
        loading: true
      };
    },
    
    async created() {
      try {
        const module = await importFn();
        this.component = module.default || module;
      } catch (err) {
        this.error = err;
        console.error('Failed to load component:', err);
      } finally {
        this.loading = false;
      }
    },
    
    render(h) {
      if (this.loading && loadingComponent) {
        return h(loadingComponent);
      }
      
      if (this.error && errorComponent) {
        return h(errorComponent, { error: this.error });
      }
      
      if (this.component) {
        return h(this.component);
      }
      
      return null;
    }
  };
}
4.4 数据懒加载(无限滚动)
class InfiniteScroll {
  constructor(options = {}) {
    this.options = {
      container: document.documentElement,
      distance: 100,
      throttle: 200,
      onLoadMore: () => Promise.resolve(),
      ...options
    };
    
    this.loading = false;
    this.hasMore = true;
    this.throttleTimer = null;
    
    this.init();
  }

  init() {
    this.container = typeof this.options.container === 'string' 
      ? document.querySelector(this.options.container)
      : this.options.container;
    
    if (!this.container) {
      console.error('Container not found');
      return;
    }
    
    this.bindEvents();
  }

  bindEvents() {
    this.container.addEventListener('scroll', this.handleScroll.bind(this));
    window.addEventListener('resize', this.handleScroll.bind(this));
  }

  handleScroll() {
    if (this.throttleTimer) {
      clearTimeout(this.throttleTimer);
    }
    
    this.throttleTimer = setTimeout(() => {
      this.checkPosition();
    }, this.options.throttle);
  }

  checkPosition() {
    if (this.loading || !this.hasMore) return;
    
    const scrollTop = this.container.scrollTop;
    const scrollHeight = this.container.scrollHeight;
    const clientHeight = this.container.clientHeight;
    
    const distanceToBottom = scrollHeight - (scrollTop + clientHeight);
    
    if (distanceToBottom <= this.options.distance) {
      this.loadMore();
    }
  }

  async loadMore() {
    if (this.loading || !this.hasMore) return;
    
    this.loading = true;
    this.container.dispatchEvent(new CustomEvent('loadstart'));
    
    try {
      const result = await this.options.onLoadMore();
      
      if (result && typeof result.hasMore === 'boolean') {
        this.hasMore = result.hasMore;
      }
      
      this.container.dispatchEvent(new CustomEvent('load', { 
        detail: result 
      }));
    } catch (error) {
      this.container.dispatchEvent(new CustomEvent('error', { 
        detail: error 
      }));
      console.error('Failed to load more data:', error);
    } finally {
      this.loading = false;
      this.container.dispatchEvent(new CustomEvent('loadend'));
      
      // 检查是否还需要继续加载(数据可能没有填满屏幕)
      if (this.hasMore) {
        setTimeout(() => this.checkPosition(), 100);
      }
    }
  }

  // 手动触发加载
  triggerLoad() {
    this.loadMore();
  }

  // 重置状态
  reset() {
    this.loading = false;
    this.hasMore = true;
  }

  // 销毁
  destroy() {
    this.container.removeEventListener('scroll', this.handleScroll);
    window.removeEventListener('resize', this.handleScroll);
    
    if (this.throttleTimer) {
      clearTimeout(this.throttleTimer);
    }
  }
}

// 使用示例
const infiniteScroll = new InfiniteScroll({
  container: '#scrollContainer',
  distance: 200,
  throttle: 300,
  async onLoadMore() {
    // 模拟API调用
    const response = await fetch(`/api/items?page=${currentPage}`);
    const data = await response.json();
    
    // 渲染新数据
    renderItems(data.items);
    
    // 返回是否有更多数据
    return { hasMore: data.hasMore };
  }
});

// 动态更新选项
function updateInfiniteScroll(options) {
  infiniteScroll.options = { ...infiniteScroll.options, ...options };
}

五、模式对比与应用场景

模式 优点 缺点 适用场景
中间件模式 解耦、可组合、易于测试 可能增加复杂性、调试困难 请求处理、数据处理管道、插件系统
管道模式 清晰的数据流向、易于测试和复用 可能创建太多小函数、错误处理复杂 数据转换、验证、清洗流程
链式调用 代码流畅、易读、减少临时变量 可能掩盖错误来源、调试困难 构建器模式、DOM操作、配置设置
惰性加载 提高性能】减少内存使用 初始化延迟、复杂性增加 图片加载、组件加载、数据计算
5.1 如何选择模式?
  1. 需要处理请求/响应流程 → 中间件模式
  2. 需要数据转换流水线 → 管道模式
  3. 需要流畅的API接口 → 链式调用
  4. 需要优化性能,延迟初始化 → 惰性加载

六、综合应用实例

6.1 完整的API客户端
class ApiClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
    this.middlewares = [];
    this.defaultHeaders = {
      'Content-Type': 'application/json'
    };
  }

  // 添加中间件
  use(middleware) {
    this.middlewares.push(middleware);
    return this;
  }

  // 创建请求管道
  async request(endpoint, options = {}) {
    const context = {
      url: `${this.baseURL}${endpoint}`,
      options: {
        method: 'GET',
        headers: { ...this.defaultHeaders, ...options.headers },
        ...options
      },
      response: null,
      error: null,
      data: null
    };

    // 执行中间件管道
    await this.executeMiddleware(context);
    
    if (context.error) {
      throw context.error;
    }

    return context.response;
  }

  async executeMiddleware(context) {
    let index = 0;
    const middlewares = this.middlewares;
    
    const next = async () => {
      if (index < middlewares.length) {
        const middleware = middlewares[index++];
        await middleware(context, next);
      } else {
        // 执行实际请求
        await this.executeRequest(context);
      }
    };
    
    await next();
  }

  async executeRequest(context) {
    try {
      const response = await fetch(context.url, context.options);
      context.response = {
        status: response.status,
        headers: Object.fromEntries(response.headers.entries()),
        data: await response.json()
      };
    } catch (error) {
      context.error = error;
    }
  }

  // 快捷方法(链式调用)
  get(endpoint, options = {}) {
    return this.request(endpoint, { ...options, method: 'GET' });
  }

  post(endpoint, data, options = {}) {
    return this.request(endpoint, {
      ...options,
      method: 'POST',
      body: JSON.stringify(data)
    });
  }

  put(endpoint, data, options = {}) {
    return this.request(endpoint, {
      ...options,
      method: 'PUT',
      body: JSON.stringify(data)
    });
  }

  delete(endpoint, options = {}) {
    return this.request(endpoint, { ...options, method: 'DELETE' });
  }
}

// 创建API客户端并配置中间件
const api = new ApiClient('https://api.example.com')
  .use(async (ctx, next) => {
    // 认证中间件
    const token = localStorage.getItem('auth_token');
    if (token) {
      ctx.options.headers.Authorization = `Bearer ${token}`;
    }
    await next();
  })
  .use(async (ctx, next) => {
    // 日志中间件
    console.log(`[API] ${ctx.options.method} ${ctx.url}`);
    const start = Date.now();
    await next();
    const duration = Date.now() - start;
    console.log(`[API] Completed in ${duration}ms`);
  })
  .use(async (ctx, next) => {
    // 错误处理中间件
    try {
      await next();
    } catch (error) {
      console.error('[API] Request failed:', error);
      // 可以在这里实现重试逻辑
      throw error;
    }
  });

// 使用示例
async function fetchUserData() {
  try {
    const response = await api.get('/users/1');
    console.log('User data:', response.data);
    return response.data;
  } catch (error) {
    console.error('Failed to fetch user:', error);
  }
}
6.2 表单处理系统
class FormProcessor {
  constructor(formElement) {
    this.form = formElement;
    this.fields = new Map();
    this.validators = new Map();
    this.transformers = [];
    this.submitHandlers = [];
    
    this.init();
  }

  init() {
    // 收集表单字段
    Array.from(this.form.elements).forEach(element => {
      if (element.name) {
        this.fields.set(element.name, element);
      }
    });
    
    // 绑定提交事件
    this.form.addEventListener('submit', this.handleSubmit.bind(this));
  }

  // 添加验证器
  addValidator(fieldName, validator) {
    if (!this.validators.has(fieldName)) {
      this.validators.set(fieldName, []);
    }
    this.validators.get(fieldName).push(validator);
    return this;
  }

  // 添加数据转换器
  addTransformer(transformer) {
    this.transformers.push(transformer);
    return this;
  }

  // 添加提交处理器
  onSubmit(handler) {
    this.submitHandlers.push(handler);
    return this;
  }

  // 获取表单数据
  getData() {
    const data = {};
    this.fields.forEach((element, name) => {
      data[name] = this.getValue(element);
    });
    return data;
  }

  getValue(element) {
    if (element.type === 'checkbox') {
      return element.checked;
    } else if (element.type === 'radio') {
      return element.checked ? element.value : null;
    } else if (element.type === 'select-multiple') {
      return Array.from(element.selectedOptions).map(opt => opt.value);
    }
    return element.value;
  }

  // 验证表单
  validate() {
    const errors = {};
    const data = this.getData();
    
    this.validators.forEach((validators, fieldName) => {
      const value = data[fieldName];
      const fieldErrors = [];
      
      validators.forEach(validator => {
        const result = validator(value, data);
        if (result !== true) {
          fieldErrors.push(result || `Validation failed for ${fieldName}`);
        }
      });
      
      if (fieldErrors.length > 0) {
        errors[fieldName] = fieldErrors;
        this.showFieldError(fieldName, fieldErrors[0]);
      } else {
        this.clearFieldError(fieldName);
      }
    });
    
    return {
      isValid: Object.keys(errors).length === 0,
      errors,
      data
    };
  }

  showFieldError(fieldName, message) {
    const field = this.fields.get(fieldName);
    if (field) {
      field.classList.add('error');
      
      let errorElement = field.parentElement.querySelector('.error-message');
      if (!errorElement) {
        errorElement = document.createElement('div');
        errorElement.className = 'error-message';
        field.parentElement.appendChild(errorElement);
      }
      errorElement.textContent = message;
    }
  }

  clearFieldError(fieldName) {
    const field = this.fields.get(fieldName);
    if (field) {
      field.classList.remove('error');
      const errorElement = field.parentElement.querySelector('.error-message');
      if (errorElement) {
        errorElement.remove();
      }
    }
  }

  // 处理提交
  async handleSubmit(event) {
    event.preventDefault();
    
    // 验证
    const validation = this.validate();
    if (!validation.isValid) {
      console.log('Form validation failed:', validation.errors);
      return;
    }
    
    // 数据转换管道
    let processedData = validation.data;
    for (const transformer of this.transformers) {
      processedData = transformer(processedData);
    }
    
    // 执行提交处理器
    for (const handler of this.submitHandlers) {
      try {
        const result = await handler(processedData);
        if (result === false || result?.stopPropagation) {
          break;
        }
      } catch (error) {
        console.error('Submit handler error:', error);
        break;
      }
    }
  }

  // 重置表单
  reset() {
    this.form.reset();
    this.fields.forEach((field, name) => {
      this.clearFieldError(name);
    });
    return this;
  }
}

// 使用示例
const formProcessor = new FormProcessor(document.getElementById('myForm'))
  .addValidator('email', (value) => {
    if (!value) return 'Email is required';
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      return 'Invalid email format';
    }
    return true;
  })
  .addValidator('password', (value) => {
    if (!value) return 'Password is required';
    if (value.length < 8) return 'Password must be at least 8 characters';
    return true;
  })
  .addTransformer((data) => {
    // 转换数据
    return {
      ...data,
      email: data.email.toLowerCase().trim(),
      createdAt: new Date().toISOString()
    };
  })
  .onSubmit(async (data) => {
    console.log('Submitting data:', data);
    
    // 模拟API调用
    const response = await fetch('/api/register', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    
    if (!response.ok) {
      throw new Error('Registration failed');
    }
    
    alert('Registration successful!');
    return true;
  })
  .onSubmit((data) => {
    // 第二个处理器:发送分析事件
    console.log('Analytics event sent for registration');
  });

七、最佳实践与注意事项

7.1 中间件模式最佳实践
  1. 保持中间件简洁单一: 每个中间件只做一件事
  2. 错误处理: 确保中间件有适当的错误处理机制
  3. 性能考虑: 避免在中间件中进行昂贵的同步操作
  4. 顺序很重要: 注意中间件的执行顺序对业务逻辑的影响
7.2 管道模式最佳实践
  1. 纯函数优先: 确保管道中的函数是纯函数,避免副作用
  2. 类型检查: 考虑添加运行时类型检查
  3. 错误处理: 现管道级别的错误处理机制
  4. 性能优化: 考虑使用流式处理大数据集
7.3 链式调用最佳实践
  1. 返回this: 确保每个链式方法都返回实例本身
  2. 不可变操作: 考虑实现不可变版本的链式调用
  3. 清晰的方法名: 方法名应该清晰表达其功能
  4. 文档完善: 链式调用可能隐藏复杂度,需要良好文档
7.4 惰性加载最佳实践
  1. 适度使用: 不要过度使用惰性加载,会增加复杂度
  2. 预加载策略: 对于可能很快需要的内容,考虑预加载
  3. 错误处理: 确保惰性加载失败时有降级方案
  4. 用户反馈: 加载过程中给用户适当的反馈

总结

这四种前端模式-中间件模式、管道模式、链式调用和惰性加载---都是现代前端开发中极其有用的工具。它们各自解决了不同的问题:

  • 中间件模式提供了处理复杂流程的模块化方式
  • 管道模式让数据转换变得清晰和可组合
  • 链式调用创造了流畅、易读的API
  • 惰性加载优化了性能和资源使用

掌握这些模式并知道何时使用它们,将帮助你编写更可维护、更高效的前端代码。记住,设计模式是工具,而不是银弹。根据具体场景选择最合适的模式,并始终以代码清晰性和可维护性为首要考虑。

在实际项目中,这些模式经常组合使用。例如,一个API客户端可能同时使用中间件模式处理请求、管道模式处理数据转换、链式调用提供流畅API,并在适当的地方使用惰性加载优化性能。

希望这篇文章能帮助你在前端开发中更好地应用这些强大的模式!

理解 Proxy 原理及如何拦截 Map、Set 等集合方法调用实现自定义拦截和日志——含示例代码解析

作者 如果你好
2025年12月12日 23:45

先理解 Proxy 的核心思想

Proxy 就像一个“拦截器”,它可以“监听”一个对象的操作,比如:

  • 访问对象的属性(读取) → 触发 get 拦截器
  • 给对象的属性赋值(写入) → 触发 set 拦截器
  • 调用对象的方法 → 其实是先访问方法(触发 get),再执行它

但集合类型(Map、Set 等)不直接用属性赋值来写入数据

  • Map 写入数据是调用它的 set(key, value) 方法
  • Set 写入数据是调用它的 add(value) 方法
  • 读取数据是调用 Map 的 get(key) 或 Set 的 has(value) 方法

所以,我们想拦截“写入”操作,就要拦截这些方法的调用。


Proxy 怎么拦截方法调用?

  • 当你访问 proxyMap.set,会触发 Proxy 的 get 拦截器,告诉你访问了 set 方法。
  • 这时我们返回一个“包装函数”,这个函数内部可以插入自定义逻辑(比如打印日志),然后再调用原始的 set 方法。
  • 这样就实现了“拦截写入操作”。

具体示例:拦截 Map 的读取和写入

const map = new Map();

const handler = {
  get(target, prop, receiver) {
    // 访问属性或方法时触发
    const origMethod = target[prop];
    if (typeof origMethod === 'function') {
      // 如果访问的是方法,返回一个包装函数
      return function (...args) {
        if (prop === 'set') {
          console.log(`写入操作:set(${args[0]}, ${args[1]})`);
        } else if (prop === 'get') {
          console.log(`读取操作:get(${args[0]})`);
        }
        // 调用原始方法
        return origMethod.apply(target, args);
      };
    }
    // 访问普通属性,直接返回
    return Reflect.get(target, prop, receiver);
  }
};

const proxyMap = new Proxy(map, handler);

proxyMap.set('name', 'CodeMoss');  // 控制台输出:写入操作:set(name, CodeMoss)
console.log(proxyMap.get('name')); // 控制台输出:读取操作:get(name)
                                   // 输出:CodeMoss

可以把它理解成:

  • 访问 proxyMap.set → Proxy 拦截,返回一个“带日志”的函数
  • 调用这个函数时,先打印日志,再调用真正的 map.set

Set 也是类似的,只是写入方法叫 add,读取方法叫 has

const set = new Set();

const handler = {
  get(target, prop, receiver) {
    const origMethod = target[prop];
    if (typeof origMethod === 'function') {
      return function (...args) {
        if (prop === 'add') {
          console.log(`写入操作:add(${args[0]})`);
        } else if (prop === 'has') {
          console.log(`读取操作:has(${args[0]})`);
        }
        return origMethod.apply(target, args);
      };
    }
    return Reflect.get(target, prop, receiver);
  }
};

const proxySet = new Proxy(set, handler);

proxySet.add(123);  // 控制台输出:写入操作:add(123)
console.log(proxySet.has(123)); // 控制台输出:读取操作:has(123)
                               // 输出:true

WeakMap 和 WeakSet 也一样,只是它们的键或值必须是对象,且不能遍历

const weakMap = new WeakMap();

const handler = {
  get(target, prop, receiver) {
    const origMethod = target[prop];
    if (typeof origMethod === 'function') {
      return function (...args) {
        if (prop === 'set') {
          console.log('WeakMap 写入操作,键:', args[0], '值:', args[1]);
        } else if (prop === 'get') {
          console.log('WeakMap 读取操作,键:', args[0]);
        }
        return origMethod.apply(target, args);
      };
    }
    return Reflect.get(target, prop, receiver);
  }
};

const proxyWeakMap = new Proxy(weakMap, handler);

const objKey = {};
proxyWeakMap.set(objKey, 'secret');  // 控制台输出:WeakMap 写入操作,键: {} 值: secret
console.log(proxyWeakMap.get(objKey)); // 控制台输出:WeakMap 读取操作,键: {}
                                       // 输出:secret

总结

  • Proxy 的 get 拦截器拦截的是“属性访问”,方法调用是先访问方法再执行。
  • 集合的写入和读取都是通过调用方法实现的,所以我们拦截方法访问,返回包装函数。
  • 包装函数里可以插入自定义逻辑(日志、权限等),然后调用原始方法完成操作。

把 const objKey = {}; 换成 map”,把 WeakMap 的键从一个普通对象 {} 换成一个 Map 对象。


先说明一点:

WeakMap 的键必须是对象,而 Map 本身是一个对象(它是一个构造函数实例),所以理论上是可以作为 WeakMap 的键的。


可以这样写:

const weakMap = new WeakMap();

const handler = {
  get(target, prop, receiver) {
    const origMethod = target[prop];
    if (typeof origMethod === 'function') {
      return function (...args) {
        if (prop === 'set') {
          console.log('WeakMap 写入操作,键:', args[0], '值:', args[1]);
        } else if (prop === 'get') {
          console.log('WeakMap 读取操作,键:', args[0]);
        }
        return origMethod.apply(target, args);
      };
    }
    return Reflect.get(target, prop, receiver);
  }
};

const proxyWeakMap = new Proxy(weakMap, handler);

// 这里用 Map 作为键
const mapKey = new Map();

proxyWeakMap.set(mapKey, 'secret');  // 控制台输出:WeakMap 写入操作,键: Map {} 值: secret
console.log(proxyWeakMap.get(mapKey)); // 控制台输出:WeakMap 读取操作,键: Map {}
                                       // 输出:secret

解释:

  • mapKey 是一个 Map 实例,属于对象类型,可以作为 WeakMap 的键。
  • WeakMap 允许任何对象作为键,包括普通对象、数组、函数、甚至 Map、Set 等实例。
  • 用 Map 作为键,完全没问题。

漫谈 JS 解析与作用域锁定

作者 一锤捌拾
2025年12月12日 18:37

这部分内容,学了当然最好,没学,也不影响前端开发。当然,能了解肯定是比不了解的强。

依旧是无图无码,网文风格。我觉得,能用文字把逻辑或者概念表述清楚,一是对作者本身的能力提升有好处,二是对读者来说 思考文字表达的内容 有助于多使用抽象思维和逻辑思维能力,构建自己的思考模式,用现在流行的说法 就是心智模型。你自己什么都可以脑补,那不是厉害大了嘛。

上面的话不要相信,其实我就是为自己懒找的借口。

因为标题就说了 是漫谈,所以有些细节做了省略 有些边界情况做了简化表述。但是总体来说 准确性还是可以的。如果有错漏的地方,还请多多指正。 这是第一部分 词法和语法分析。

一.词法分析和语法分析

当浏览器从网络下载了js文件,比如app.js,浏览器引擎拿到的最初形态是一串**字节流 **。

  1. 识别: V8 首先要处理编码,V8 接收的是 UTF-8 编码的字节流,内部会转换为 UTF-16 处理字符串。

  2. 流式快速处理: 引擎并不是等整个文件下载完才开始干活的。只要网络传过来一段数据,V8 的扫描器就开始工作了。 这样可以加快启动速度。此时的状态就是毫无意义的字符 c, o, n, s, t, , a, , =, , 1, ; ...

  3. 然后的这一步叫 Tokenization 词语切分。 负责这一步的组件就是上面提到的叫 Scanner(扫描器)。它的工作就像是一个切菜工,把滔滔不绝连绵不断的字符串切成一个个有语法意义的最小单位,叫做 Token(记号)。看到这个词 ,大家是不是惊觉心一缩,没错,就是它,它们就是以它为单位来收咱钱的。

    scanner 内部是一个状态机。它逐个读取字符:

    • 读到 c 可能是 const,也可能是变量名,继续。
    • 读到 o, n, s, t 凑齐了5个娃,且下一个字符不是字母(比如是空格),确认这是一个关键字 const。”(防止误判 constant 这种变量名)
    • 读到 空格 忽略,跳过去。
    • 读到 1 这是一个数字。

    这样就由原来的字节流变成了 Token 流。这是一种扁平的列表结构。

    • 源码: const a = 1;
    • Token 流:
      • CONST (关键字)
      • IDENTIFIER (值为 "a")
      • ASSIGN (符号 "=")
      • SMI (小整数 "1")
      • SEMICOLON (符号 ";")

    这一步,注释和多余的空格和换行符会被抛弃。

  4. 现在就是解析阶段了

    其实解析是一个总称,它分为 全量解析 和 预解析 两种形式。

    这就是v8的懒解析机制。看到这个懒字,也差不多能明白了吧。

    对于那些不是立即执行的函数(比如点击按钮才触发的回调),V8 会先用预解析快速扫一遍。

    检查基本的语法错误(比如有没有少写括号),确认这是一个函数。并不会生成复杂的 AST 结构,也不建立具体的变量绑定,只进行最基础的闭包引用检查。御姐喜的结果是这个函数在内存里只是一个很小的占位符,跳过内部细节。

    而只有那些立即执行函数或者顶层代码,才会进入真正的全量解析,进行完整的 AST 构建。

    那么,问题就来了,v8怎么判断到底是使用预解析还是使用全量解析呢?

    它的原则就是 懒惰为主 全量为辅

    就是v8默认你写的函数暂时不会执行,除非是已经显式的通过语法告诉它,这段这行代码 马上就要跑 你赶快全量解析。

    下面 我们稍微详细的说一下

    • 默认绝大多数函数都是预解析

      v8认为js在初始运行时,仅仅只有很少很少一部分代码 是需要马上使用的 其他觉得大部分 都是要么是回调 要么是其他的暂时用不到的,所以,凡是具名函数声明、嵌套函数,默认都是预解析。

      function clickHandler() {
        console.log("要不要解析我");
      }
      // 引擎认为 这是一个函数声明  看起来还没人调勇它
      // 先不浪费时间了,只检查一下括号匹配吧,
      // 把它标记为 'uncompiled',然后跳过。"
      
    • 那么 如何才能符合它进行全量解析的条件呢

      1. 顶层代码

        写在最外层 不在任何函数内 的代码,加载完必须立即执行。

        判断依据: 只要不在 function 块里的代码,全是顶层代码,必须全量解析。

      2. 立即执行函数

        那么这里有个问题,就是V8 如何在还没运行代码时,就知道这个函数是立即调用执行函数呢?

        答案就是 看括号()

        当解析器扫描到一个函数关键字 function 时,它会看一眼这个 function 之前有没有左括号 (

        • 没括号

          function foo() { ... }
          // 没看到左括号,那你先靠边吧, 对它预解析。
          
        • 有括号

          (function() { ... })();
          // 扫描器扫到了这个左括号
          // 欸,这有个左括号包着 function
          // 根据万年经验,这是个立即执行函数,马上就要执行。
          // 直接上大菜,全量解析,生成 AST
          
        • 其他的立即执行的迹象:除了括号,!+- 等一元运算符放在 function 前面,也会触发全量解析

          !function() { ... }(); // 全量解析
          
    • 如果有嵌套函数咋办呢

      嵌套函数默认是预解析,即使外部函数进行的是全量解析,它内部定义的子函数,默认依然是预解析。只有当子函数真的被调用时,V8 才会暂停执行,去把子函数的全量解析做完 把 AST 补齐

      //顶层代码全量解析
      (function outer() {
        var a = 1;
      
        // 内部函数 inner:
        // 虽然 outer 正在执行,但 inner 还没被调用
        // 引擎也不确定 inner 会不会被调用。
        // 所以inner 默认预解析。
        function inner() {
          var b = 2;
        }
      
        inner(); // 直到执行到这一行,引擎才会回头去对 inner 进行全量解析
      })();
      
    • 那么 引擎根据自己的判断 进行全量解析或者预解析,会出错吗

      当然会,

      如果是本该预解析的 结果判断错了 进行了全量解析 浪费了时间和内存生成了 AST 和字节码,结果这代码根本没跑。

      如果是本该全量解析的又巨又大又重的函数 结果判断错了 进行了预解析,然后马上下一行代码就调用了,结果就是 白白预解析了一遍,浪费了时间,发现马上被调用,又马上回头全量解析一边 又花了时间,两次的花费。

  5. 在上面只是讲了解析阶段的预解析和全量解析的不同,现在我们讲解析阶段的过程

    V8 使用的是递归下降分析法。它根据js 的语法规则来匹配 Token。

    它的规则类似于:当我们遇到 const,根据语法规则,后面必须跟一个变量名,然后是一个赋值号,然后是一个表达式。

    过程示例:

    看到 const 创建一个变量声明节点。

    看到 a 把它作为声明的标识符

    看到 = 知道后面是初始值

    看到 1 创建一个字面量节点,挂在 = 的右边。

    而在这个阶段的同时,作用域分析也在同步进行,因为在构建 AST 的过程中,解析器必须要搞清楚变量在哪里

    它会盘算 这个 a 是全局变量,还是函数内的局部变量?

    如果当前函数内部引用了外层的变量,解析器会在这个阶段打上标记:“要小心,这个变量被逮住了,将来可能需要上下文来分配”。

    这个作用域分析比较重要,我们用稍微大点的篇幅来讲讲。

    首先 强烈建议 不要再去用以前的 活动对象AO vo 等等的说法来思考问题。应该使用现在的词法作用域 环境记录 等等思考模型。

    词法作用域 (Lexical Scoping)” 的定义:作用域是由代码书写的位置决定的,而不是由调用位置决定的。

    这说明,引擎在还没开始执行代码,仅仅通过“扫描”源代码生成 AST 的阶段,就已经把“谁能访问谁”、“谁被谁逮住”这笔账算得清清楚楚了。

    一旦AST被生成,那么至少意味着下面的情况

    作用域层级被确定

    AST 本身的树状结构,就是作用域层级的物理体现。

    • AST 节点: 当解析器遇到一个 function 关键字,它会在 AST 上生成一个 FunctionLiteral 节点。

    • Scope 对象: 在 V8 内部,随着 AST 的生成,解析器会同时维护一棵 “作用域树”

      • 每进入一个函数,V8 就会创建一个新的 Scope 对象。
      • 这个 Scope 对象会有一个指针指向它的 Outer Scope父作用域。
    • 结果: 这种“父子关系”是静态锁定的。无论你将来在哪里调用这个函数,它的“父级”永远是定义时的那个作用域。

    变量引用关系被识别

    这是解析器最忙碌的工作之一,叫做 变量解析

    • 声明: 当解析器遇到 let a = 1,它会在当前 Scope 记录:“我有了一个叫 a 的变量”。
    • 引用: 当解析器遇到 console.log(a) 时,它会生成一个 变量代理
    • 链接过程: 解析器会尝试“连接”这个代理和声明:
      1. 先在当前 Scope 找 a
      2. 找不到?沿着 Scope Tree 往上找父作用域。
      3. 找到了?建立绑定。
      4. 一直到了全局还没找到?标记为全局变量(或者报错)。

    这里要注意: 这个“找”的过程是在编译阶段完成的逻辑推导。

    闭包的蓝图被预判

    这一步是 V8 性能优化的关键,也就是作用域分析。

    • 发现闭包: 解析器发现内部函数 inner 引用了外部函数 outer 的变量 x
    • 打个大标签:
      • 解析器会给 x 打上一个标签:“强制上下文分配”
      • 意思是:“虽然 x 是局部变量,但因为有人跨作用域引用它,所以它不能住在普通的栈(Stack)上了... 必须搬家,住到堆(Heap)里专门开辟的 Context(上下文对象) 中去。”
    • 还没有实例化:
      • 此时内存里没有上下文对象,也没有变量 x 的值(那是运行时的事)。
      • AST 只是生成了一张**“蓝图”**,图纸上写着:“注意,将来运行的时候,这个 x 要放在特别的地方 - Context里,别放在栈上。”

下面就是解释器Ignition该登场了。我们第二部分再见。

昨天以前首页

# 🌟 JavaScript原型与原型链终极指南:从Function到Object的完整闭环解析 ,深入理解JavaScript原型系统核心

作者 AY1024
2025年12月12日 16:55

深入理解JavaScript原型系统核心

📖 目录


🎯 核心概念

四大基本原则

  1. 原则一:每个对象都有构造函数(constructor)

    • 指向构建该对象或实例的函数
  2. 原则二:只有函数对象才有prototype属性

    • 非函数对象没有prototype属性
    • 实例只有__proto__属性
    • 两者指向同一个对象(函数的原型对象)
  3. 原则三:Function函数是所有函数的构造函数

    • 包括它自己
    • 代码中声明的所有函数都是Function的实例
  4. 原则四:Object也是函数

    • 所以Object也是Function函数的实例

实例,函数,对象,原型对象,构造函数,关系总览图

image.png

🔍 非函数对象分类

  • 实例对象,const person = new Foo(),person就是实例对象
  • 普通对象({}new Object()
  • 内置非函数对象实例

🔄 显式原型与隐式原型

对象分类

  • 函数对象:拥有prototype属性
  • 非函数对象:只有__proto__属性

相同点

  • 都指向同一个原型对象

📝 示例代码

function Person(){}
const person = new Person();

console.log("Person.prototype指向:", Person.prototype)
console.log("person.__proto__指向", person.__proto__)

🖼️ 执行结果

显式原型

隐式原型


🎯 构造函数的指向

默认情况

function Person(){}
const person = new Person();

console.log("Person.prototype.constructor指向", Person.prototype.constructor)
// 输出:[Function: Person]

执行结果

默认构造函数指向

默认构造函数指向详情


修改原型对象后

function Person(){}
const person = new Person();

Person.prototype = new foo();  // 修改原型对象

console.log("Person.prototype.constructor指向", Person.prototype.constructor)
// 输出:[Function: foo]

执行结果

修改后构造函数指向

修改后构造函数指向详情


📊 核心原理说明

解释

Person.prototype被当作函数foo的实例,继承了foo函数(此篇不展开继承详解)

总结规律

  • 每个原型对象或实例都有.constructor属性
  • 实例通过原型链查找constructor
  • 原型对象默认指向自身的函数(如果不是其他函数的实例)

查找过程示例

// Person.prototype被当作实例时
Person.prototype.__proto__ → foo.prototypefoo()

🖼️ 可视化关系图

三者关系图

原型关系图


🔬 代码验证

function Person(){}

// 创建新的原型对象
Person.prototype = {
    name: "杨",
    age: "18",
    histype: "sleep"
}

// 添加方法
Person.prototype.print = function(){
    console.log("你好我是原型对象");
}

// 创建实例
const person01 = new Person();
const person02 = new Person();

// 验证指向
console.log("Person.prototype指向:", Person.prototype)
console.log("person01.__proto__指向", person01.__proto__)
console.log("person02.__proto__指向", person02.__proto__)
console.log("Person.prototype.constructor指向", Person.prototype.constructor)

执行结果

代码验证结果


⚠️ 特别说明

关键细节

创建新对象时,Person.prototype.constructor指向Object,因为Person.prototype成了Object的实例。

对比情况

  • 创建新对象时Person.prototype.constructorObject
  • 未创建新对象时Person.prototype.constructorPerson

示意图

构造函数指向对比

构造函数指向对比详情


Function和Object

小故事

从前有个力大无穷的大力神,能举起任何东西,有一天,小A在路上和这个大力神相遇了。

大力神:小子,我可是力大无穷的大力神,我能举起任何东西,你信不信?

小A:呦呦呦,还大力神,你说你能举起任何东西,那你能把你自己抬起来吗?

...

  • Function是所有函数的加工厂,你在代码声明的所有函数都是Function的实例,包括Function函数本身,Object也是函数,所以它也是Functiod的实例

  • Function就是这样的大力神,而且是可以把自己抬起来的大力神,这听起来比较扯,但是这就是事实,请看VCR:

function Person (){}

const person01 = new Person();

console.log("Function.__proto__指向",Function.__proto__)//Function.__proto__指向 [Function (anonymous)] Object
console.log("Function.prototype指向",Function.prototype)//Function.prototype指向 [Function (anonymous)] Object
console.log("Function.__proto__ == Function.prototype???",Function.__proto__ == Function.prototype)
//Function.__proto__ == Function.prototype??? true

image.png

image.png

Object 在 JavaScript 中扮演三重角色:

  • 构造函数:用于创建对象

  • 命名空间:提供一系列静态方法用于对象操作

  • 原型终点:Object.prototype 是所有原型链的终点,在往上没有了,值==null

请看VCR:

function Person (){};

const persoon01 = new Person();
const obj = {};//通过对象字面量{}创建obj实例
const obj1 = new Object();//通过构造函数new Object()创建obj1实例
const obj2 = Object.create(Object.prototype);//通过委托创建,或者叫原型创建,来创建obj2实例

console.log("Person.prototype.__proto__指向",Person.prototype.__proto__);
//Person.prototype.__proto__指向 [Object: null prototype] {}

console.log("Function.prototype.__proto__指向",Function.prototype.__proto__)
//Function.prototype.__proto__指向 [Object: null prototype] {}

console.log("通过对象字面量{}创建的obj实例,obj.__proto__指向",obj.__proto__);
//通过对象字面量{}创建的obj实例,obj.__proto__指向 [Object: null prototype] {}

console.log("通过构造函数new Object()创建obj1实例,指向",obj1.__proto__);
//通过构造函数new Object()创建obj1实例,指向 [Object: null prototype] {}

console.log("通过委托创建,或者叫原型创建,来创建obj2实例,指向",obj2.__proto__);
//通过委托创建,或者叫原型创建,来创建obj2实例,指向 [Object: null prototype] {}

image.png

image.png

Function和Object的关系

  • 相互依赖的循环引用
    • Object 是 Function 的实例(构造函数层面)

    • Function 是 Object 的子类(原型继承层面)

    • 这是 JavaScript 的自举(Bootstrap)机制

根据关系总览图,我们可以看到,Function和Object,它们两形成了一个闭环,将所有的函数和对象都包裹在这个闭环里

📋 JavaScript 原型系统核心概念表

概念 描述 示例 特殊说明
prototype 函数特有,指向原型对象 Person.prototype 只有函数对象才有此属性
proto 所有对象都有,指向构造函数的原型 person.__proto__ 实际应使用 Object.getPrototypeOf()
constructor 指向创建该对象的构造函数 Person.prototype.constructor 可被修改,查找时沿原型链进行
原型链查找 通过 __proto__ 逐级向上查找 person.__proto__.__proto__ 终点为 null
Function 所有函数的构造函数 Function.prototype Function.__proto__ === Function.prototype
Object 所有对象的基类 Object.prototype 原型链终点,Object.prototype.__proto__ === null

🔍 补充说明

prototype 补充

  • 函数的 prototype 属性默认包含 constructor 属性指向函数自身
  • 用于实现基于原型的继承

proto 补充

  • 现在更推荐使用 Object.getPrototypeOf(obj)Object.setPrototypeOf(obj, proto)
  • __proto__ 是访问器属性,不是数据属性

constructor 补充

  • constructor 属性可以通过原型链查找
  • 示例:person.constructor === Person(实际查找的是 person.__proto__.constructor

原型链查找补充

  • 当访问对象属性时,如果对象自身没有,会沿着原型链向上查找
  • 直到找到该属性或到达原型链终点 null

Function 补充

  • 是所有函数的构造函数,包括内置构造函数(Object、Array等)和自定义函数
  • 自身也是函数,所以 Function.__proto__ === Function.prototype

Object 补充

  • Object.prototype 是所有原型链的最终原型对象
  • 通过 Object.create(null) 可以创建没有原型的"纯净对象"

💡 记忆口诀

  • 函数看prototype,实例看__proto__
  • constructor找根源,原型链上寻答案
  • Object是终点,Function是关键

结语:

看完这篇文章,你应该可以读懂上面的关系总览图了,望学习愉快!!!

护航隐私!小程序纯前端“证件加水印”:OffscreenCanvas 全屏平铺实战

作者 小皮虾
2025年12月12日 16:34

1. 背景与痛点:证件“裸奔”的风险

在日常生活中,我们经常需要上传身份证、驾照或房产证照片来办理各种业务。然而,直接发送原图存在巨大的安全隐患:

  • 被二次盗用:不法分子可能将你的证件照用于网贷、注册账号等非法用途。
  • 服务器隐私泄露:如果使用在线工具加水印,图片必须上传到第三方服务器,这就好比“把钥匙交给陌生人保管”,风险不可控。

为了解决这一痛点,可利用小程序的 OffscreenCanvas 能力,在用户手机本地毫秒级合成水印,图片数据永远不会离开用户手机

2. 核心思路:离屏渲染 + 矩阵平铺

实现全屏倾斜水印,主要难点在于坐标计算性能平衡。我们的方案如下:

  1. 离屏渲染 (OffscreenCanvas): 使用离屏画布在内存中处理,避免页面闪烁,且支持高性能的 2D 渲染模式。
  2. 智能 DPR 降级: 沿用我们之前文章提到的防爆内存策略。证件照通常分辨率很高,必须计算安全尺寸,防止 Canvas 内存溢出闪退。
  3. 矩阵平铺算法: 不简单的旋转画布,而是采用 “保存环境 -> 平移 -> 旋转 -> 绘制 -> 恢复环境” 的策略,在一个网格循环中将文字铺满全屏,确保无论图片比例如何,水印都能均匀分布。

3. 硬核代码实现

以下是封装好的 watermarkUtils.js。包含了智能 DPR 计算全屏水印绘制的核心逻辑。

// utils/watermarkUtils.js

// 1. 获取系统基础信息
const wxt = {
  dpr: wx.getSystemInfoSync().pixelRatio || 2
};

// 图片缓存,避免重复加载
const cacheCanvasImageMap = new Map();

/**
 * 内部方法:获取/创建 Canvas Image 对象
 */
async function getCanvasImage(canvas, imageUrl) {
  if (cacheCanvasImageMap.has(imageUrl)) return cacheCanvasImageMap.get(imageUrl);
  
  // 兼容性处理:若不支持 Promise.withResolvers,请改用 new Promise
  const { promise, resolve, reject } = Promise.withResolvers();
  const image = canvas.createImage();
  image.onload = () => {
    cacheCanvasImageMap.set(imageUrl, image);
    resolve(image);
  };
  image.onerror = (e) => reject(new Error(`加载失败: ${e.errMsg}`));
  image.src = imageUrl;
  await promise;
  return image;
}

/**
 * 给图片添加全屏倾斜水印
 * @param {string} imageUrl 图片路径
 * @param {string} text 水印文字,如 "仅供办理租房业务使用"
 * @param {object} options 配置项 { color, size, opacity }
 */
export async function addWatermark(imageUrl, text = '仅供办理业务使用', options = {}) {
  // 默认配置
  const config = {
    color: '#aaaaaa',
    opacity: 0.5,
    fontSize: 0, // 0 表示自动计算
    gap: 100,    // 水印间距
    ...options
  };

  const offscreenCanvas = wx.createOffscreenCanvas({ type: '2d' });
  const image = await getCanvasImage(offscreenCanvas, imageUrl);
  const { width, height } = image;

  // --- ⚡️ 性能优化:智能 DPR 计算 (防止大图闪退) ---
  const LIMIT_SIZE = 4096; 
  let useDpr = wxt.dpr;
  if (Math.max(width, height) * useDpr > LIMIT_SIZE) {
    useDpr = LIMIT_SIZE / Math.max(width, height);
  }

  // 设置画布尺寸
  offscreenCanvas.width = width * useDpr;
  offscreenCanvas.height = height * useDpr;

  const ctx = offscreenCanvas.getContext('2d');
  ctx.scale(useDpr, useDpr);
  
  // 1. 绘制底图
  ctx.drawImage(image, 0, 0, width, height);

  // 2. 配置水印样式
  // 自动计算字号:约为图片宽度的 4%
  const fontSize = config.fontSize || Math.floor(width * 0.04); 
  ctx.font = `bold ${fontSize}px sans-serif`;
  ctx.fillStyle = config.color;
  ctx.globalAlpha = config.opacity;
  ctx.textBaseline = 'middle';
  ctx.textAlign = 'center';

  // 3. 计算平铺逻辑
  // 旋转 45 度后,覆盖范围需要比原图大,这里简单取对角线长度作为边界
  const maxSize = Math.sqrt(width * width + height * height);
  // 步长 = 文字宽度 + 间距
  const step = ctx.measureText(text).width + config.gap; 
  
  // 4. 循环绘制水印
  // 从负坐标开始绘制,确保旋转后边缘也有水印
  for (let x = -maxSize; x < maxSize; x += step) {
    for (let y = -maxSize; y < maxSize; y += step) {
      ctx.save();
      
      // 核心变换:平移到网格点 -> 旋转 -> 绘制
      ctx.translate(x, y);
      ctx.rotate(-45 * Math.PI / 180); // 逆时针旋转 45 度
      ctx.fillText(text, 0, 0);
      
      ctx.restore();
    }
  }

  // 5. 导出图片
  const res = await wx.canvasToTempFilePath({
    canvas: offscreenCanvas,
    fileType: 'jpg',
    quality: 0.8, // 稍微压缩以减小体积
  });

  return res.tempFilePath;
}

4. 业务调用示例

在小程序页面中,用户选择图片并输入水印文字后,实时预览效果。

// pages/watermark/index.js
import { addWatermark } from '../../utils/watermarkUtils';

Page({
  data: {
    originImg: '',
    resultImg: '',
    watermarkText: '仅供本次业务使用 他用无效'
  },

  async onAddWatermark() {
    if (!this.data.originImg) return;

    wx.showLoading({ title: '安全合成中...' });
    
    try {
      const tempFilePath = await addWatermark(
        this.data.originImg, 
        this.data.watermarkText,
        {
          color: '#ffffff', // 白色水印
          opacity: 0.4,     // 半透明
          gap: 120          // 间距疏松一点
        }
      );
      
      this.setData({ resultImg: tempFilePath });
      
    } catch (err) {
      console.error(err);
      wx.showToast({ title: '合成失败', icon: 'none' });
    } finally {
      wx.hideLoading();
    }
  }
})

5. 避坑与实战经验

  1. 自动字号的重要性: 不要写死 fontSize = 20px。用户上传的图片分辨率差异极大(有的 500px 宽,有的 4000px 宽)。最佳实践是根据图片宽度动态计算字号(如 width * 0.04),这样无论处理缩略图还是 4K 原图,水印比例看起来都是协调的。
  2. 平铺范围的陷阱: 因为文字需要旋转 45 度,如果循环只从 0width,图片的左下角和右上角可能会出现空白。代码中我们从 -maxSize(负数区域)开始循环,确保旋转后的文字能完全覆盖画布的每一个角落。
  3. 隐私第一: 在工具的 UI 界面上,建议显著提示 “纯本地处理,无上传服务器”,这能极大地增加用户的信任感,提升工具的使用率。

写在最后

通过帮小忙工具箱的这个实践案例,我们可以看到,利用小程序强大的 Canvas 能力,开发者完全可以在保护用户隐私的前提下,提供专业级的图片处理服务。

技术不只是代码,更是对用户安全的守护。 希望这篇分享能帮你在小程序中实现更安全、更高效的功能!

Vue 3 动画效果实现:Transition和TransitionGroup详解

作者 微芒不朽
2025年12月12日 16:22

Vue 3 动画效果实现:Transition和TransitionGroup详解

前言

在现代Web应用中,流畅的动画效果不仅能提升用户体验,还能有效传达界面状态变化的信息。Vue 3 提供了强大的过渡和动画系统,通过 <transition><transition-group> 组件,开发者可以轻松地为元素的进入、离开和列表变化添加动画效果。本文将深入探讨这两个组件的使用方法和高级技巧。

Transition 组件基础

基本用法

<transition> 组件用于包装单个元素或组件,在插入、更新或移除时应用过渡效果。

<template>
  <div>
    <button @click="show = !show">切换显示</button>
    <transition name="fade">
      <p v-if="show">Hello Vue 3!</p>
    </transition>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const show = ref(true)
</script>

<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

过渡类名详解

Vue 3 为进入/离开过渡提供了6个CSS类名:

  1. v-enter-from:进入过渡的开始状态
  2. v-enter-active:进入过渡生效时的状态
  3. v-enter-to:进入过渡的结束状态
  4. v-leave-from:离开过渡的开始状态
  5. v-leave-active:离开过渡生效时的状态
  6. v-leave-to:离开过渡的结束状态

注意:在 Vue 3 中,类名前缀从 v-enter 改为 v-enter-from,其他类名也相应调整。

JavaScript 钩子函数

除了CSS过渡,还可以使用JavaScript钩子来控制动画:

<template>
  <transition
    @before-enter="beforeEnter"
    @enter="enter"
    @after-enter="afterEnter"
    @before-leave="beforeLeave"
    @leave="leave"
    @after-leave="afterLeave"
  >
    <div v-if="show" class="box">Animated Box</div>
  </transition>
</template>

<script setup>
import { ref } from 'vue'
import gsap from 'gsap'

const show = ref(true)

const beforeEnter = (el) => {
  el.style.opacity = 0
  el.style.transform = 'scale(0)'
}

const enter = (el, done) => {
  gsap.to(el, {
    duration: 0.5,
    opacity: 1,
    scale: 1,
    onComplete: done
  })
}

const afterEnter = (el) => {
  console.log('进入完成')
}

const beforeLeave = (el) => {
  el.style.transformOrigin = 'center'
}

const leave = (el, done) => {
  gsap.to(el, {
    duration: 0.5,
    opacity: 0,
    scale: 0,
    onComplete: done
  })
}

const afterLeave = (el) => {
  console.log('离开完成')
}
</script>

常见动画效果实现

1. 淡入淡出效果

<template>
  <div class="demo">
    <button @click="show = !show">Toggle Fade</button>
    <transition name="fade">
      <div v-if="show" class="content">Fade Effect Content</div>
    </transition>
  </div>
</template>

<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease-in-out;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

2. 滑动效果

<template>
  <div class="demo">
    <button @click="show = !show">Toggle Slide</button>
    <transition name="slide">
      <div v-if="show" class="content">Slide Effect Content</div>
    </transition>
  </div>
</template>

<style>
.slide-enter-active,
.slide-leave-active {
  transition: all 0.3s ease;
  max-height: 200px;
  overflow: hidden;
}

.slide-enter-from,
.slide-leave-to {
  max-height: 0;
  opacity: 0;
  transform: translateY(-20px);
}
</style>

3. 弹跳效果

<template>
  <div class="demo">
    <button @click="show = !show">Toggle Bounce</button>
    <transition name="bounce">
      <div v-if="show" class="content">Bounce Effect Content</div>
    </transition>
  </div>
</template>

<style>
.bounce-enter-active {
  animation: bounce-in 0.5s;
}

.bounce-leave-active {
  animation: bounce-in 0.5s reverse;
}

@keyframes bounce-in {
  0% {
    transform: scale(0);
    opacity: 0;
  }
  50% {
    transform: scale(1.2);
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}
</style>

4. 翻转效果

<template>
  <div class="demo">
    <button @click="show = !show">Toggle Flip</button>
    <transition name="flip">
      <div v-if="show" class="content flip-content">Flip Effect Content</div>
    </transition>
  </div>
</template>

<style>
.flip-enter-active {
  animation: flip-in 0.6s ease forwards;
}

.flip-leave-active {
  animation: flip-out 0.6s ease forwards;
}

@keyframes flip-in {
  0% {
    transform: perspective(400px) rotateY(90deg);
    opacity: 0;
  }
  40% {
    transform: perspective(400px) rotateY(-10deg);
  }
  70% {
    transform: perspective(400px) rotateY(10deg);
  }
  100% {
    transform: perspective(400px) rotateY(0deg);
    opacity: 1;
  }
}

@keyframes flip-out {
  0% {
    transform: perspective(400px) rotateY(0deg);
    opacity: 1;
  }
  100% {
    transform: perspective(400px) rotateY(90deg);
    opacity: 0;
  }
}
</style>

TransitionGroup 组件详解

基本列表动画

<transition-group> 用于为列表中的元素添加进入/离开过渡效果:

<template>
  <div class="list-demo">
    <button @click="addItem">添加项目</button>
    <button @click="removeItem">删除项目</button>
  
    <transition-group name="list" tag="ul">
      <li v-for="item in items" :key="item.id" class="list-item">
        {{ item.text }}
      </li>
    </transition-group>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'

const items = reactive([
  { id: 1, text: '项目 1' },
  { id: 2, text: '项目 2' },
  { id: 3, text: '项目 3' }
])

let nextId = 4

const addItem = () => {
  const index = Math.floor(Math.random() * (items.length + 1))
  items.splice(index, 0, {
    id: nextId++,
    text: `新项目 ${nextId - 1}`
  })
}

const removeItem = () => {
  if (items.length > 0) {
    const index = Math.floor(Math.random() * items.length)
    items.splice(index, 1)
  }
}
</script>

<style>
.list-enter-active,
.list-leave-active {
  transition: all 0.5s ease;
}

.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}

.list-move {
  transition: transform 0.5s ease;
}

.list-item {
  padding: 10px;
  margin: 5px 0;
  background-color: #f0f0f0;
  border-radius: 4px;
}
</style>

列表排序动画

<template>
  <div class="shuffle-demo">
    <button @click="shuffle">随机排序</button>
    <button @click="add">添加</button>
    <button @click="remove">删除</button>
  
    <transition-group name="shuffle" tag="div" class="grid">
      <div 
        v-for="item in items" 
        :key="item.id" 
        class="grid-item"
        @click="removeItem(item)"
      >
        {{ item.number }}
      </div>
    </transition-group>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'

const items = reactive([
  { id: 1, number: 1 },
  { id: 2, number: 2 },
  { id: 3, number: 3 },
  { id: 4, number: 4 },
  { id: 5, number: 5 }
])

const shuffle = () => {
  // Fisher-Yates 洗牌算法
  for (let i = items.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [items[i], items[j]] = [items[j], items[i]]
  }
}

const add = () => {
  const newNumber = items.length > 0 ? Math.max(...items.map(i => i.number)) + 1 : 1
  items.push({
    id: Date.now(),
    number: newNumber
  })
}

const remove = () => {
  if (items.length > 0) {
    items.pop()
  }
}

const removeItem = (item) => {
  const index = items.indexOf(item)
  if (index > -1) {
    items.splice(index, 1)
  }
}
</script>

<style>
.grid {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 20px;
}

.grid-item {
  width: 60px;
  height: 60px;
  background-color: #42b883;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  cursor: pointer;
  font-weight: bold;
  user-select: none;
}

.shuffle-enter-active,
.shuffle-leave-active {
  transition: all 0.5s ease;
}

.shuffle-enter-from {
  opacity: 0;
  transform: scale(0.5);
}

.shuffle-leave-to {
  opacity: 0;
  transform: scale(0.5);
}

.shuffle-move {
  transition: transform 0.5s ease;
}
</style>

高级动画技巧

1. FLIP 技术实现平滑动画

FLIP (First, Last, Invert, Play) 是一种优化动画性能的技术:

<template>
  <div class="flip-demo">
    <button @click="filterItems">筛选奇数</button>
    <button @click="resetFilter">重置</button>
  
    <transition-group 
      name="flip-list" 
      tag="div" 
      class="flip-container"
      @before-enter="beforeEnter"
      @enter="enter"
      @leave="leave"
    >
      <div 
        v-for="item in filteredItems" 
        :key="item.id" 
        class="flip-item"
      >
        {{ item.value }}
      </div>
    </transition-group>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const items = ref(Array.from({ length: 20 }, (_, i) => ({
  id: i + 1,
  value: i + 1
})))

const filterOdd = ref(false)

const filteredItems = computed(() => {
  return filterOdd.value 
    ? items.value.filter(item => item.value % 2 === 1)
    : items.value
})

const filterItems = () => {
  filterOdd.value = true
}

const resetFilter = () => {
  filterOdd.value = false
}

const positions = new Map()

const beforeEnter = (el) => {
  el.style.opacity = '0'
  el.style.transform = 'scale(0.8)'
}

const enter = (el, done) => {
  // 获取最终位置
  const end = el.getBoundingClientRect()
  const start = positions.get(el)

  if (start) {
    // 计算位置差
    const dx = start.left - end.left
    const dy = start.top - end.top
    const ds = start.width / end.width
  
    // 反向变换
    el.style.transform = `translate(${dx}px, ${dy}px) scale(${ds})`
  
    // 强制重绘
    el.offsetHeight
  
    // 执行动画
    el.style.transition = 'all 0.3s ease'
    el.style.transform = ''
    el.style.opacity = '1'
  
    setTimeout(done, 300)
  } else {
    el.style.transition = 'all 0.3s ease'
    el.style.transform = ''
    el.style.opacity = '1'
    setTimeout(done, 300)
  }
}

const leave = (el, done) => {
  // 记录初始位置
  positions.set(el, el.getBoundingClientRect())
  el.style.position = 'absolute'
  done()
}
</script>

<style>
.flip-container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
  gap: 10px;
  position: relative;
  min-height: 200px;
}

.flip-item {
  background-color: #3498db;
  color: white;
  padding: 20px;
  text-align: center;
  border-radius: 8px;
  font-weight: bold;
}

.flip-list-enter-active,
.flip-list-leave-active {
  transition: all 0.3s ease;
}

.flip-list-enter-from,
.flip-list-leave-to {
  opacity: 0;
  transform: translateY(30px);
}

.flip-list-move {
  transition: transform 0.3s ease;
}
</style>

2. 交错动画

<template>
  <div class="stagger-demo">
    <button @click="loadItems">加载项目</button>
    <button @click="clearItems">清空</button>
  
    <transition-group 
      name="staggered-fade" 
      tag="ul" 
      class="staggered-list"
    >
      <li 
        v-for="(item, index) in items" 
        :key="item.id"
        :data-index="index"
        class="staggered-item"
      >
        {{ item.text }}
      </li>
    </transition-group>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const items = ref([])

const loadItems = () => {
  items.value = Array.from({ length: 10 }, (_, i) => ({
    id: Date.now() + i,
    text: `项目 ${i + 1}`
  }))
}

const clearItems = () => {
  items.value = []
}
</script>

<style>
.staggered-list {
  list-style: none;
  padding: 0;
}

.staggered-item {
  padding: 15px;
  margin: 5px 0;
  background-color: #e74c3c;
  color: white;
  border-radius: 6px;
  opacity: 0;
}

/* 进入动画 */
.staggered-fade-enter-active {
  transition: all 0.3s ease;
}

.staggered-fade-enter-from {
  opacity: 0;
  transform: translateX(-30px);
}

/* 离开动画 */
.staggered-fade-leave-active {
  transition: all 0.3s ease;
  position: absolute;
}

.staggered-fade-leave-to {
  opacity: 0;
  transform: translateX(30px);
}

/* 移动动画 */
.staggered-fade-move {
  transition: transform 0.3s ease;
}

/* 交错延迟 */
.staggered-item:nth-child(1) { transition-delay: 0.05s; }
.staggered-item:nth-child(2) { transition-delay: 0.1s; }
.staggered-item:nth-child(3) { transition-delay: 0.15s; }
.staggered-item:nth-child(4) { transition-delay: 0.2s; }
.staggered-item:nth-child(5) { transition-delay: 0.25s; }
.staggered-item:nth-child(6) { transition-delay: 0.3s; }
.staggered-item:nth-child(7) { transition-delay: 0.35s; }
.staggered-item:nth-child(8) { transition-delay: 0.4s; }
.staggered-item:nth-child(9) { transition-delay: 0.45s; }
.staggered-item:nth-child(10) { transition-delay: 0.5s; }
</style>

3. 页面切换动画

<!-- App.vue -->
<template>
  <div id="app">
    <nav>
      <router-link to="/">首页</router-link>
      <router-link to="/about">关于</router-link>
      <router-link to="/contact">联系</router-link>
    </nav>
  
    <router-view v-slot="{ Component }">
      <transition name="page" mode="out-in">
        <component :is="Component" />
      </transition>
    </router-view>
  </div>
</template>

<style>
.page-enter-active,
.page-leave-active {
  transition: all 0.3s ease;
  position: absolute;
  top: 60px;
  left: 0;
  right: 0;
}

.page-enter-from {
  opacity: 0;
  transform: translateX(30px);
}

.page-leave-to {
  opacity: 0;
  transform: translateX(-30px);
}

nav {
  padding: 20px;
  background-color: #f8f9fa;
}

nav a {
  margin-right: 20px;
  text-decoration: none;
  color: #333;
}

nav a.router-link-active {
  color: #42b883;
  font-weight: bold;
}
</style>

性能优化建议

1. 使用 transform 和 opacity

优先使用 transformopacity 属性,因为它们不会触发重排:

/* 推荐 */
.good-animation {
  transition: transform 0.3s ease, opacity 0.3s ease;
}

/* 避免 */
.bad-animation {
  transition: left 0.3s ease, top 0.3s ease;
}

2. 合理使用 will-change

对于复杂的动画,可以提前告知浏览器优化:

.animated-element {
  will-change: transform, opacity;
}

3. 避免阻塞主线程

对于复杂动画,考虑使用 Web Workers 或 requestAnimationFrame:

const animateElement = (element, duration) => {
  const startTime = performance.now()

  const animate = (currentTime) => {
    const elapsed = currentTime - startTime
    const progress = Math.min(elapsed / duration, 1)
  
    // 更新元素样式
    element.style.transform = `translateX(${progress * 100}px)`
  
    if (progress < 1) {
      requestAnimationFrame(animate)
    }
  }

  requestAnimationFrame(animate)
}

结语

Vue 3 的过渡和动画系统为我们提供了强大而灵活的工具来创建丰富的用户界面体验。通过合理运用 <transition><transition-group> 组件,结合 CSS3 动画和 JavaScript 控制,我们能够实现从简单到复杂的各种动画效果。

关键要点总结:

  1. 理解过渡类名机制:掌握6个核心类名的作用时机
  2. 善用 JavaScript 钩子:实现更复杂的自定义动画逻辑
  3. 列表动画的重要性:使用 <transition-group> 处理动态列表
  4. 性能优化意识:选择合适的 CSS 属性和动画技术
  5. 用户体验考量:动画应该增强而不是阻碍用户操作

在实际项目中,建议根据具体需求选择合适的动画方案,并始终考虑性能影响。适度的动画能够显著提升用户体验,但过度或不当的动画反而会适得其反。希望本文能够帮助你在 Vue 3 项目中更好地实现和控制动画效果。

别再用mixin了!Vue3自定义Hooks让逻辑复用爽到飞起

作者 微芒不朽
2025年12月12日 16:13

前言

随着 Vue 3 的普及,Composition API 成为了构建复杂应用的主流方式。相比 Options API,Composition API 提供了更好的逻辑组织和复用能力。而自定义 Hooks 正是这一能力的核心体现,它让我们能够将业务逻辑抽象成可复用的函数,极大地提升了代码的可维护性和开发效率。

什么是自定义 Hooks?

自定义 Hooks 是基于 Composition API 封装的可复用逻辑函数。它们通常以 use 开头命名,返回响应式数据、方法或计算属性。通过自定义 Hooks,我们可以将组件中的逻辑抽离出来,在多个组件间共享。

基本结构

// useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)

  const increment = () => {
    count.value++
  }

  const decrement = () => {
    count.value--
  }

  const doubleCount = computed(() => count.value * 2)

  return {
    count,
    increment,
    decrement,
    doubleCount
  }
}

实战案例:常用自定义 Hooks

1. 网络请求 Hook

// useApi.js
import { ref, onMounted } from 'vue'
import axios from 'axios'

export function useApi(url, options = {}) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)

  const fetchData = async (params = {}) => {
    loading.value = true
    error.value = null
  
    try {
      const response = await axios.get(url, { ...options, params })
      data.value = response.data
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  onMounted(() => {
    if (options.immediate !== false) {
      fetchData()
    }
  })

  return {
    data,
    loading,
    error,
    fetchData
  }
}

使用示例:

<template>
  <div>
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">{{ error }}</div>
    <ul v-else>
      <li v-for="item in data" :key="item.id">
        {{ item.name }}
      </li>
    </ul>
    <button @click="fetchData">刷新</button>
  </div>
</template>

<script setup>
import { useApi } from '@/hooks/useApi'

const { data, loading, error, fetchData } = useApi('/api/users')
</script>

2. 表单验证 Hook

// useForm.js
import { reactive, computed } from 'vue'

export function useForm(initialValues, rules) {
  const formData = reactive({ ...initialValues })
  const errors = reactive({})

  const validateField = (field) => {
    const value = formData[field]
    const fieldRules = rules[field] || []
  
    for (const rule of fieldRules) {
      if (!rule.validator(value, formData)) {
        errors[field] = rule.message
        return false
      }
    }
  
    delete errors[field]
    return true
  }

  const validateAll = () => {
    let isValid = true
    Object.keys(rules).forEach(field => {
      if (!validateField(field)) {
        isValid = false
      }
    })
    return isValid
  }

  const resetForm = () => {
    Object.assign(formData, initialValues)
    Object.keys(errors).forEach(key => {
      delete errors[key]
    })
  }

  const isDirty = computed(() => {
    return JSON.stringify(formData) !== JSON.stringify(initialValues)
  })

  return {
    formData,
    errors,
    validateField,
    validateAll,
    resetForm,
    isDirty
  }
}

使用示例:

<template>
  <form @submit.prevent="handleSubmit">
    <div>
      <input 
        v-model="formData.username" 
        @blur="() => validateField('username')"
        placeholder="用户名"
      />
      <span v-if="errors.username" class="error">{{ errors.username }}</span>
    </div>
  
    <div>
      <input 
        v-model="formData.email" 
        @blur="() => validateField('email')"
        placeholder="邮箱"
      />
      <span v-if="errors.email" class="error">{{ errors.email }}</span>
    </div>
  
    <button type="submit" :disabled="!isDirty">提交</button>
    <button type="button" @click="resetForm">重置</button>
  </form>
</template>

<script setup>
import { useForm } from '@/hooks/useForm'

const { formData, errors, validateField, validateAll, resetForm, isDirty } = useForm(
  { username: '', email: '' },
  {
    username: [
      {
        validator: (value) => value.length >= 3,
        message: '用户名至少3个字符'
      }
    ],
    email: [
      {
        validator: (value) => /\S+@\S+\.\S+/.test(value),
        message: '请输入有效的邮箱地址'
      }
    ]
  }
)

const handleSubmit = () => {
  if (validateAll()) {
    console.log('表单验证通过:', formData)
  }
}
</script>

3. 防抖节流 Hook

// useDebounce.js
import { ref, watch } from 'vue'

export function useDebounce(value, delay = 300) {
  const debouncedValue = ref(value.value)
  let timeoutId = null

  watch(value, (newValue) => {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => {
      debouncedValue.value = newValue
    }, delay)
  })

  return debouncedValue
}

// useThrottle.js
export function useThrottle(value, delay = 300) {
  const throttledValue = ref(value.value)
  let lastTime = 0

  watch(value, (newValue) => {
    const now = Date.now()
    if (now - lastTime >= delay) {
      throttledValue.value = newValue
      lastTime = now
    }
  })

  return throttledValue
}

4. 本地存储 Hook

// useLocalStorage.js
import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
  const storedValue = localStorage.getItem(key)
  const value = ref(storedValue ? JSON.parse(storedValue) : defaultValue)

  watch(value, (newValue) => {
    if (newValue === null) {
      localStorage.removeItem(key)
    } else {
      localStorage.setItem(key, JSON.stringify(newValue))
    }
  }, { deep: true })

  const remove = () => {
    value.value = null
  }

  return [value, remove]
}

高级技巧与最佳实践

1. Hook 组合

// useUserManagement.js
import { useApi } from './useApi'
import { useLocalStorage } from './useLocalStorage'

export function useUserManagement() {
  const [currentUser, removeCurrentUser] = useLocalStorage('currentUser', null)
  const { data: users, loading, error, fetchData } = useApi('/api/users')

  const login = async (credentials) => {
    // 登录逻辑
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(credentials)
    })
    const userData = await response.json()
    currentUser.value = userData
  }

  const logout = () => {
    removeCurrentUser()
    // 其他登出逻辑
  }

  return {
    currentUser,
    users,
    loading,
    error,
    login,
    logout,
    refreshUsers: fetchData
  }
}

2. 错误处理

// useAsync.js
import { ref, onMounted } from 'vue'

export function useAsync(asyncFunction, immediate = true) {
  const result = ref(null)
  const loading = ref(false)
  const error = ref(null)

  const execute = async (...args) => {
    loading.value = true
    error.value = null
  
    try {
      const response = await asyncFunction(...args)
      result.value = response
      return response
    } catch (err) {
      error.value = err
      throw err
    } finally {
      loading.value = false
    }
  }

  onMounted(() => {
    if (immediate) {
      execute()
    }
  })

  return {
    result,
    loading,
    error,
    execute
  }
}

3. 类型安全(TypeScript)

// useCounter.ts
import { ref, computed, Ref, ComputedRef } from 'vue'

interface UseCounterReturn {
  count: Ref<number>
  increment: () => void
  decrement: () => void
  doubleCount: ComputedRef<number>
}

export function useCounter(initialValue: number = 0): UseCounterReturn {
  const count = ref(initialValue)

  const increment = () => {
    count.value++
  }

  const decrement = () => {
    count.value--
  }

  const doubleCount = computed(() => count.value * 2)

  return {
    count,
    increment,
    decrement,
    doubleCount
  }
}

设计原则与注意事项

1. 单一职责原则

每个 Hook 应该只负责一个特定的功能领域,保持功能单一且专注。

2. 命名规范

  • 使用 use 前缀
  • 名称清晰表达 Hook 的用途
  • 避免过于通用的名称

3. 返回值设计

  • 返回对象而非数组(便于解构时命名)
  • 保持返回值的一致性
  • 考虑添加辅助方法

4. 性能优化

  • 合理使用 watchcomputed
  • 避免不必要的重新计算
  • 及时清理副作用

结语

自定义 Hooks 是 Vue 3 Composition API 生态中的重要组成部分,它不仅解决了逻辑复用的问题,更提供了一种更加灵活和可组合的开发模式。通过合理地设计和使用自定义 Hooks,我们可以:

  1. 提升代码复用性:将通用逻辑抽象成独立模块
  2. 改善代码组织:让组件更加关注视图逻辑
  3. 增强可测试性:独立的逻辑更容易进行单元测试
  4. 提高开发效率:减少重复代码编写

在实际项目中,建议根据业务需求逐步积累和优化自定义 Hooks,建立属于团队的 Hooks 库,这将是提升前端开发质量和效率的重要手段。

记住,好的自定义 Hooks 不仅要解决当前问题,更要具备良好的扩展性和可维护性。随着经验的积累,你会发现自己能够创造出越来越优雅和实用的自定义 Hooks。

Vite开发环境按需编译是怎么实现的?

作者 sorryhc
2025年12月11日 14:55

前言

Vite的快,我们并不陌生,主要体现在开发环境时的体验。

而相较于其他构建工具,Vite核心是依靠了现代浏览器对于原生esm模块的支持+按需实时编译将性能达到了极致。

我们基于源码来看看esbuild编译的完整过程。

核心流程图

Browser Request
    ↓
Vite DevServer (Connect 中间件)
    ↓
请求路由判断
    ├─ /.vite/client → 注入客户端代码
    ├─ /@modules/* → node_modules 导入
    ├─ /src/* → 源代码文件
    └─ *.json, *.css 等 → 特殊处理
    ↓
ModuleGraph 缓存检查
    ├─ 命中缓存 → 返回
    └─ 未命中 → esbuild 编译
    ↓
TransformPlugin 流程
    ├─ pre plugins
    ├─ esbuild transform
    └─ post plugins
    ↓
发送给浏览器 (ES Modules)

DevServer入口代码

这里初始化了开发服务器、模块图(缓存系统)、很多中间件(用于拦截实时编译)。

// packages/vite/src/node/server/index.ts

import connect from 'connect'
import { createPluginContainer } from './pluginContainer'

export async function createServer(inlineConfig: InlineConfig = {}) {
  const config = await resolveConfig(inlineConfig, 'serve')
  
  // 创建 Express-like 应用
  const middlewares = connect()
  const httpServer = createHttpServer(middlewares)
  
  // 创建模块图(缓存系统)
  const moduleGraph = new ModuleGraph((url) =>
    pluginContainer.resolveId(url)
  )
  
  // 创建插件容器(执行插件)
  const pluginContainer = await createPluginContainer(config)
  
  // 核心中间件们
  middlewares.use(timeMiddleware)
  middlewares.use(cors)
  middlewares.use(transformMiddleware(server))  // ⭐ 重点
  middlewares.use(servePublicDir)
  middlewares.use(serveRawFs)
  
  const server = {
    middlewares,
    httpServer,
    moduleGraph,
    pluginContainer,
    ws: createWebSocketServer(httpServer),
    // ... 其他属性
  }
  
  return server
}

Transform中间件(请求拦截)

这里是一个很经典的例子,从浏览器发起第一次main.ts请求开始,Vite做了ts文件的转换。

而后续的请求会从main.ts中发起。

// packages/vite/src/node/server/middlewares/transform.ts

export function transformMiddleware(server: ViteDevServer) {
  return async (req: IncomingMessage, res: ServerResponse, next: NextFunction) => {
    if (req.method !== 'GET' || isSkipped(req.url)) {
      return next()
    }

    let url = req.url
    const { pathname, search, hash } = new URL(url, `http://${req.headers.host}`)
    
    // 示例:/src/main.ts?t=123 → /src/main.ts
    url = pathname + search + hash

    try {
      // ⭐ 核心:调用加载和转换
      const result = await transformRequest(url, server, {
        raw: req.headers['accept']?.includes('application/octet-stream'),
      })

      if (result) {
        const type = isDirectCSSRequest(url) ? 'text/css' : 'application/javascript'
        res.setHeader('Content-Type', type)
        res.setHeader('Cache-Control', 'no-cache')
        res.setHeader('ETag', getEtag(result.code))
        
        return res.end(result.code)
      }
    } catch (e) {
      // 错误处理
      if (e.code === 'ENOENT') {
        return next()
      }
      // HMR 错误通知浏览器
      server.ws.send({
        type: 'error',
        event: 'vite:error',
        err: e,
      })
    }

    next()
  }
}

请求转换核心逻辑

这里是核心的源码转换逻辑,基于源码优先从模块缓存表中取,如果没有才走该模块的首次转换,最后会落到缓存中。

// packages/vite/src/node/server/transformRequest.ts

export async function transformRequest(
  url: string,
  server: ViteDevServer,
  options?: TransformOptions,
) {
  // 1️⃣ 获取文件内容 + 元数据
  const { code: raw, map } = await loadRawRequest(url, server)
  
  let code = raw
  const inMap = map

  // 2️⃣ 检查缓存
  const cached = server.moduleGraph.getModuleByUrl(url)
  if (!server.config.command === 'serve' && cached?.transformedCode) {
    return {
      code: cached.transformedCode,
      map: cached.map,
    }
  }

  // 3️⃣ 执行插件转换
  const result = await pluginContainer.transform(code, url)
  if (result) {
    code = result.code
  }

  // 4️⃣ 特殊处理:自动导入注入
  if (!options?.raw) {
    code = injectHelper(code, url)
  }

  // 5️⃣ 缓存结果
  server.moduleGraph.updateModuleInfo(url, {
    transformedCode: code,
    map: result?.map,
  })

  return { code, map: result?.map }
}

加载原始请求(磁盘读写)

而加载和编译源码则是直接通过esbuild能力来实现。

// packages/vite/src/node/server/transformRequest.ts

async function loadRawRequest(url: string, server: ViteDevServer) {
  let id = decodeURIComponent(parseUrl(url).pathname)
  
  // ⭐ 调用插件的 resolveId hook
  const resolveResult = await server.pluginContainer.resolveId(id)
  
  if (resolveResult?.id) {
    id = resolveResult.id
  }

  // 从文件系统读取
  let code = await fs.promises.readFile(id, 'utf-8')
  let map: SourceMap | null = null

  // 如果是 TypeScript,用 esbuild 转译
  if (id.endsWith('.ts') || id.endsWith('.tsx')) {
    const result = await esbuildService.transform(code, {
      loader: 'ts',
      target: 'esnext',
      sourcemap: true,
    })
    code = result.code
    map = result.map
  }

  return { code, map }
}

因此一次完整的编译流程如下:

// 实际请求处理过程

// 浏览器请求:GET /src/main.ts
// ↓
// transformMiddleware 拦截
// ↓
// transformRequest('/src/main.ts', server)
// ↓
// loadRawRequest: 从磁盘读取 main.ts
// ├─ 如果是 .ts,用 esbuild 转译为 .js
// └─ 返回 { code, map }
// ↓
// pluginContainer.transform(code, '/src/main.ts')
// ├─ vue plugin: .vue 转换为 { script, template, style }
// ├─ css-in-js plugin: 处理 styled-components 等
// ├─ import-analysis plugin: 分析依赖,重写为 /@modules/xxx
// └─ ...其他插件
// ↓
// 返回转换后的代码给浏览器
// ↓
// 浏览器 import './main.ts' 
// → 收到 ESM 代码,正常执行

依赖解析重写

Vite如果这样设计,会面临一个问题:请求的数量特别大,导致浏览器首屏时间反而更久。

Vite做了一层设计,将多个模块合并到一个模块,即依赖解析重写,如vue -> @modules/vue?v=xxx

// packages/vite/src/node/plugins/importAnalysis.ts

export function importAnalysisPlugin(): Plugin {
  return {
    name: 'vite:import-analysis',
    
    async transform(code: string, id: string) {
      // 匹配 import/export 语句
      const imports = parse(code) // 用 es-module-lexer 解析
      
      let s = new MagicString(code)
      
      for (const imp of imports) {
        // 例如:import { ref } from 'vue'
        const source = imp.source
        
        if (isRelative(source)) {
          // 相对路径,保持不变
          // import Foo from './foo.ts'
        } else if (isBuiltin(source)) {
          // Node 内置模块,忽略
        } else {
          // ⭐ NPM 包,重写为 /@modules/xxx
          // import { ref } from 'vue'
          // ↓
          // import { ref } from '/@modules/vue?v=xxx'
          
          const resolved = await resolveImport(source)
          const rewritten = `/@modules/${resolved.id}`
          
          s.overwrite(imp.startPos, imp.endPos, 
            `import {...} from '${rewritten}'`
          )
        }
      }
      
      return {
        code: s.toString(),
        map: s.generateMap(),
      }
    }
  }
}

处理node_modules三方库请求

既然将三方库依赖路径重写,那处理对应的请求也需要进行一次路径转换。

// 当浏览器请求 /@modules/vue?v=xxx 时

middlewares.use('/@modules/', async (req, res, next) => {
  const moduleName = req.url.split('/')[2]?.split('?')[0]
  
  // /@modules/vue → node_modules/vue/dist/vue.esm.js
  const modulePath = require.resolve(moduleName, {
    paths: [config.root],
  })
  
  const code = await fs.promises.readFile(modulePath, 'utf-8')
  
  // 继续执行 transform 中间件处理
  // 确保 node_modules 中的代码也被正确处理
  res.end(code)
})

HMR热更新

那按照这样的设计,所有模块只要经过一次编译,就会保存在模块缓存表中,热更新如何处理呢?

Vite做的也比较通俗易懂,当文件系统监听到文件变化,则清除该模块相关缓存信息,然后websocket通知浏览器,Vite client runtime会重新发起相关改动模块的请求。

// packages/vite/src/node/server/hmr.ts

// 当文件变更时
watcher.on('change', async (file) => {
  const url = urlFromFile(file, config.root)
  
  // 1️⃣ 清除模块缓存
  server.moduleGraph.invalidateModule(url)
  
  // 2️⃣ 收集受影响的模块
  const affectedModules = server.moduleGraph.getImporters(url)
  
  // 3️⃣ 通过 WebSocket 通知浏览器
  server.ws.send({
    type: 'update',
    event: 'vite:beforeUpdate',
    updates: affectedModules.map(m => ({
      type: m.isSelfAccepting ? 'js-update' : 'full-reload',
      event: 'vite:beforeUpdate',
      path: m.url,
      acceptedPath: url,
      timestamp: Date.now(),
    }))
  })
})

HMR客户端脚本注入

这就是客户端热更新的核心代码。

// packages/vite/src/client/client.ts

// 注入到每个 HTML 的脚本
const hotModule = import.meta.hot

if (hotModule) {
  hotModule.accept(({ default: newModule }) => {
    // 接收模块更新
    // 执行自定义 HMR 逻辑或完整重载
  })
  
  // 监听服务器推送
  hotModule.on('vite:beforeUpdate', async (event) => {
    if (event.type === 'js-update') {
      // 动态 import 新版本模块
      await import(event.path + `?t=${event.timestamp}`)
    } else {
      // 完整页面刷新
      window.location.reload()
    }
  })
}

因此热更新的流程总结如下:

用户编辑文件保存
    ↓
文件系统监听器检测变化
    ↓
清除 ModuleGraph 缓存
    ↓
WebSocket 通知浏览器
    ↓
浏览器发起新请求(带时间戳)
    ↓
transformMiddleware 拦截
    ↓
loadRawRequest (esbuild 编译 TS/JSX)
    ↓
pluginContainer.transform (执行插件 Vue/CSS 等)
    ↓
返回最新的 ESM 代码
    ↓
浏览器执行 HMR 回调更新页面

结尾

这就是Vite开发环境的核心机制!按需编译+缓存+HMR推送,相比于Webpack,少了最早的整个bundle的构建,自然而然会快非常多,因为Vite在初始化根本就没有build的过程,甚至连main.ts入口文件都是实时编译的。

拒绝做 DOM 的“搬运工”:从 Vanilla JS 到 Vue 3 响应式思维的进化

作者 San30
2025年12月12日 13:20

在前端开发的漫长演进中,我们经常听到“数据驱动”这个词。但对于很多习惯了 jQuery 或者原生 JavaScript(Vanilla JS)的开发者来说,从“操作 DOM”到“操作数据”的思维转变,往往比学习新语法更难。

今天,我们将通过重构一个经典的 Todos 任务清单应用,来深度剖析 Vue 3 Composition API 是如何解放我们的双手,让我们专注于业务逻辑而非繁琐的页面渲染。

1. 痛点回顾:原生 JS 的“命令式”困境

在没有框架的时代,写一个简单的输入框回显功能,我们通常需要经历这几个步骤:寻找元素 -> 监听事件 -> 获取值 -> 修改 DOM。

让我们看看这个基于原生 JS 的实现片段:

// 先找到DOM元素, 命令式的, 机械的
const app = document.getElementById('app');
const todoInput = document.getElementById('todo-input');

todoInput.addEventListener('change', function(event) {
    const todo = event.target.value.trim();
    if (!todo) return;
    // 手动操作 DOM 更新
    app.innerHTML = todo; 
})

这种代码被称为命令式编程(Imperative Programming) 。正如在代码注释中所写,这是一种“机械”的过程。我们需要关注每一个步骤的实现细节。而且,频繁地操作 DOM 性能是低下的,因为这涉及到了 JS 引擎(V8)与渲染引擎之间的跨界通信。

随着应用变得复杂,大量的 getElementByIdinnerHTML 会让代码变成难以维护的“意大利面条”。

2. Vue 3 的破局:响应式数据与声明式渲染

Vue 的核心在于声明式编程(Declarative Programming) 。你只需要告诉 Vue “想要什么结果”,中间的 DOM 更新过程由 Vue 替你完成。

在 Vue 3 中,我们利用 setup 函数和 Composition API(组合式 API)来组织逻辑。

2.1 核心概念:ref 与数据驱动

App.vue 中,我们不再去查询 DOM 元素,而是定义了响应式数据

import { ref, computed } from 'vue'

// 响应式数据
const title = ref("");
const todos = ref([
  { id: 1, title: '睡觉', done: true },
  { id: 2, title: '吃饭', done: false }
]);

这里体现了 Vue 开发的核心思路: “不再需要思考页面的元素怎么操作,而是要思考数据是怎么变化的”

2.2 指令:连接数据与视图的桥梁

有了数据,我们通过 Vue 的指令将数据绑定到模板上:

  • 双向绑定 (v-model)<input type="text" v-model="title">。当用户输入时,title 变量自动更新;反之亦然。这比手动写 addEventListener 优雅得多。
  • 列表渲染 (v-for)<li v-for="todo in todos" :key="todo.id">。Vue 会根据 todos 数组的变化,智能地添加、删除或更新 <li> 元素。注意这里 :key 的使用,它是 Vue 识别节点的唯一标识,对性能至关重要。
  • 样式绑定 (:class)<span :class="{done: todo.done}">。我们不再需要手动 classList.add('done'),只需改变数据 todo.done,样式就会自动生效。

2.3 智能的条件渲染:v-if 与 v-else 的排他性逻辑

在实际应用中,用户体验细节至关重要。例如,当任务列表被清空时,我们不应该留给用户一片空白,而应该展示“暂无任务”的提示。在原生 JS 中,这通常需要我们在每次添加或删除操作后,手动检查数组长度并切换 DOM 的 display 属性。

而在 Vue 中,我们可以通过 v-ifv-else 指令,像写 if-else 代码块一样在模板中轻松处理这种逻辑分支:

<ul v-if="todos.length">
  <li v-for="todo in todos" :key="todo.id">
    ...
  </li>
</ul>
<div v-else>
  <span>暂无任务</span>
</div>

代码深度解析:

  1. 真实 DOM 的销毁与重建v-if 是真正的条件渲染。当 todos.length 为 0 时,Vue 不仅仅是隐藏了 <ul>(像 CSS 的 display: none 那样),而是直接从 DOM 中移除了整个列表元素。这意味着此时 DOM 中只有 <div>暂无任务</div>,减少了页面的 DOM 节点数量。
  2. 响应式切换:一旦我们向 todos 数组 push 了一条新数据,todos.length 变为 1。Vue 的响应式系统会立即感知,销毁 v-else 元素,并重新创建并插入 <ul> 列表。
  3. 逻辑互斥v-else 必须紧跟在 v-if 元素之后,它们构成了一个封闭的逻辑组,保证了同一时间页面上只会存在其中一种状态。

通过这两个指令,我们不仅实现了界面的动态交互,更重要的是,我们将“列表为空时显示什么”的业务逻辑直接通过模板表达了出来,不仅代码量减少了,意图也更加清晰。

3. 深度解析:Computed 计算属性 vs. 模板逻辑

在开发中,我们经常需要根据现有的数据计算出新的状态,比如统计“剩余未完成任务数”。

3.1 为什么要用 Computed?

初学者可能会直接在模板里写逻辑:

{{ todos.filter(todo => !todo.done).length }}

虽然这也能工作,但 Vue 官方更推荐使用 Computed(计算属性)

// 创建一个响应式的计算属性
const active = computed(() => {
  return todos.value.filter(todo => !todo.done).length
})

computed 的四大优势:

  1. 性能优化(带缓存) :这是最大的区别。模板内的表达式在每次组件重渲染时都会重新执行。而 computed 只有在它依赖的数据(这里是 todos)发生变化时才会重新计算。如果 todos 没变,多次访问 active 会直接返回缓存值。
  2. 可读性:将复杂的逻辑从 HTML 模板中剥离到 JS 中,让模板保持干净、语义化。
  3. 可复用性active 可以在模板中多处使用,也可以在 JS 逻辑中被引用。
  4. 调试与测试:单独测试一个 JS 函数远比测试模板中的一段逻辑要容易。

3.2 进阶技巧:Computed 的 Get 与 Set

计算属性通常是只读的,但 Vue 也允许我们定义 set 方法,这在处理“全选/全不选”功能时非常强大。

看看这段精妙的代码:

const allDone = computed({
  // 读取值:判断是否所有任务都已完成
  get() {
    return todos.value.every(todo => todo.done)
  },
  // 设置值:当点击全选框时,将所有任务状态同步修改
  set(value) {
    todos.value.forEach(todo => todo.done = value)
  }
})

在模板中,我们只需绑定 <input type="checkbox" v-model="allDone">

  • 当用户点击复选框,Vue 调用 set(value),我们遍历数组更新所有 todo.done
  • 当所有子任务被手动勾选,get() 返回 true,全选框自动被勾选。

这种双向的逻辑联动,如果用原生 JS 实现,需要编写大量的事件监听和状态判断代码,而在 Vue 中,它被封装成了一个优雅的属性。

4. 总结:Vue 开发方式的哲学

demo.htmlApp.vue,我们经历的不仅仅是语法的改变,更是思维模式的重构:

  • Focus on Business:我们不再是浏览器的“建筑工人”(搬运 DOM),而是“设计师”(定义数据状态)。
  • Composition APIsetuprefcomputed 让我们能够更灵活地组合逻辑,比 Vue 2 的 Options API 更利于代码复用和类型推断。
  • Best Practices:永远不要在模板中写复杂的逻辑,善用 computed 缓存机制。

Vue 3 通过响应式系统,替我们处理了脏活累活(DOM 更新),让我们能将精力集中在真正有价值的业务逻辑上。对于想要构建复杂交互系统(如粒子特效、数据可视化)的开发者来说,掌握这种“数据驱动”的思维是迈向高阶开发的第一步。

5.附录:完整App.vue代码

<template>
   <div>
    <h2>{{ title }}</h2>
    <input type="text" v-model="title" @keydown.enter="addTodo">
    <ul v-if="todos.length">
      <li v-for="todo in todos" :key="todo.id">
        <input type="checkbox" v-model="todo.done">
        <span :class="{done: todo.done}">{{ todo.title }}</span>
      </li>
    </ul>
    <div v-else>
      <span>暂无任务</span>
    </div>
    <div>
      全选<input type="checkbox" v-model="allDone">
      {{ active }}
      /
      {{ todos.length }}
    </div>
   </div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 响应式数据
const title = ref("");
const todos = ref([
  {
    id: 1,
    title: '睡觉',
    done: true
  },
  {
    id: 2,
    title: '吃饭',
    done: false
  }
]);

const active = computed(() => {
  return todos.value.filter(todo => !todo.done).length
})

const addTodo = () => {
  if(!title.value) return;
  todos.value.push({
    id: todos.value.length + 1,
    title: title.value,
    done: false
  });
  title.value = '';
}
const allDone = computed({
  get() {
    return todos.value.every(todo => todo.done)
  },
  set(value) {
    todos.value.forEach(todo => todo.done = value)
  }
})
</script>
<style>
  .done {
    color: gray;
    text-decoration: line-through;
  }
</style>

Vue 任务清单开发:数据驱动 vs 传统 DOM 操作

作者 冻梨政哥
2025年12月12日 13:04

Vue 任务清单开发:数据驱动 vs 传统 DOM 操作

在前端开发中,任务清单是一个常见的案例,通过这个案例我们可以清晰对比传统 DOM 操作与 Vue 数据驱动开发的差异。本文将结合具体代码,解析 Vue 的核心思想和常用 API。

传统开发方式的局限

传统 JavaScript 开发中,我们需要手动操作 DOM 元素来实现功能。以下代码为例:

<h2 id="app"></h2>
<input type="text" id="todo-input">
<script>
    // 传统方式需要先获取DOM元素
    const app = document.getElementById('app');
    const todoInput = document.getElementById('todo-input');
    
    // 手动绑定事件并操作DOM
    todoInput.addEventListener('change',function(event) {
        const todo = event.target.value.trim();
        if(!todo){
            console.log('请输入任务');
            return ;
        }else{
            // 直接修改DOM内容
            app.innerHTML = todo;
        }
    })
</script>

这种方式的特点是:

  • 需要手动获取 DOM 元素
  • 命令式地操作 DOM 进行更新
  • 业务逻辑与 DOM 操作混杂
  • 随着功能复杂,代码会变得难以维护

Vue 的数据驱动开发理念

Vue 采用了完全不同的思路:开发者只需关注数据本身,而非 DOM 操作。以任务清单为例:

<template>
  <div>
    <!-- 数据绑定 -->
    <h2>{{ title }}</h2>
    <!-- 双向数据绑定 -->
    <input type="text" v-model="title" @keydown.enter="addTodo">
    
    <!-- 条件渲染 -->
    <ul v-if="todos.length">
      <!-- 循环渲染 -->
      <li v-for="todo in todos" :key="todo.id">
        <input type="checkbox" v-model="todo.done">
        <span :class="{done: todo.done}">{{ todo.title }}</span>
      </li>
    </ul>
    <div v-else>暂无计划</div>
  </div>
</template>

Vue 的核心思想是:不再关心页面元素如何操作,只关注数据如何变化。当数据发生改变时,Vue 会自动更新 DOM,开发者无需手动操作。

Vue 常用 API 解析

  1. v-model 双向数据绑定

    <input type="text" v-model="title">
    

    实现表单输入与数据的双向绑定,输入框的变化会自动更新数据,数据的变化也会自动反映到输入框。

  2. v-for 循环渲染

    <li v-for="todo in todos" :key="todo.id">
    

    基于数组渲染列表,:key用于标识每个元素的唯一性,提高渲染性能。

  3. v-if/v-else 条件渲染

    <ul v-if="todos.length">
      ...
    </ul>
    <div v-else>暂无计划</div>
    

    根据条件动态渲染不同的内容,当todos数组为空时显示 "暂无计划"。

  4. :class 动态类绑定

    <span :class="{done: todo.done}">{{ todo.title }}</span>
    

    todo.donetrue时,自动为元素添加done类,实现完成状态的样式变化。

  5. @事件监听

    <input type="text" @keydown.enter="addTodo">
    

    监听键盘回车事件,触发addTodo方法,@v-bind:的缩写。

  6. computed 计算属性

    // 计算未完成的任务数量
    const active = computed(() => {
      return todos.value.filter(todo => !todo.done).length
    })
    
    // 全选功能的实现
    const allDone = computed({
      get(){
        return todos.value.every(todo => todo.done)
      },
      set(val){
        todos.value.forEach(todo => todo.done = val)
      }
    })
    

    计算属性具有缓存特性,只有依赖的数据变化时才会重新计算,相比方法调用更节省性能。全选功能展示了计算属性的高级用法,通过getset实现双向绑定。

  7. ref 响应式数据

    import { ref } from 'vue'
    const title = ref("");
    const todos = ref([...])
    

    创建响应式数据,当这些数据变化时,Vue 会自动更新相关的 DOM。

总结

Vue 通过数据驱动的方式,极大简化了前端开发流程:

  • 开发者可以专注于业务逻辑和数据处理
  • 减少了大量手动 DOM 操作的代码
  • 提供了简洁直观的 API,降低学习成本
  • 内置的性能优化(如计算属性缓存)让应用运行更高效

JavaScript常用设计模式完整指南

作者 1024肥宅
2025年12月12日 12:46

引言

设计模式是软件工程中解决常见问题的可复用方案。在JavaScript开发中,合理运用设计模式可以提高代码的可维护性、可扩展性和可读性。本文将详细介绍JavaScript中常用的设计模式及其实现。

一、设计模式分类

设计模式主要分为三大类:

  • 创建型模式: 处理对象创建机制
  • 结构型模式: 处理对象组合和关系
  • 行为型模式: 处理对象间通信和职责分配

二、创建型模式

2.1 单例模式 (Singleton Pattern)

确保一个类只有一个实例, 并提供全局访问点。

// ES6实现
class Singleton {
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    this.data = {};
    Singleton.instance = this;
    return this;
  }

  setData(key, value) {
    this.data[key] = value;
  }

  getData(key) {
    return this.data[key];
  }
}

// 使用示例
const instance1 = new Singleton();
const instance2 = new Singleton();

console.log(instance1 === instance2); // true
instance1.setData('name', 'Singleton');
console.log(instance2.getData('name')); // 'Singleton'

// 闭包实现
const SingletonClosure = (function() {
  let instance;
  
  function createInstance() {
    const object = { data: {} };
    return {
      setData: (key, value) => object.data[key] = value,
      getData: (key) => object.data[key]
    };
  }
  
  return {
    getInstance: function() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    }
  };
})();
2.2 工厂模式 (Factory Pattern)

创建对象而不暴露创建逻辑, 通过一个公共接口创建对象。

// 简单工厂模式
class Car {
  constructor(options) {
    this.type = options.type || 'sedan';
    this.color = options.color || 'white';
    this.price = options.price || 20000;
  }
}

class Truck {
  constructor(options) {
    this.type = options.type || 'truck';
    this.color = options.color || 'blue';
    this.price = options.price || 50000;
    this.capacity = options.capacity || '5t';
  }
}

class VehicleFactory {
  static createVehicle(type, options) {
    switch (type) {
      case 'car':
        return new Car(options);
      case 'truck':
        return new Truck(options);
      default:
        throw new Error('Unknown vehicle type');
    }
  }
}

// 使用示例
const myCar = VehicleFactory.createVehicle('car', {
  color: 'red',
  price: 25000
});

const myTruck = VehicleFactory.createVehicle('truck', {
  color: 'black',
  capacity: '10t'
});

// 工厂方法模式
class Vehicle {
  drive() {
    console.log(`${this.type} is driving`);
  }
}

class Car2 extends Vehicle {
  constructor() {
    super();
    this.type = 'Car';
  }
}

class Truck2 extends Vehicle {
  constructor() {
    super();
    this.type = 'Truck';
  }
}

class VehicleFactory2 {
  createVehicle() {
    throw new Error('This method must be overridden');
  }
}

class CarFactory extends VehicleFactory2 {
  createVehicle() {
    return new Car2();
  }
}

class TruckFactory extends VehicleFactory2 {
  createVehicle() {
    return new Truck2();
  }
}
2.3 建造者模式 (Builder Pattern)

将复杂对象的构建与其表示分离, 使同样的构建过程可以创建不同的表示。

class Pizza {
  constructor() {
    this.size = null;
    this.crust = null;
    this.cheese = false;
    this.pepperoni = false;
    this.mushrooms = false;
    this.onions = false;
  }

  describe() {
    console.log(`Pizza: Size-${this.size}, Crust-${this.crust}, 
      Cheese-${this.cheese}, Pepperoni-${this.pepperoni},
      Mushrooms-${this.mushrooms}, Onions-${this.onions}`);
  }
}

class PizzaBuilder {
  constructor() {
    this.pizza = new Pizza();
  }

  setSize(size) {
    this.pizza.size = size;
    return this;
  }

  setCrust(crust) {
    this.pizza.crust = crust;
    return this;
  }

  addCheese() {
    this.pizza.cheese = true;
    return this;
  }

  addPepperoni() {
    this.pizza.pepperoni = true;
    return this;
  }

  addMushrooms() {
    this.pizza.mushrooms = true;
    return this;
  }

  addOnions() {
    this.pizza.onions = true;
    return this;
  }

  build() {
    return this.pizza;
  }
}

// 使用示例
const pizza = new PizzaBuilder()
  .setSize('large')
  .setCrust('thin')
  .addCheese()
  .addPepperoni()
  .addMushrooms()
  .build();

pizza.describe();
2.4 原型模式 (Prototype Pattern)

通过复制现有对象来创建新对象, 而不是通过实例化类。

// 使用Object.create实现原型模式
const carPrototype = {
  wheels: 4,
  drive() {
    console.log(`${this.brand} is driving with ${this.wheels} wheels`);
  },
  clone() {
    return Object.create(this);
  }
};

// 创建新对象
const tesla = Object.create(carPrototype);
tesla.brand = 'Tesla';
tesla.model = 'Model 3';

const anotherTesla = Object.create(tesla);
anotherTesla.model = 'Model S';

tesla.drive(); // Tesla is driving with 4 wheels
anotherTesla.drive(); // Tesla is driving with 4 wheels

// ES6类实现原型模式
class VehiclePrototype {
  constructor(proto) {
    Object.assign(this, proto);
  }

  clone() {
    return new VehiclePrototype(this);
  }
}

const bikeProto = {
  wheels: 2,
  ride() {
    console.log(`Riding ${this.brand} with ${this.wheels} wheels`);
  }
};

const bike = new VehiclePrototype(bikeProto);
bike.brand = 'Giant';
const anotherBike = bike.clone();
anotherBike.brand = 'Trek';

三、结构型模式

3.1 装饰器模式 (Decorator Pattern)

动态地给对象添加额外职责, 而不改变其结构。

// ES7装饰器语法
function log(target, name, descriptor) {
  const original = descriptor.value;
  
  descriptor.value = function(...args) {
    console.log(`Calling ${name} with`, args);
    const result = original.apply(this, args);
    console.log(`Result: ${result}`);
    return result;
  };
  
  return descriptor;
}

class Calculator {
  @log
  add(a, b) {
    return a + b;
  }
}

// 传统JavaScript实现
class Coffee {
  cost() {
    return 5;
  }
}

class CoffeeDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }

  cost() {
    return this.coffee.cost();
  }
}

class MilkDecorator extends CoffeeDecorator {
  cost() {
    return this.coffee.cost() + 2;
  }
}

class SugarDecorator extends CoffeeDecorator {
  cost() {
    return this.coffee.cost() + 1;
  }
}

// 使用示例
let myCoffee = new Coffee();
console.log(`Basic coffee: $${myCoffee.cost()}`);

myCoffee = new MilkDecorator(myCoffee);
console.log(`Coffee with milk: $${myCoffee.cost()}`);

myCoffee = new SugarDecorator(myCoffee);
console.log(`Coffee with milk and sugar: $${myCoffee.cost()}`);
3.2 代理模式 (Proxy Pattern)

为其他对象提供一种代理以控制对这个对象的访问。

// ES6 Proxy实现
const target = {
  message: "Hello, World!",
  getMessage() {
    return this.message;
  }
};

const handler = {
  get: function(obj, prop) {
    if (prop === 'message') {
      console.log('Accessing message property');
      return obj[prop] + ' (via proxy)';
    }
    return obj[prop];
  },
  
  set: function(obj, prop, value) {
    if (prop === 'message') {
      console.log(`Setting message to: ${value}`);
      obj[prop] = value;
      return true;
    }
    return false;
  }
};

const proxy = new Proxy(target, handler);

console.log(proxy.message); // "Hello, World! (via proxy)"
proxy.message = "New Message"; // "Setting message to: New Message"

// 保护代理示例
const sensitiveData = {
  username: 'admin',
  password: 'secret123',
  creditCard: '1234-5678-9012-3456'
};

const protectionHandler = {
  get: function(obj, prop) {
    if (prop === 'password' || prop === 'creditCard') {
      return 'Access denied';
    }
    return obj[prop];
  },
  
  set: function(obj, prop, value) {
    if (prop === 'password' || prop === 'creditCard') {
      console.log('Cannot modify sensitive data directly');
      return false;
    }
    obj[prop] = value;
    return true;
  }
};

const protectedData = new Proxy(sensitiveData, protectionHandler);
3.3 适配器模式 (Adapter Pattern)

将一个类的接口转换成客户期望的另一个接口。

// 旧系统接口
class OldSystem {
  specificRequest() {
    return 'Old system response';
  }
}

// 新系统期望的接口
class NewSystem {
  request() {
    return 'New system response';
  }
}

// 适配器
class Adapter {
  constructor(oldSystem) {
    this.oldSystem = oldSystem;
  }

  request() {
    const result = this.oldSystem.specificRequest();
    return `Adapted: ${result}`;
  }
}

// 使用示例
const oldSystem = new OldSystem();
const adapter = new Adapter(oldSystem);

console.log(adapter.request()); // "Adapted: Old system response"

// 实际应用示例:数据格式适配
class JSONData {
  getData() {
    return '{"name": "John", "age": 30}';
  }
}

class XMLData {
  getData() {
    return '<user><name>John</name><age>30</age></user>';
  }
}

class DataAdapter {
  constructor(dataSource) {
    this.dataSource = dataSource;
  }

  getJSON() {
    const data = this.dataSource.getData();
    
    // 如果是XML,转换为JSON
    if (data.startsWith('<')) {
      // 简单转换逻辑
      const nameMatch = data.match(/<name>(.*?)<\/name>/);
      const ageMatch = data.match(/<age>(.*?)<\/age>/);
      
      return JSON.stringify({
        name: nameMatch ? nameMatch[1] : '',
        age: ageMatch ? parseInt(ageMatch[1]) : 0
      });
    }
    
    return data;
  }
}
3.4 外观模式 (Facade Pattern)

为复杂的子系统提供一个统一的简单接口。

// 复杂的子系统
class CPU {
  start() {
    console.log('CPU started');
  }
  
  execute() {
    console.log('CPU executing instructions');
  }
}

class Memory {
  load() {
    console.log('Memory loading data');
  }
}

class HardDrive {
  read() {
    console.log('Hard drive reading data');
  }
}

// 外观
class ComputerFacade {
  constructor() {
    this.cpu = new CPU();
    this.memory = new Memory();
    this.hardDrive = new HardDrive();
  }

  startComputer() {
    console.log('Starting computer...');
    this.cpu.start();
    this.memory.load();
    this.hardDrive.read();
    this.cpu.execute();
    console.log('Computer started successfully');
  }
}

// 使用示例
const computer = new ComputerFacade();
computer.startComputer();

// 另一个例子:DOM操作外观
class DOMFacade {
  constructor(elementId) {
    this.element = document.getElementById(elementId);
  }

  setText(text) {
    this.element.textContent = text;
    return this;
  }

  setStyle(styles) {
    Object.assign(this.element.style, styles);
    return this;
  }

  addClass(className) {
    this.element.classList.add(className);
    return this;
  }

  on(event, handler) {
    this.element.addEventListener(event, handler);
    return this;
  }
}
3.5 组合模式 (Composite Pattern)

将对象组合成树形结构以表示'部分-整体'的层次结构。

// 组件接口
class Component {
  constructor(name) {
    this.name = name;
  }

  add(component) {
    throw new Error('This method must be overridden');
  }

  remove(component) {
    throw new Error('This method must be overridden');
  }

  getChild(index) {
    throw new Error('This method must be overridden');
  }

  operation() {
    throw new Error('This method must be overridden');
  }
}

// 叶子节点
class Leaf extends Component {
  constructor(name) {
    super(name);
  }

  operation() {
    console.log(`Leaf ${this.name} operation`);
  }
}

// 复合节点
class Composite extends Component {
  constructor(name) {
    super(name);
    this.children = [];
  }

  add(component) {
    this.children.push(component);
  }

  remove(component) {
    const index = this.children.indexOf(component);
    if (index > -1) {
      this.children.splice(index, 1);
    }
  }

  getChild(index) {
    return this.children[index];
  }

  operation() {
    console.log(`Composite ${this.name} operation`);
    for (const child of this.children) {
      child.operation();
    }
  }
}

// 使用示例:文件系统
const root = new Composite('root');
const home = new Composite('home');
const user = new Composite('user');

const file1 = new Leaf('file1.txt');
const file2 = new Leaf('file2.txt');
const file3 = new Leaf('file3.txt');

root.add(home);
home.add(user);
user.add(file1);
user.add(file2);
root.add(file3);

root.operation();

四、行为型模式

4.1 策略模式 (Strategy Pattern)

定义一系列算法, 封装每个算法, 并使它们可以互相替换。

// 策略接口
class PaymentStrategy {
  pay(amount) {
    throw new Error('This method must be overridden');
  }
}

// 具体策略
class CreditCardStrategy extends PaymentStrategy {
  constructor(cardNumber, cvv) {
    super();
    this.cardNumber = cardNumber;
    this.cvv = cvv;
  }

  pay(amount) {
    console.log(`Paid $${amount} using Credit Card ${this.cardNumber.slice(-4)}`);
    return true;
  }
}

class PayPalStrategy extends PaymentStrategy {
  constructor(email) {
    super();
    this.email = email;
  }

  pay(amount) {
    console.log(`Paid $${amount} using PayPal (${this.email})`);
    return true;
  }
}

class CryptoStrategy extends PaymentStrategy {
  constructor(walletAddress) {
    super();
    this.walletAddress = walletAddress;
  }

  pay(amount) {
    console.log(`Paid $${amount} using Crypto Wallet ${this.walletAddress.slice(0, 8)}...`);
    return true;
  }
}

// 上下文
class ShoppingCart {
  constructor() {
    this.items = [];
    this.paymentStrategy = null;
  }

  addItem(item, price) {
    this.items.push({ item, price });
  }

  calculateTotal() {
    return this.items.reduce((total, item) => total + item.price, 0);
  }

  setPaymentStrategy(strategy) {
    this.paymentStrategy = strategy;
  }

  checkout() {
    const total = this.calculateTotal();
    if (!this.paymentStrategy) {
      console.log('Please select a payment method');
      return false;
    }
    return this.paymentStrategy.pay(total);
  }
}

// 使用示例
const cart = new ShoppingCart();
cart.addItem('Book', 25);
cart.addItem('Headphones', 100);

cart.setPaymentStrategy(new CreditCardStrategy('1234-5678-9012-3456', '123'));
cart.checkout();

cart.setPaymentStrategy(new PayPalStrategy('user@example.com'));
cart.checkout();
4.2 观察者模式 (Observer Pattern / 发布-订阅模式)

定义对象间的一对多依赖关系, 当一个对象状态改变时, 所有依赖它的对象都会得到通知。

// 发布-订阅实现
class EventEmitter {
  constructor() {
    this.events = {};
  }

  on(event, listener) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(listener);
    return () => this.off(event, listener);
  }

  off(event, listener) {
    if (!this.events[event]) return;
    
    const index = this.events[event].indexOf(listener);
    if (index > -1) {
      this.events[event].splice(index, 1);
    }
  }

  emit(event, ...args) {
    if (!this.events[event]) return;
    
    this.events[event].forEach(listener => {
      try {
        listener.apply(this, args);
      } catch (error) {
        console.error(`Error in event listener for ${event}:`, error);
      }
    });
  }

  once(event, listener) {
    const removeListener = this.on(event, (...args) => {
      listener.apply(this, args);
      removeListener();
    });
    return removeListener;
  }
}

// 使用示例
const emitter = new EventEmitter();

// 订阅事件
const unsubscribe = emitter.on('userLoggedIn', (user) => {
  console.log(`Welcome, ${user.name}!`);
});

emitter.on('userLoggedIn', (user) => {
  console.log(`Sending login notification to ${user.email}`);
});

// 发布事件
emitter.emit('userLoggedIn', { 
  name: 'John Doe', 
  email: 'john@example.com' 
});

// 取消订阅
unsubscribe();

// 观察者模式实现
class Subject {
  constructor() {
    this.observers = [];
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  removeObserver(observer) {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }

  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  constructor(name) {
    this.name = name;
  }

  update(data) {
    console.log(`${this.name} received:`, data);
  }
}
4.3 迭代器模式 (Iterator Pattern)

提供一种方法顺序访问聚合对象中的各个元素, 而又不暴露其内部表示。

// 自定义迭代器
class Range {
  constructor(start, end, step = 1) {
    this.start = start;
    this.end = end;
    this.step = step;
  }

  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    const step = this.step;
    
    return {
      next() {
        if (current <= end) {
          const value = current;
          current += step;
          return { value, done: false };
        }
        return { done: true };
      }
    };
  }
}

// 使用示例
for (const num of new Range(1, 5)) {
  console.log(num); // 1, 2, 3, 4, 5
}

// 自定义集合迭代器
class Collection {
  constructor() {
    this.items = [];
  }

  add(item) {
    this.items.push(item);
  }

  [Symbol.iterator]() {
    let index = 0;
    const items = this.items;
    
    return {
      next() {
        if (index < items.length) {
          return { value: items[index++], done: false };
        }
        return { done: true };
      },
      
      return() {
        console.log('Iteration stopped prematurely');
        return { done: true };
      }
    };
  }

  // 生成器实现
  *filter(predicate) {
    for (const item of this.items) {
      if (predicate(item)) {
        yield item;
      }
    }
  }

  *map(transform) {
    for (const item of this.items) {
      yield transform(item);
    }
  }
}
4.4 命令模式 (Command Pattern)

将请求封装为对象, 从而允许参数化客户、队列请求、记录日志以及支持可撤销操作。

// 命令接口
class Command {
  execute() {
    throw new Error('This method must be overridden');
  }

  undo() {
    throw new Error('This method must be overridden');
  }
}

// 具体命令
class LightOnCommand extends Command {
  constructor(light) {
    super();
    this.light = light;
  }

  execute() {
    this.light.turnOn();
  }

  undo() {
    this.light.turnOff();
  }
}

class LightOffCommand extends Command {
  constructor(light) {
    super();
    this.light = light;
  }

  execute() {
    this.light.turnOff();
  }

  undo() {
    this.light.turnOn();
  }
}

// 接收者
class Light {
  constructor(location) {
    this.location = location;
    this.isOn = false;
  }

  turnOn() {
    this.isOn = true;
    console.log(`${this.location} light is ON`);
  }

  turnOff() {
    this.isOn = false;
    console.log(`${this.location} light is OFF`);
  }
}

// 调用者
class RemoteControl {
  constructor() {
    this.commands = [];
    this.history = [];
  }

  setCommand(command) {
    this.commands.push(command);
  }

  executeCommands() {
    this.commands.forEach(command => {
      command.execute();
      this.history.push(command);
    });
    this.commands = [];
  }

  undoLast() {
    if (this.history.length > 0) {
      const lastCommand = this.history.pop();
      lastCommand.undo();
    }
  }
}

// 使用示例
const livingRoomLight = new Light('Living Room');
const kitchenLight = new Light('Kitchen');

const remote = new RemoteControl();

remote.setCommand(new LightOnCommand(livingRoomLight));
remote.setCommand(new LightOnCommand(kitchenLight));
remote.executeCommands();

remote.undoLast();

// 宏命令
class MacroCommand extends Command {
  constructor(commands) {
    super();
    this.commands = commands;
  }

  execute() {
    this.commands.forEach(command => command.execute());
  }

  undo() {
    // 逆序执行撤销
    this.commands.reverse().forEach(command => command.undo());
  }
}
4.5 状态模式 (State Pattern)

允许对象在其内部状态改变时改变其行为, 看起来像是修改了类。

// 状态接口
class TrafficLightState {
  constructor(context) {
    this.context = context;
  }

  change() {
    throw new Error('This method must be overridden');
  }
}

// 具体状态
class RedLightState extends TrafficLightState {
  change() {
    console.log('Red light - STOP');
    this.context.setState(new GreenLightState(this.context));
  }
}

class GreenLightState extends TrafficLightState {
  change() {
    console.log('Green light - GO');
    this.context.setState(new YellowLightState(this.context));
  }
}

class YellowLightState extends TrafficLightState {
  change() {
    console.log('Yellow light - CAUTION');
    this.context.setState(new RedLightState(this.context));
  }
}

// 上下文
class TrafficLight {
  constructor() {
    this.state = new RedLightState(this);
  }

  setState(state) {
    this.state = state;
  }

  change() {
    this.state.change();
  }
}

// 使用示例
const trafficLight = new TrafficLight();

trafficLight.change(); // Red light - STOP
trafficLight.change(); // Green light - GO
trafficLight.change(); // Yellow light - CAUTION
trafficLight.change(); // Red light - STOP

// 更复杂的例子:文档编辑器状态
class Document {
  constructor() {
    this.state = new DraftState(this);
    this.content = '';
  }

  setState(state) {
    this.state = state;
  }

  write(text) {
    this.state.write(text);
  }

  publish() {
    this.state.publish();
  }
}

class DraftState {
  constructor(document) {
    this.document = document;
  }

  write(text) {
    this.document.content += text;
    console.log(`Draft: Added "${text}"`);
  }

  publish() {
    console.log('Publishing draft...');
    this.document.setState(new PublishedState(this.document));
  }
}

class PublishedState {
  constructor(document) {
    this.document = document;
  }

  write(text) {
    console.log('Cannot write to published document. Create new draft first.');
  }

  publish() {
    console.log('Document is already published.');
  }
}
4.6 职责链模式 (Chain of Responsibility Pattern)

使多个对象都有机会处理请求, 从而避免请求发送者和接收者之间的耦合关系。

// 处理者接口
class Handler {
  constructor() {
    this.nextHandler = null;
  }

  setNext(handler) {
    this.nextHandler = handler;
    return handler;
  }

  handle(request) {
    if (this.nextHandler) {
      return this.nextHandler.handle(request);
    }
    console.log('No handler found for request:', request);
    return null;
  }
}

// 具体处理者
class AuthenticationHandler extends Handler {
  handle(request) {
    if (request.type === 'auth' && request.credentials === 'valid') {
      console.log('Authentication successful');
      return super.handle(request);
    } else if (request.type === 'auth') {
      console.log('Authentication failed');
      return null;
    }
    return super.handle(request);
  }
}

class AuthorizationHandler extends Handler {
  handle(request) {
    if (request.type === 'auth' && request.role === 'admin') {
      console.log('Authorization granted for admin');
      return super.handle(request);
    } else if (request.type === 'auth') {
      console.log('Authorization denied');
      return null;
    }
    return super.handle(request);
  }
}

class LoggingHandler extends Handler {
  handle(request) {
    console.log(`Logging request: ${JSON.stringify(request)}`);
    return super.handle(request);
  }
}

// 使用示例
const authHandler = new AuthenticationHandler();
const authzHandler = new AuthorizationHandler();
const logHandler = new LoggingHandler();

authHandler
  .setNext(authzHandler)
  .setNext(logHandler);

// 处理请求
const request1 = { type: 'auth', credentials: 'valid', role: 'admin' };
authHandler.handle(request1);

const request2 = { type: 'auth', credentials: 'invalid' };
authHandler.handle(request2);

// 实际应用:请求处理管道
class ValidationHandler extends Handler {
  handle(data) {
    if (!data.email || !data.email.includes('@')) {
      console.log('Validation failed: Invalid email');
      return null;
    }
    console.log('Validation passed');
    return super.handle(data);
  }
}

class SanitizationHandler extends Handler {
  handle(data) {
    data.email = data.email.trim().toLowerCase();
    console.log('Data sanitized');
    return super.handle(data);
  }
}

class SaveHandler extends Handler {
  handle(data) {
    console.log(`Saving data: ${data.email}`);
    return { success: true, id: Date.now() };
  }
}

五、其他重要模式

5.1 模块模式 (Module Pattern)
// 使用IIFE实现模块模式
const UserModule = (function() {
  // 私有变量
  let users = [];
  let userCount = 0;

  // 私有方法
  function generateId() {
    return Date.now().toString(36) + Math.random().toString(36).substr(2);
  }

  // 公共接口
  return {
    addUser: function(name, email) {
      const user = {
        id: generateId(),
        name,
        email,
        createdAt: new Date()
      };
      users.push(user);
      userCount++;
      return user.id;
    },

    getUser: function(id) {
      return users.find(user => user.id === id);
    },

    getUsers: function() {
      return [...users]; // 返回副本
    },

    getUserCount: function() {
      return userCount;
    },

    removeUser: function(id) {
      const index = users.findIndex(user => user.id === id);
      if (index > -1) {
        users.splice(index, 1);
        userCount--;
        return true;
      }
      return false;
    }
  };
})();

// ES6模块语法
export class Calculator {
  static add(a, b) {
    return a + b;
  }

  static multiply(a, b) {
    return a * b;
  }
}
5.2 混入模式 (Mixin Pattern)
// 混入函数
function mixin(target, ...sources) {
  Object.assign(target, ...sources);
  return target;
}

// 可复用的混入对象
const CanEat = {
  eat(food) {
    console.log(`${this.name} is eating ${food}`);
    this.energy += 10;
  }
};

const CanSleep = {
  sleep() {
    console.log(`${this.name} is sleeping`);
    this.energy += 20;
  }
};

const CanPlay = {
  play() {
    console.log(`${this.name} is playing`);
    this.energy -= 5;
  }
};

// 使用混入
class Animal {
  constructor(name) {
    this.name = name;
    this.energy = 100;
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name);
    mixin(this, CanEat, CanSleep, CanPlay);
  }

  bark() {
    console.log(`${this.name} is barking!`);
  }
}

// 使用示例
const dog = new Dog('Rex');
dog.eat('bone');
dog.play();
dog.sleep();
dog.bark();

// ES6类混入
const Flyable = BaseClass => class extends BaseClass {
  fly() {
    console.log(`${this.name} is flying!`);
  }
};

class Bird extends Flyable(Animal) {
  constructor(name) {
    super(name);
  }
}

const bird = new Bird('Tweety');
bird.fly();
5.3 中介者模式 (Mediator Pattern)
// 中介者
class ChatRoom {
  constructor() {
    this.users = new Map();
  }

  register(user) {
    this.users.set(user.name, user);
    user.chatRoom = this;
  }

  send(message, from, to) {
    if (to) {
      // 私聊
      const receiver = this.users.get(to);
      if (receiver) {
        receiver.receive(message, from);
      }
    } else {
      // 群聊
      this.users.forEach(user => {
        if (user.name !== from) {
          user.receive(message, from);
        }
      });
    }
  }
}

// 同事类
class User {
  constructor(name) {
    this.name = name;
    this.chatRoom = null;
  }

  send(message, to = null) {
    this.chatRoom.send(message, this.name, to);
  }

  receive(message, from) {
    console.log(`${from} to ${this.name}: ${message}`);
  }
}

// 使用示例
const chatRoom = new ChatRoom();

const alice = new User('Alice');
const bob = new User('Bob');
const charlie = new User('Charlie');

chatRoom.register(alice);
chatRoom.register(bob);
chatRoom.register(charlie);

alice.send('Hello everyone!');
bob.send('Hi Alice!', 'Alice');
charlie.send('Meeting at 3 PM', 'Alice');

六、总结与最佳实践

何时使用设计模式
  1. 单例模式: 全局配置、日志记录器、数据库连接池
  2. 工厂模式: 创建复杂对象、需要根据条件创建不同对象
  3. 观察者模式: 事件处理系统、实时数据更新
  4. 策略模式: 多种算法实现、需要动态切换行为
  5. 装饰器模式: 动态添加功能、AOP编程
JavaScript设计模式特点
  1. 灵活性: JavaScript的动态特性使得模式实现更加灵活
  2. 函数式特性: 可以利用高阶函数、闭包等特性简化模式实现
  3. 原型继承: 充分利用原型链实现继承和共享方法
  4. ES6+特性: 类语法、Proxy、Symbol、装饰器等增强了模式表达能力
最佳实践建议
  1. 不要过度设计: 只在必要时使用设计模式
  2. 保持简洁: JavaScript本身就很灵活,避免过度复杂的模式实现
  3. 结合语言特性: 充分利用JavaScript的函数式特性
  4. 考虑性能: 某些模式可能带来性能开销,在性能敏感场景要谨慎
  5. 团队共识: 确保团队成员理解所使用的设计模式

设计模式是解决特定问题的工具,而不是银弹。在实际开发中,应根据具体需求选择合适的模式,并灵活调整以适应JavaScript的语言特性。理解模式的核心思想比死记硬背实现方式更重要。

【译】从零开始理解 JavaScript Promise:彻底搞懂异步编程

作者 Sherry007
2025年12月12日 10:52

🎯 从零开始理解 JavaScript Promise:彻底搞懂异步编程

🔗 原文链接:Promises From The Ground Up
👨‍💻 原作者:Josh W. Comeau
📅 发布时间:2024年6月3日
🕐 最后更新:2025年3月18日

⚠️ 关于本译文

本文基于 Josh W. Comeau 的原文进行忠实翻译,力求准确传达原作者的技术观点和逻辑结构。

🎨 特色亮点:

  • 保持原文的完整性和技术准确性
  • 采用自然流畅的中文表达,避免翻译腔
  • 添加画外音板块,提供译者的补充解读和实践心得
  • 使用生动比喻帮助理解复杂概念

💡 画外音说明: 文中标注为画外音的部分是译者基于实际开发经验添加的拓展解释,旨在帮助读者更好地理解和应用这些概念,不代表原作者观点。


在学习 JavaScript 的路上,有很多坎儿要过。其中最大、最让人头疼的,就是 Promise(承诺)

要想真正理解 Promise,咱们得对 JavaScript 的工作原理和它的局限性有相当深入的了解。没有这些背景知识,Promise 就像天书一样难懂。

这事儿确实挺让人抓狂的,因为 Promise API 在现代 JavaScript 开发中实在太重要了。它已经成为处理异步代码的标准方式。现代的 Web API 都是建立在 Promise 之上的。没办法绕过去:如果想用 JavaScript 高效工作,真的很有必要搞懂 Promise。

所以在这篇教程里,咱们要学习 Promise,但会从最基础的地方开始。我会分享那些花了我好几年才搞明白的关键知识点。希望读到最后,你能对 Promise 是什么、怎么有效使用它们有更深的理解。✨

💡 画外音:我刚开始学 Promise 的时候,总是记不住 .then().catch() 到底该怎么用,感觉像是硬记 API。后来才明白,一旦理解了 JavaScript 单线程的本质和异步编程的必要性,这些 API 设计就显得非常自然了。所以这篇文章真的是从根源讲起,强烈推荐耐心读完。

适合谁看

这篇文章适合初级到中级的 JavaScript 开发者。你需要懂一些基本的 JavaScript 语法。


🤔 为啥要这么设计?

假设咱们想做一个"新年倒计时",像这样的效果:

钉钉录屏_2025-12-11 195030.gif 如果 JavaScript 和大多数其他编程语言一样,咱们可以这样解决问题:

function newYearsCountdown() {
  print("3");
  sleep(1000);

  print("2");
  sleep(1000);

  print("1");
  sleep(1000);

  print("Happy New Year! 🎉");
}

在这段假想的代码里,程序会在遇到 sleep() 调用时暂停,等指定的时间过去后再继续执行。

可惜的是,JavaScript 里没有 sleep 函数,因为它是一门单线程语言。*

💡 画外音:这里的"线程"(thread)指的是执行代码的长时间运行进程。JavaScript 只有一个线程,所以它一次只能做一件事,不能同时处理多个任务。这是个问题,因为如果咱们唯一的 JavaScript 线程忙着管理倒计时,它就干不了别的事儿了。

*技术上讲,现代 JavaScript 可以通过 Web Workers 访问多线程,但这些额外的线程无法访问 DOM,所以在大多数场景下用不上。

当我刚学这些东西的时候,不太明白为什么这是个问题。如果倒计时是现在唯一发生的事情,那 JS 线程在这段时间被完全占用不是挺正常的吗?

嗯,虽然 JavaScript 没有 sleep 函数,但它确实有一些其他函数会长时间占用主线程。咱们可以用这些方法来体验一下,如果 JavaScript 真有 sleep 函数会是什么样子。

比如说,window.prompt()。这个函数用来从用户那里收集信息,它会暂停代码执行,就像咱们假想的 sleep() 函数一样。

点击下面这个示例中的按钮,然后在提示框打开时试着和页面交互

image.png

💡 提示:这里只放了截图。如果想亲自体验这个效果(强烈推荐!),可以去原文页面试试,点击按钮后你会发现整个页面真的卡住了,完全动不了。

注意到了吗?当提示框打开的时候,整个页面完全没反应!你没法滚动、点击任何链接,也没法选择任何文本!JavaScript 线程正忙着等咱们输入值,好让它能继续运行代码。在等待的过程中,它干不了别的任何事,所以浏览器就把整个 UI 都锁住了。

其他语言有多个线程,所以其中一个被占用一会儿也没啥大不了的。但在 JavaScript 里,咱们就这一个线程,而且它要用来干所有事情:处理事件、管理网络请求、更新 UI 等等。

如果想做一个倒计时,咱们得找个不阻塞线程的方法。

💡 画外音:这就是为什么你有时会看到有人说"不要在主线程做耗时操作"。比如复杂的计算、大数据处理,如果放在主线程,用户就会感觉页面卡死了。这也是为什么后来出现了 Web Workers,专门用来处理这类重活儿。

为什么整个 UI 都冻结了?

在上面 window.prompt() 的例子中,浏览器等待咱们输入值的时候,整个 UI 都变得没反应了。

这有点奇怪……浏览器滚动页面或选择文本又不依赖 JavaScript。那为什么这些操作也做不了呢?

我觉得浏览器这么做是为了防止 bug。比如滚动页面会触发 "scroll" 事件,这些事件可以被 JavaScript 捕获和处理。如果 JS 线程忙着的时候滚动事件发生了,那段代码就永远不会运行,如果开发者假设滚动事件总是会被处理,就可能导致 bug。

这也可能是出于用户体验的考虑;也许浏览器禁用 UI 是为了让用户不能忽略提示框。不管怎样,我估计原生的 sleep 函数也得这么工作才能防止 bug。


📞 回调函数(Callbacks)

咱们工具箱里解决这类问题的主要工具是 setTimeoutsetTimeout 是一个接受两个参数的函数:

  1. 未来某个时刻要做的一块工作
  2. 要等待的时间

来看个例子:

console.log('Start');

setTimeout(
  () => {
    console.log('After one second');
  },
  1000
);

这块工作通过一个函数传进去。这种模式叫做回调(callback)

前面假想的 sleep() 函数就像给公司打电话,然后一直等着接通下一个客服。而 setTimeout() 就像按 1 让他们在客服有空的时候给你回电。你可以挂掉电话,该干嘛干嘛。

setTimeout() 被称为异步函数。这意味着它不会阻塞线程。相比之下,window.prompt()同步的,因为 JavaScript 线程在等待的时候干不了别的。

异步代码的一个大坑是,它意味着咱们的代码不会总是按线性顺序运行。看看下面这个例子:

console.log('1. Before setTimeout');

setTimeout(() => {
  console.log('2. Inside setTimeout');
}, 500);

console.log('3. After setTimeout');

你可能期望这些日志按从上到下的顺序触发:1 > 2 > 3但记住,回调的核心思想就是"留个号,一会儿回你。 JavaScript 线程不会干坐着等,它会继续运行。

想象一下,如果咱们给 JavaScript 线程一本日记,让它记录运行这段代码时做的所有事情。运行完之后,日记会是这样:

  • 00:000:打印 "1. Before setTimeout"
  • 00:001:注册一个定时器
  • 00:002:打印 "3. After setTimeout"
  • 00:501:打印 "2. Inside setTimeout"

setTimeout() 注册了回调,就像在日历上安排一个会议。注册回调只需要极短的时间,一旦完成,它就继续往下走,执行程序的其余部分。

💡 画外音:这个"日记"的比喻特别好,帮我彻底理解了事件循环。很多新手(包括当年的我)觉得 setTimeout(fn, 0) 很神奇——明明延迟是 0,为什么还是异步的?就是因为它会被"注册"到日历上,即使时间到了,也得等当前同步代码都跑完才轮到它。

回调在 JavaScript 里到处都是,不只是用于定时器。比如,咱们这样监听指针事件(pointer events):

钉钉录屏_2025-12-11 201119.gif

💡 画外音:"pointer"(指针)是个统称,涵盖了所有涉及"指向"的 UI 输入方式,包括鼠标、手指在触摸屏上的点击、触控笔等。所以 pointer events 比 mouse events 的概念更广。

window.addEventListener() 注册了一个回调,每当检测到特定事件时就会被调用。在这个例子中,咱们监听鼠标移动。每当用户移动鼠标或在触摸屏上拖动手指,咱们就会运行一块代码作为响应。

就像 setTimeout 一样,JavaScript 线程不会专注于监视和等待这些事件。它告诉浏览器"嘿,用户移动指针的时候告诉我一声"。当事件触发时,JS 线程会回过头来运行咱们的回调。

好吧,咱们已经跑得有点远了。回到最初的问题:如果想做一个 3 秒倒计时,该怎么做?

在过去,最常见的解决方案是设置嵌套的回调,像这样:

console.log("3…");

setTimeout(() => {
  console.log("2…");

  setTimeout(() => {
    console.log("1…");

    setTimeout(() => {
      console.log("Happy New Year!!");
    }, 1000);
  }, 1000);
}, 1000);

这太疯狂了,对吧?咱们的 setTimeout 回调里又创建了新的 setTimeout 回调!

当我在 2000 年代早期开始折腾 JavaScript 的时候,这种模式挺常见的,虽然大家都觉得不太理想。咱们把这种模式叫做回调地狱(Callback Hell)

Promise 就是为了解决回调地狱的一些问题而开发的。

💡 画外音:回调地狱不仅仅是代码难看的问题。真正的痛点是:错误处理变得超级复杂,每层嵌套都要处理错误;代码的可读性和维护性极差,嵌套超过 3 层基本就看不懂了。我曾经维护过一个 7 层嵌套的回调,那酸爽,现在想起来还头疼。

等等,定时器怎么知道什么时候触发?

setTimeout API 接收一个回调函数和一个持续时间。过了指定时间后,回调函数就会被调用。

但怎么做到的?如果 JavaScript 线程没有看着定时器,像老鹰盯小鸡一样盯着它,它怎么知道该调用回调了?

这超出了本教程的范围,但 JavaScript 有个东西叫做事件循环(event loop)。当咱们调用 setTimeout 时,一条小消息会被添加到队列里。每当 JS 线程不在执行代码时,它就在监视事件循环,检查消息。

定时器到期时,事件循环里就会亮起一个提示灯,就像有新留言的答录机。如果 JS 线程当时没在忙,它会立刻跳过去执行传给 setTimeout() 的回调。

这确实意味着定时器不是 100% 精确的。JavaScript 只有一个线程,它可能正忙着干别的事儿,比如处理滚动事件或等待 window.prompt()。如果咱们指定了 1000ms 的定时器,可以确信至少过了 1000 毫秒,但可能会稍微长一点。

你可以在 MDN 上了解更多关于事件循环的内容。


🎁 Promise 登场

前面说过,咱们不能让 JavaScript 傻等着再执行下一行代码,因为那会把线程堵死。得想办法把工作拆成一块块异步执行。

不过嵌套太难看了,能不能换个思路?要是能把这些操作像串珠子一样连起来就好了——先做这个,做完了做那个,再做下一个。

就当好玩儿,咱们假设有根魔法棒,可以随意改变 setTimeout 函数的工作方式。如果咱们这样做会怎样:

console.log('3');

setTimeout(1000)
  .then(() => {
    console.log('2');

    return setTimeout(1000);
  })
  .then(() => {
    console.log('1');

    return setTimeout(1000);
  })
  .then(() => {
    console.log('Happy New Year!!');
  });

不直接把回调传给 setTimeout(那会导致嵌套和回调地狱),而是用一个特殊的 .then() 方法把它们串起来,是不是好多了?

这就是 Promise 的核心思想。Promise 是 JavaScript 在 2015 年一次大更新中加入的特殊结构。

可惜 setTimeout 还是老样子,用的是回调风格。因为 setTimeout 在 Promise 出现之前就已经存在很久了,要是改了它的工作方式,会导致很多老网站挂掉。向后兼容是好事,但也意味着有些东西没法那么优雅。

不过现代的 Web API 都是基于 Promise 构建的。咱们来看个例子。


🔧 使用 Promise

fetch() 函数允许咱们发起网络请求,通常是从服务器获取一些数据。

看看这段代码:

const fetchValue = fetch('/api/get-data');

console.log(fetchValue);
// -> Promise {<pending>}

当咱们调用 fetch() 时,它启动网络请求。这是一个异步操作,所以 JavaScript 线程不会停下来等待。代码继续运行。

fetch() 函数到底返回了啥?肯定不是服务器返回的真实数据,因为咱们才刚发起请求,数据还在路上呢。它返回的其实是一张"欠条"(IOU),就像浏览器给你打的一张白条,上面写着:"嘿,数据我还没拿到,但我保证马上就给你!"

💡 画外音:IOU 是 "I Owe You"(我欠你)的缩写,读音就像说"I Owe You"。它是一种表示欠债的凭据。用这个比喻特别贴切——Promise 就像浏览器给你打的一张欠条:"数据我现在还没拿到,但我欠你的,到时候一定给你"。

具体来说,Promise 就是个 JavaScript 对象。它内部永远只会处于三种状态之一:

  • pending(待定) — 工作正在进行中,还没完成
  • fulfilled(已完成) — 工作已成功完成
  • rejected(已拒绝) — 出了点问题,Promise 无法完成

只要 Promise 还在 pending 状态,就说它是未解决的(unresolved)。一旦工作完成了,它就变成已解决(resolved)。这里要注意:不管最后是成功(fulfilled)还是失败(rejected),都算是"解决了"。

💡 画外音:Promise 的这三种状态一开始可能有点绕。我喜欢这样理解:pending 就像快递在路上,fulfilled 就像快递送到了,rejected 就像快递丢了或地址错了。一旦快递状态确定(送到或丢失),就不会再变了。

一般来说,咱们会希望在 Promise 完成后做点什么。这时候就用 .then() 方法:

fetch('/api/get-data')
  .then((response) => {
    console.log(response);
    // Response { type: 'basic', status: 200, ...}
  });

fetch() 返回一个 Promise,咱们用 .then() 挂上一个回调函数。等浏览器收到响应了,这个回调就会被执行,响应对象也会作为参数传进来。

等待 JSON?

如果你用过 Fetch API,可能注意到需要第二步才能真正拿到咱们需要的 JSON 数据:

fetch('/api/get-data')
  .then((response) => {
    return response.json();
})
 .then((json) => {
   console.log(json);
   // { data: { ... } }
 });

response.json() 会返回一个全新的 Promise,等响应数据完全转成 JSON 格式后,这个 Promise 才算完成。

但等等,为啥 response.json() 还是异步的?咱们不是已经拿到响应了吗,数据不应该早就是 JSON 了吗?

还真不一定。Web 的一个核心特性是,服务器可以流式传输数据,一点点分批发送。这在传视频(比如 YouTube)的时候很常见,对于大一点的 JSON 数据也可以这么干。

fetch() 返回的 Promise,在浏览器收到第一个字节数据时就算完成了。而 response.json() 的 Promise,要等到收到最后一个字节才算完成。

实际上,JSON 数据很少分批发送,所以这两个 Promise 大多数时候会同时完成。但 Fetch API 在设计时就考虑到了流式响应的场景,所以才需要这么绕一下。

💡 画外音:新手常犯的一个错误是:拿到 response 后直接用,忘了调用 .json()。记住,fetch() 返回的第一个 Promise 只是给你一个"响应对象",里面的数据还是原始格式,需要再调用 .json() 才能解析成 JavaScript 对象。这也是为什么你经常看到两个 .then() 的原因。


🛠️ 创建自己的 Promise

用 Fetch API 的时候,Promise 是 fetch() 函数在背后帮咱们创建的。但要是咱们用的 API 不支持 Promise 呢?

比如 setTimeout,它是在 Promise 出现之前就有了。要想用定时器又不掉进回调地狱,就得自己动手包装一个 Promise。

语法是这样的:

const demoPromise = new Promise((resolve) => {
  // 做一些异步工作,然后
  // 调用 `resolve()` 来完成 Promise
});

demoPromise.then(() => {
  // 当 Promise 完成时,
  // 这个回调会被调用!
})

Promise 其实是个通用容器,它本身不干活儿。当咱们用 new Promise() 创建 Promise 时,得同时告诉它"你要干啥活儿"——通过传入一个函数来指定具体的异步任务。这个任务可以是任何东西:发网络请求、等个定时器、读个文件,啥都行。

等这个活儿干完了,咱们就调用 resolve(),告诉 Promise:"搞定了,一切顺利!"这样 Promise 就变成已解决状态了。

回到咱们一开始的问题——做个倒计时。在这个场景里,异步任务就是"等 setTimeout 跑完"。

那咱们可以自己动手,写一个基于 Promise 的小工具函数,把 setTimeout 包装一下:

function wait(duration) {
  return new Promise((resolve) => {
    setTimeout(resolve, duration);
  });
}

const timeoutPromise = wait(1000);

timeoutPromise.then(() => {
  console.log('1 second later!')
});

这段代码看起来超级吓人。咱们试着分解一下:

  • 咱们写了个新的工具函数 wait,它接收一个参数 duration(持续时间)。目标是把它当成 sleep 函数用,但是异步的、不阻塞线程的那种。
  • wait 函数里创建并返回了一个新的 Promise。Promise 自己啥也不干,得靠咱们在异步工作完成时调用 resolve
  • Promise 内部,咱们用 setTimeout 启动了一个定时器。把 Promise 给的 resolve 函数和用户传进来的 duration 都给它。
  • 定时器时间到了,就会执行回调。这就形成了连锁反应:setTimeout 执行了 resolveresolve 告诉 Promise "搞定了",然后 .then() 里的回调也跟着被触发。

这段代码要是还让你头疼,别担心😅。这里确实揉了好多高级概念在一起!能理解大概思路就行,细节慢慢消化。

有个点可能会帮你理清楚:上面代码里,咱们把 resolve 函数直接扔给了 setTimeout。其实也可以这样写,创建一个箭头函数来调用 resolve

function wait(duration) {
  return new Promise((resolve) => {
    setTimeout(
      () => resolve(),
      duration
    );
  });
}

JavaScript 里函数是"一等公民",意思是函数可以像字符串、数字那样随便传来传去。这特性挺厉害,但新手可能需要点时间才能习惯。上面这种写法不那么直接,但效果完全一样,哪种看着舒服就用哪种!

💡 画外音:这个 wait 函数是我在实际项目中常用的一个工具。很多人会把它加到工具函数库里。甚至有些库(比如 p-timeout)专门提供这类 Promise 工具。学会包装旧的回调式 API 成 Promise,这个技能超级有用,因为还有很多老代码和库用的是回调。


⛓️ 链式调用 Promise

关于 Promise,有一点很重要要理解:它们只能被解决一次。一旦 Promise 被完成或拒绝,它就永远保持那个状态了。

这意味着 Promise 并不真正适合某些场景。比如事件监听器:

window.addEventListener('mousemove', (event) => {
  console.log(event.clientX);
})

这个回调会在用户每次移动鼠标时触发,可能成百上千次。Promise 干不了这活儿。

那咱们的倒计时怎么办?虽然不能重复用同一个 wait Promise,但可以把多个 Promise 串成一条链:

wait(1000)
  .then(() => {
    console.log('2');
    return wait(1000);
  })
  .then(() => {
    console.log('1');
    return wait(1000);
  })
  .then(() => {
    console.log('Happy New Year!!');
  });

第一个 Promise 完成了,.then() 回调就被执行。这个回调又创建并返回一个新的 Promise,就这样一个接一个地串下去。

💡 画外音:Promise 链是个很强大的模式。关键点在于每个 .then() 都会返回一个新的 Promise,这样就能一直链下去。不过要注意:如果忘记 return,链就断了,后面的 .then() 不会等前面的异步操作完成。这是新手常犯的错误,我也踩过好几次坑。


📦 传递数据

前面的例子里,咱们调用 resolve 时都没传参数,只是用它来标记"活儿干完了"。但有时候,咱们还得把结果数据传出来!

来看个例子,假设有个用回调的数据库库:

function getUser(userId) {
  return new Promise((resolve) => {
    // 在这个例子中,异步工作是
    // 根据 ID 查找用户
    db.get({ id: userId }, (user) => {
      // 现在咱们有了完整的 user 对象,
      // 可以在这里传进去...
      resolve(user);
    });
  });
}

getUser('abc123').then((user) => {
  // ...然后在这里取出来!
  console.log(user);
  // { name: 'Josh', ... }
})

传给 resolve 的参数,会原封不动地传到 .then() 的回调函数里。这样就能把异步操作的结果一路传出去了。


❌ 被拒绝的 Promise

可惜,JavaScript 的世界里,Promise 不是总能兑现。有时候也会黄了。

比如用 Fetch API 发网络请求,不一定能成功啊!可能网络不稳定,也可能服务器挂了。这些情况下,Promise 就会被拒绝(rejected),而不是正常完成。

咱们可以用 .catch() 方法来处理:

fetch('/api/get-data')
  .then((response) => {
    // ...
  })
  .catch((error) => {
    console.error(error);
  });

Promise 成功了,就走 .then() 这条路。失败了,就走 .catch()。可以理解为两条岔路,看 Promise 最后是啥状态。

💡 画外音:错误处理是 Promise 相比回调的一大优势。在回调地狱里,每层嵌套都要单独处理错误。但用 Promise,你可以在链的末尾加一个 .catch(),它能捕获整个链中任何地方的错误。这大大简化了错误处理逻辑。

Fetch 的坑

假设服务器返回了个错误,比如 404 Not Found 或者 500 Internal Server Error。这应该会触发 Promise 被拒绝,对不对?

意外的是,并不会!这种情况下,Promise 还是会正常完成,只不过 Response 对象里会带着错误信息:

Response {
  ok: false,
  status: 404,
  statusText: 'Not Found',
}

这看着有点奇怪,但仔细想想也说得通:咱们的 Promise 确实完成了,也从服务器拿到响应了!虽然不是咱们想要的那种响应,但确实有响应。

至少按"许三个愿望的精灵"的逻辑,这没毛病。

自己写 Promise 的时候,可以用第二个参数 reject 来标记拒绝:

new Promise((resolve, reject) => {
  someAsynchronousWork((result, error) => {
    if (error) {
      reject(error);
      return;
    }

    resolve(result);
  });
});

Promise 里面要是出了问题,就调用 reject() 来标记失败。传给 reject() 的参数(通常是个错误对象)会被传到 .catch() 回调里。

令人困惑的名字

前面说过,Promise 有三种状态:pending(进行中)、fulfilled(成功)和 rejected(失败)。那为啥参数不叫 "fulfill" 和 "reject",而是叫 "resolve" 和 "reject" 呢?

原因是这样的:resolve() 大多数情况下确实会让 Promise 变成 fulfilled 状态。但有个特殊情况——如果你在 resolve() 里传入的不是普通值,而是另一个 Promise,事情就不一样了。

举个例子:

const promise1 = new Promise((resolve) => {
  const promise2 = fetch('/api/data');
  resolve(promise2); // 传入了另一个 Promise!
});

这时候,promise1 会"挂靠"到 promise2 上,等 promise2 的结果。虽然 promise1 技术上还在 pending 状态,但它已经算是 "resolved"(已交接)了——因为它已经把自己的命运交给 promise2 了,JavaScript 线程也已经去忙 promise2 的事儿了。

所以 "resolved" 不等于 "fulfilled",它更像是"已经有着落了"(不管最后成功还是失败)。

这个细节我也是发完博文后读者告诉我才知道的(感谢大家!)。老实说,99% 的开发者都不会碰到这种情况,不用纠结。如果你真的想深入研究,可以看这个文档:States and Fates

💡 画外音:说实话,这个"resolved vs fulfilled"的区别在日常开发中真的不太需要纠结,记住 resolve() 表示成功、reject() 表示失败就够了。不过如果你在面试或者读规范文档的时候碰到,至少知道是咋回事。


🎭 Async / Await

现代 JavaScript 最牛的一点就是 async / await 语法。用了这个语法,咱们终于能写出接近理想状态的倒计时代码了:

async function countdown() {
  console.log("5…");
  await wait(1000);

  console.log("4…");
  await wait(1000);

  console.log("3…");
  await wait(1000);

  console.log("2…");
  await wait(1000);

  console.log("1…");
  await wait(1000);

  console.log("Happy New Year!");
}

等等,这不是不可能吗! 函数执行到一半不能暂停啊,那会把线程堵死的!

其实这个新语法底层还是 Promise。咱们来扒开看看它是怎么运作的:

async function addNums(a, b) {
  return a + b;
}

const result = addNums(1, 1);

console.log(result);
// -> Promise {<fulfilled>: 2}

本以为返回值应该是数字 2,结果却是个 Promise,里面包着数字 2。只要给函数加上 async 关键字,它就一定会返回 Promise,哪怕函数里压根没干异步的活儿。

上面的代码其实是这样的语法糖:

function addNums(a, b) {
  return new Promise((resolve) => {
    resolve(a + b);
  });
}

同样的,await 关键字也是 .then() 回调的语法糖:

// 这段代码...
async function pingEndpoint(endpoint) {
  const response = await fetch(endpoint);
  return response.status;
}

// ...等价于这个:
function pingEndpoint(endpoint) {
  return fetch(endpoint)
    .then((response) => {
      return response.status;
    });
}

Promise 给 JavaScript 打好了底层基础,让咱们能写出看着像同步、实际是异步的代码。

这设计,真的绝了。

💡 画外音async/await 是我最喜欢的 JavaScript 特性之一。它让异步代码读起来就像同步代码一样自然。不过有个常见误区:很多人以为 async/await 是一种新的异步机制,其实它只是 Promise 的语法糖。理解这一点很重要,因为有时候你还是需要直接用 Promise(比如 Promise.all() 并发请求)。另外,别忘了用 try/catch 包裹 await,不然错误可能会悄悄溜走!


🚀 更多内容即将推出!

过去几年,我全职都在做教育内容,制作和分享像这篇博文这样的资源。我已经做了 CSS 课程和 React 课程。

学生们问得最多的就是:"能不能做个原生 JavaScript 的课程?"这事儿我一直在想。接下来几个月,应该会发更多关于原生 JavaScript 的文章。

想在我发布新内容时第一时间知道的话,最好是订阅我的邮件列表。有新博文或者课程更新,我都会发邮件通知你。❤️


📝 译者总结

💡 核心要点回顾

概念 关键理解
单线程本质 JavaScript 只有一个线程,不能像其他语言那样"停下来等"
回调地狱 嵌套回调难以维护,错误处理复杂,这是 Promise 要解决的核心问题
Promise 状态 pending(进行中)→ fulfilled(成功)或 rejected(失败)
链式调用 .then() 返回新 Promise,可以一直链下去,避免嵌套
async/await Promise 的语法糖,让异步代码看起来像同步,但本质还是 Promise

🎯 实用建议

  1. 包装旧 API:很多老 API 还在用回调,学会用 Promise 包装它们(像文中的 wait 函数)
  2. 错误处理:养成在 Promise 链末尾加 .catch() 的习惯,或者用 try/catch 包裹 await
  3. 别忘了 return:Promise 链中如果需要传递数据或继续链式调用,一定要 return
  4. 并发请求:需要同时发起多个请求时,用 Promise.all() 而不是多个 await
  5. Fetch 陷阱:记住 HTTP 错误状态码(404、500等)不会触发 .catch(),要检查 response.ok

从“拼字符串”到“魔法响应”:一场数据驱动页面的奇幻进化之旅

作者 AAA阿giao
2025年12月12日 09:47

引言

你有没有想过,为什么今天的网页能像变魔术一样——点一下按钮,列表自动刷新;输个名字,头像立刻出现?而十几年前,想换个内容却要整个页面“唰”地重载?

这一切的背后,是一场关于数据如何驱动页面的技术革命。今天,我们就穿越三段代码时空,跟随一份用户列表的命运,看看 Web 开发是如何从“手搓 HTML”一步步进化到“声明即渲染”的魔法世界的。


第一章:远古时代 —— 服务端“手工编织”页面(server.js

源代码链接:[vue/ref/demo/server.js · Zou/lesson_zp - 码云 - 开源中国](gitee.com/giaoZou/les… "vue/ref/demo/server.js · Zou/lesson_zp - 码云 - 开源中国")

时间回到 Web 初期,那时没有 Vue,没有 React,甚至连 AJAX 都还没普及。开发者们用最原始的方式:在服务器上把数据和 HTML 混在一起,直接“烤”成完整网页,再扔给浏览器

打开 server.js,你会看到这样一段代码:

const users = [
    {id: 1, name: '张三',email:'zhangsan@qq.com'},
    {id: 2, name: '李四',email:'lisi@qq.com'},
    {id: 3, name: '王五',email:'wangwu@qq.com'},
]

这是一份硬编码的用户名单,就像藏在厨房抽屉里的老式通讯录。

接着,一个叫 generateUsersHtml 的函数登场了:

function generateUsersHtml(users){
    const userRows = users.map(user => `
| ${user.id}|${user.name}|${user.email}|
| ---|---|---|
`).join('');
    return `
Users
| ID|Name|Email|
| ---|---|---|
${userRows}
            
    `
}

注意!这里用的不是标准 HTML 表格,而是 Markdown 表格语法(可能是为了简化演示)。但原理惊人地朴素:用 JavaScript 字符串拼接,把数据“缝”进模板里

当用户访问 /users 时:

if (parsedUrl.pathname === '/' || parsedUrl.pathname === '/users') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/html;charset=utf-8'); 
    const html = generateUsersHtml(users);
    res.end(html);
}

服务器把拼好的“成品网页”通过 res.end() 发出去。浏览器收到后,直接显示——没有请求、没有等待、没有交互,一切都在服务端完成

这就像一位老裁缝,根据你的身材(数据)现场量体裁衣(生成 HTML),然后把做好的衣服(完整页面)递给你。但如果你胖了两斤,他得重新做一件——整个页面刷新!

优点:简单、快速、SEO 友好。
缺点:交互差、前后端耦合、改一点就要重刷。


第二章:工业革命 —— 前后端“分家”,API 登场(index.html + db.json + package.json

随着网站越来越复杂,开发者发现:“让前端和后端各干各的,效率更高!”于是,前后端分离成为新潮流。

完整项目结构及链接:[vue/ref/demo2 · Zou/lesson_zp - 码云 - 开源中国](gitee.com/giaoZou/les… "vue/ref/demo2 · Zou/lesson_zp - 码云 - 开源中国")

![](<> "点击并拖拽以移动")

项目准备

前后端分离需要进行项目初始化

第一步:对负责后端的文件夹 backend 进行初始化,在终端打开该文件夹,执行以下命令

# 初始化项目,生成 package.json(只需执行一次)
npm init -y
 
# 安装 json-server,用于基于 JSON 文件快速创建本地 REST API 服务。
npm i json-server

第二步:在前端文件夹 frontend 中添加文件 index.html ,在后端文件夹 backend 中添加 db.json 文件

第三步:修改 package.json 文件内容

将文件内的JavaScript语句修改为如下内容

  &#34;scripts&#34;: {
    &#34;dev&#34;: &#34;json-server --watch db.json&#34;
  },

这段代码定义了一个名为 dev 的 npm 脚本,作用是使用 json-server 工具监听(--watch)项目根目录下的 db.json 文件。当运行 npm run dev 时,会启动一个本地 RESTful API 服务器,自动将 db.json 中的数据暴露为 API 接口,并在文件内容变化时实时更新接口数据,常用于前端开发中的模拟后端服务。

静态骨架:index.html 的“空舞台”

看一眼 index.html

User List
Users
| ID|Name|Email|
| ---|---|---|

这简直是个“幽灵页面”——有标题、有表头,但没有一行真实数据!它就像一个空荡荡的剧院舞台,只搭好了布景,就等演员(数据)登场。

数据仓库:db.json 的“假数据库”

真正的数据藏在这里:

{
    &#34;users&#34;: [
        {
            &#34;id&#34;: 1,
            &#34;name&#34;: &#34;张三&#34;,
            &#34;email&#34;: &#34;zhangsan@qq.com&#34;
        },
        {
            &#34;id&#34;: 2,
            &#34;name&#34;: &#34;李四&#34;,
            &#34;email&#34;: &#34;lisi@qq.com&#34;
        }
        ,
        {
            &#34;id&#34;: 3,
            &#34;name&#34;: &#34;王五&#34;,
            &#34;email&#34;: &#34;wangwu@qq.com&#34;    
        }
    ]
}

这是一个纯 JSON 文件,结构清晰,但本身不会动。它需要一个“翻译官”把它变成 API。

自动化 API 工厂:json-server(来自 package.json

package.json

{
  &#34;scripts&#34;: {
    &#34;dev&#34;: &#34;json-server --watch db.json&#34;
  },
  &#34;dependencies&#34;: {
    &#34;json-server&#34;: &#34;^1.0.0-beta.3&#34;
  }
}

运行 npm run dev,神奇的事情发生了:json-server 自动监听 db.json,并启动一个本地服务器(默认 http://localhost:3000),将 users 数组暴露为 RESTful 接口:

  • GET /users → 返回全部用户
  • GET /users/1 → 返回 ID 为 1 的用户

现在,前端只需一句 fetch('/users'),就能拿到 JSON 数据!

舞台(index.html)不再自己造演员,而是打电话给经纪公司(API):“请派三位演员上台!” 演员来了,前台 JS 再手动把他们安排到座位上(操作 DOM)。

优点:前后端解耦、接口复用、便于测试。
痛点:前端仍需手动更新 DOM,代码冗长易错——“找到表格 → 清空 → 循环创建行 → 插入单元格……”

于是,人们渴望一种更智能的方式……


第三章:魔法纪元 —— Vue 的“响应式咒语”(App.vue

如果说前后端分离是“分工”,那么 Vue 就是“自动化”。它引入了一个颠覆性理念:你只管描述“页面应该长什么样”,数据变了,页面自动跟着变

项目准备

创建Vue3项目,在终端打开创建项目的文件夹,运行以下命令

npm init vite

回车后输入项目名称 ref-demo,选择 Vue + JavaScript 即可

![](<> "点击并拖拽以移动")

完整项目结构及 App.vue 文件链接:[vue/ref/demo3/ref-demo/src/App.vue · Zou/lesson_zp - 码云 - 开源中国](gitee.com/giaoZou/les… "vue/ref/demo3/ref-demo/src/App.vue · Zou/lesson_zp - 码云 - 开源中国")

![](<> "点击并拖拽以移动")

响应式数据:会“思考”的变量 —— Vue 的魔法心脏

App.vue 中,我们看到这样两行代码:

import { ref } from 'vue';
const users = ref([]);

别小看这短短两行——它们开启了一个自动同步数据与视图的魔法世界

ref([]) 不是普通数组,而是一个“活”的数据容器

乍一看,users 似乎只是个空数组。但其实,ref() 把它包装成了一个响应式引用对象(reactive reference) 。你可以把它想象成一个装着数据的“智能玻璃瓶”:

  • 瓶子里装的是你的真实数据(比如用户列表);
  • 瓶子本身会“监听”:只要有人往里放新东西(修改 .value),它就会立刻广播:“注意!内容变了!”
  • 所有“订阅”了这个瓶子的 UI 元素(比如模板中的 {{user.name}}),都会自动更新自己。

换句话说,users 不再是死气沉沉的变量,而是一个会呼吸、会通知、会联动的活体数据

技术小贴士:在 Vue 3 的 Composition API 中,ref 内部使用了 JavaScript 的 Proxygetter/setter 机制,实现对 .value 访问和赋值的拦截,从而建立“依赖追踪”系统。


生命周期钩子:组件的“出生仪式”

接下来这段代码,是组件的“成人礼”:

onMounted(() => {
  console.log('组件挂载完成,开始从后端获取数据');
  fetch('/users')
    .then(res => res.json())
    .then(data => {
      users.value = data;
      console.log('从后端获取到的数据:', users.value);
    })
})
什么是 onMounted

Vue 组件有自己的“生命周期”:创建 → 挂载 → 更新 → 卸载。
onMounted 就是那个关键的“我已上线”时刻——当组件的 DOM 已经被渲染到页面上,Vue 就会执行这个回调函数。

这就像一个新生儿睁开眼睛的第一秒,立刻说:“世界你好!我要开始干活了!”

为什么在这里发请求?

因为:

  • 页面结构已经存在(模板已解析);
  • 此时发起 fetch('/users'),既能拿到数据,又能确保有地方展示它;
  • 如果在组件还没挂载前就操作 DOM 或赋值,可能会失败或造成内存泄漏。

于是,组件一“睁眼”,就向后端(由 json-server 提供的 /users 接口)发出请求,拿到 JSON 数据后:

users.value = data;

Boom!魔法触发!


重点:这一行代码,如何让页面“活”起来?

当你写下 users.value = data,看似只是赋值,实则引爆了一连串精妙的连锁反应:

  1. Vue 的响应式系统检测到 users 的值发生了变化
    → 因为 users 是用 ref 创建的,任何对 .value 的写入都会被拦截。
  2. 系统立刻找出所有“依赖”这个数据的地方
    → 在编译阶段,Vue 已经悄悄记录下:模板中用了 users 的地方(比如 v-for=&#34;user in users&#34;)都需要被通知。
  3. 重新执行相关的渲染逻辑
    → Vue 并不会重绘整个页面,而是只重新计算“受影响的部分”——也就是用户列表区域。
  4. 生成新的虚拟 DOM(Virtual DOM)
    → Vue 先在内存中构建一个轻量级的 DOM 树副本。
  5. 与旧虚拟 DOM 对比(diff 算法)
    → 找出最小差异:比如新增了三行、删除了零行。
  6. 精准更新真实 DOM
    → 只修改浏览器中真正需要变动的节点,避免不必要的重排重绘。

 整个过程毫秒级完成,用户毫无感知,却看到了最新数据!


声明式模板:所想即所得的 UI 编程 

现在,看看 `` 部分的神奇之处:

<table>
  <tr>
    <th>id</th>
    <th>name</th>
    <th>email</th>
  </tr>
<tbody>
  <tr>
    <td>{{user.id}}</td>
    <td>{{user.name}}</td>
    <td>{{user.email}}</td>
  </tr>
</tbody>
</table>

这里藏着 Vue 两大核心法宝:

{{ }}:插值表达式 —— 数据的“透明窗口”
  • {{user.name}} 不是一段字符串,而是一个动态绑定
  • 它告诉 Vue:“请在这里显示 user.name 的当前值,并且当它变时,自动刷新。”
  • 你不需要手动拼接 HTML,也不用担心 XSS(Vue 默认会转义内容,安全可靠)。
 v-for:列表渲染指令 —— 自动化的“克隆工厂”
  • v-for=&#34;user in users&#34; 是 Vue 的循环指令。
  • 它会遍历 users 数组,为每个 user 自动生成一行 <tr>
  • :key=&#34;user.id&#34; 提供唯一标识,帮助 Vue 高效追踪每个节点的身份(比如移动、删除时保持状态)。

在传统开发中,你要写:

// 找到表格
const tbody = document.querySelector('#user-table tbody');
// 清空
tbody.innerHTML = '';
// 循环创建行
data.forEach(user => {
  const tr = document.createElement('tr');
  tr.innerHTML = `<td>${user.id}</td><td>${user.name}</td>...`;
  tbody.appendChild(tr);
});

而在 Vue 中,你只需说:

“我想显示一个用户列表,每一行包含 id、name 和 email。”

然后 Vue 就默默完成了剩下的所有工作。

这就是声明式编程的魅力:你描述“是什么”,框架负责“怎么做”。

从此,开发者从 DOM 操作的泥潭中解放出来,专注于业务逻辑与用户体验——而这,正是现代前端框架最伟大的馈赠。


终章:三次飞跃,一条主线

时代 核心思想 数据流向 开发体验 用户体验
服务端渲染 “我给你完整的饭” 数据 → 服务端 → 完整 HTML → 浏览器 简单但笨重 刷新卡顿,交互弱
前后端分离 “我给你食材,你自己做” 浏览器 → 请求 API → 获取 JSON → 手动更新 DOM 灵活但繁琐 局部更新,但依赖手动编码
响应式框架 “你告诉我菜谱,我自动做饭” 数据变化 → 自动驱动视图更新 声明式、高效、可维护 流畅、实时、无感

结语:技术的本质是“解放人”

server.js 的字符串拼接,到 App.vue 的响应式绑定,表面看是代码风格的变迁,实则是开发范式的跃迁

从“命令式”(怎么做)走向“声明式”(做什么)

今天的前端开发者,不再需要关心“如何插入一行表格”,而是专注“用户列表应该展示哪些字段”。这种抽象,让我们能更专注于业务逻辑与用户体验。

而这,正是技术进步最美的样子——让复杂消失,让创造浮现

🌟 下次当你看到一个动态刷新的列表时,不妨微笑一下:那背后,是一场跨越二十年的工程智慧结晶。

Vite 到底能干嘛?为什么 Vue3 官方推荐它做工程化工具?

作者 刘大华
2025年12月12日 08:26

很多朋友在开发项目的时候其实都有用过Vite,也知道它是现代化构建工具,但却不清楚它是怎么用的。

只是知道项目里集成了Vite,开发的时候启动很快,配置文件也很清晰,但很少去了解它是什么,起到了什么作用。

这篇文章我们就来了解一下。

一、Vite 是什么?

它的名字来源于法语单词"vite",意思是"快速",由 Vue.js 作者尤雨溪开发的一款现代化前端构建工具

它的目标很简单:让开发体验更快、更简单

Vite不仅支持Vue,还原生支持React、Svelte、Preact、Lit等主流框架,甚至可以用于纯 HTML + JavaScript 项目。


二、Vite 的核心优势

Vite之所以火,是因为它解决了传统构建工具的几个痛点:

启动极快 开发服务器启动几乎秒开,不打包!

热更新飞快 修改代码后,浏览器只更新改动的部分,毫秒级响应

原生 ES 模块支持 直接利用现代浏览器的 ES Module 能力

零配置上手 创建项目只需一条命令,无需复杂配置

插件生态丰富 兼容 Rollup 插件,生态强大

开箱即用的丰富功能 支持 TypeScript、JSX、CSS 预处理器等,无需复杂配置。


三、与传统工具(如 Webpack)对比

1. 工作方式的根本不同

Webpack:开发时会先打包整个项目(把所有 JS、CSS 合并成 bundle),然后启动服务器。项目越大,打包越慢。

Vite:开发时不打包!直接利用浏览器原生支持的 ES 模块(<script type="module">),按需加载文件。

2. 举个实际例子

假设你有一个简单的项目结构:

src/
├── main.js
└── utils.js

使用 Webpack(传统方式)

// utils.js
export const add = (a, b) => a + b;

// main.js
import { add } from './utils.js';
console.log(add(1, 2));

Webpack 会: 1.把main.jsutils.js打包成一个bundle.js 2.启动本地服务器,返回这个bundle 3.浏览器加载整个bundle

即使你只改了utils.js中的一行,Webpack也要重新分析依赖、重新打包整个应用(虽然有缓存优化,但仍有延迟)。

使用 Vite

Vite 在开发时直接生成这样的 HTML:

<!-- index.html -->
<script type="module" src="/src/main.js"></script>

浏览器会: 1.请求 /src/main.js 2.发现里面 import './utils.js',自动再请求 /src/utils.js 3.按需加载,无需打包!

当你修改utils.js,Vite只告诉浏览器:“喂,utils.js更新了”, 浏览器只重新加载这一个文件,热更新速度接近实时

Vite 在开发阶段用原生 ESM,在生产阶段会用 Rollup 打包,兼顾速度和兼容性。


四、Vite 的工作原理:为什么这么快?

关键就两点:

1. 利用现代浏览器的原生 ES 模块(ESM)

现代浏览器(Chrome、Firefox、Edge 等)早就支持 <script type="module">,可以直接 import/export 模块,无需打包。

Vite 直接把这个能力用起来——开发时不打包,让浏览器自己去加载模块

2. 依赖预构建(Dependency Pre-bundling)

你可能会问:那 node_modules 里的包怎么办?它们很多不是 ESM 格式啊!

Vite会在首次启动时,用 esbuild(超快的 JS 打包器)把 node_modules 里的依赖预构建为 ESM 格式,并缓存起来。

esbuild 是用 Go 写的,比 Webpack 快 10~100 倍!

预构建只做一次,后续开发直接用缓存

这样既保证了兼容性,又不影响开发速度。


五、如何使用 Vite?

1. 创建项目

# 使用 npm
npm create vite@latest my-vue-app -- --template vue

# 使用 yarn
yarn create vite my-react-app --template react

# 使用 pnpm
pnpm create vite my-vanilla-app --template vanilla

2. 项目结构

my-vite-project/
├── index.html
├── package.json
├── vite.config.js
├── public/
│   └── favicon.ico
└── src/
    ├── main.js
    ├── App.vue
    ├── components/
    └── styles/

3. 开发服务器

# 进入项目目录
cd my-vue-app

# 安装依赖
npm install

# 启动开发服务器
npm run dev

访问 http://localhost:5173 即可看到你的应用!

在这里插入图片描述

4. 基础配置示例

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  // 插件配置
  plugins: [vue()],
  
  // 开发服务器配置
  server: {
    port: 5173,
    open: true // 自动打开浏览器
  },
  
  // 构建配置
  build: {
    outDir: 'dist',
    sourcemap: true
  },
  
  // 路径别名
  resolve: {
    alias: {
      '@': '/src'
    }
  }
})

5. 完整的Vue组件示例

<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>我的Vite应用</title>
</head>
<body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
</body>
</html>
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import './style.css'

createApp(App).mount('#app')
<!-- src/App.vue -->
<template>
  <div class="app">
    <h1>欢迎使用 Vite!</h1>
    <p>当前计数: {{ count }}</p>
    <button @click="increment">点击我</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

<style>
.app {
  text-align: center;
  padding: 2rem;
}

button {
  padding: 10px 20px;
  font-size: 16px;
  cursor: pointer;
}
</style>
/* src/style.css */
body {
  margin: 0;
  font-family: Arial, sans-serif;
  background-color: #f5f5f5;
}

* {
  box-sizing: border-box;
}

6. 构建生产版本

# 构建生产版本
npm run build

# 预览生产版本
npm run preview

结语

总的来说,Vite 通过利用现代浏览器的原生能力,在开发阶段省去了打包步骤,大大提升了开发效率。同时,它配置简单、上手容易,还拥有强大的插件生态,非常适合现代前端项目。

本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《async/await 到底要不要加 try-catch?异步错误处理最佳实践》

《如何查看 SpringBoot 当前线程数?3 种方法亲测有效》

《Java 开发必看:什么时候用 for,什么时候用 Stream?》

《这 10 个 MySQL 高级用法,让你的代码又快又好看》

❌
❌