阅读视图

发现新文章,点击刷新页面。

HTTP一些问题的解答(接上篇)

一、在弱网环境下HTTP1会比HTTP2更快的原因是啥?

在弱网环境(高延迟、高丢包率)下,HTTP/1.x 有时比 HTTP/2 表现更好,核心原因是 HTTP/2 的多路复用机制与 TCP 协议的固有缺陷在弱网下产生了 “负协同效应” ,而 HTTP/1.x 的多连接策略反而规避了这种风险。具体可从以下几个角度拆解:

1. 多路复用放大了 TCP 队头阻塞的影响

HTTP/2 的核心优势是 “多路复用”—— 所有请求通过单个 TCP 连接传输,不同请求的帧(Frame)在该连接中交错发送。但这也意味着:单个 TCP 数据包的丢失会阻塞所有请求

  • 弱网下的问题:弱网环境丢包率高(比如 5% 以上),TCP 层一旦丢失一个数据包(可能包含某个请求的帧),会触发重传机制。由于 TCP 是 “按序交付” 的,重传期间,该连接上所有后续数据包(无论属于哪个请求)都会被暂存在接收端的 TCP 缓冲区,无法提交给 HTTP/2 应用层。例如:一个 TCP 连接上有 10 个并发请求的帧在传输,若第 3 个请求的某个帧丢失,TCP 重传期间,后面 7 个请求的帧即使已到达,也会被阻塞,导致所有 10 个请求都变慢。
  • HTTP/1.x 的规避方式:HTTP/1.x 依赖多个并行 TCP 连接(浏览器通常限制为 6-8 个),每个连接处理一个串行请求。若某个连接发生丢包,仅影响该连接上的请求,其他连接的请求仍可正常传输。例如:8 个连接中 1 个丢包,仅 1 个请求受影响,其余 7 个可继续,整体效率反而更高。

2. HTTP/2 的复杂机制在弱网下 “水土不服”

HTTP/2 为优化性能引入的机制(如头部压缩、流优先级),在弱网环境下可能变成负担:

  • HPACK 头部压缩的脆弱性:HTTP/2 用 HPACK 算法压缩请求头,依赖客户端和服务器维护 “共享压缩上下文”(记录已传输的头部字段)。若传输过程中某个头部帧丢失,可能导致双方压缩上下文不一致,需要重新同步,反而增加额外的传输开销和延迟。而 HTTP/1.x 的头部虽未压缩(冗余字节多),但结构简单,单个请求的头部丢失仅影响该请求,无需复杂同步。
  • 流优先级调度的失效:HTTP/2 允许标记请求的优先级(如 CSS/JS 优先于图片),但弱网下,TCP 层的丢包和重传会打乱优先级 —— 高优先级请求的帧可能因低优先级帧的丢包而被阻塞,导致优先级机制失效。而 HTTP/1.x 虽无优先级调度,但多连接天然隔离了不同请求,重要资源可通过独立连接传输,受其他请求影响更小。

3. TCP 拥塞控制对单连接的 “惩罚” 更重

TCP 有拥塞控制机制(如慢启动、拥塞避免),当检测到丢包时,会剧烈降低发送窗口(减少数据发送速率),直到网络恢复。

  • HTTP/2 的单连接劣势:单个 TCP 连接发生丢包后,拥塞窗口会被大幅缩减,导致所有请求的传输速率同时下降。弱网下丢包频繁,单连接可能长期处于低速率状态,整体吞吐量骤降。
  • HTTP/1.x 的多连接优势:多个 TCP 连接独立进行拥塞控制,某个连接因丢包被降速时,其他连接可保持正常速率。即使部分连接受影响,整体吞吐量仍能维持在较高水平。

总结:弱网下 “简单” 比 “高效” 更可靠

HTTP/2 的设计是为了在良好网络环境下通过多路复用提升效率(减少连接开销、优化资源调度),但依赖 TCP 作为底层传输协议。而弱网环境的高丢包、高延迟会放大 TCP 队头阻塞的影响,让多路复用从 “优势” 变成 “劣势”。

HTTP/1.x 虽然存在连接开销大、串行请求的问题,但多连接的 “分散风险” 特性在弱网下更稳健 —— 丢包的影响被限制在单个连接内,整体表现反而更优。这也是为什么在 3G 网络、高丢包的偏远地区,部分场景下 HTTP/1.x 反而比 HTTP/2 更快的核心原因。

二、HTTP2只能有一个连接吗?

HTTP/2 不是 “只能有一个连接” ,而是推荐使用单个 TCP 连接实现多路复用,但协议本身并不禁止建立多个连接。是否使用单连接,更多是基于性能优化的 “最佳实践”,而非强制限制。

1. 为什么 HTTP/2 通常用 “单个连接”?

HTTP/2 的核心设计目标之一是解决 HTTP/1.x “多连接开销大” 的问题,因此推荐通过单个 TCP 连接承载所有请求,利用 “多路复用”(多个 Stream 在同一连接中交错传输)提升效率:

  • 减少连接建立成本:TCP 三次握手、TLS 握手都有延迟(尤其是首次连接),单个连接可避免多次握手的开销;
  • 优化拥塞控制:单个连接的 TCP 拥塞窗口(发送速率)可集中利用带宽,多个连接可能因各自的拥塞控制相互竞争带宽,反而降低整体效率;
  • 简化多路复用逻辑:单个连接中,所有 Stream 的帧(Frame)通过Stream ID区分,接收端更容易管理和调度,多个连接会增加状态同步的复杂度。

2. 什么情况下 HTTP/2 会用 “多个连接”?

虽然不推荐,但 HTTP/2 协议允许同一客户端与服务器建立多个 TCP 连接,常见场景包括:

  • 兼容性兜底:部分老旧服务器或中间代理(如 CDN 节点)对 HTTP/2 的多路复用支持不完善(比如不识别 Stream ID),客户端可能 fallback 到多个连接以确保通信正常;
  • 域名分片残留:HTTP/1.x 常用 “域名分片”(将资源分散到多个子域名,突破浏览器单域名连接数限制),迁移到 HTTP/2 后,若未完全改造,可能仍保留多个子域名的连接(每个子域名一个 HTTP/2 连接);
  • 故障隔离:若单个连接因网络问题(如长时间卡顿、丢包)不可用,客户端可新建一个 HTTP/2 连接继续传输,避免整体中断;
  • 带宽限制突破:某些场景下(如超大文件传输),单个 TCP 连接的拥塞控制可能无法充分利用带宽,通过多个连接 “并行” 传输(类似 HTTP/1.x)可提升吞吐量(但这是对 HTTP/2 设计的 “反用”,较少见)。

3. 浏览器的实际行为:单连接为主,多连接为辅

现代浏览器(如 Chrome、Firefox)对 HTTP/2 的实现遵循 “单连接优先” 原则:

  • 同一域名,默认只建立1 个 HTTP/2 连接,所有请求通过该连接的多路复用传输;
  • 若该连接出现异常(如 TCP 断连),浏览器会自动新建一个 HTTP/2 连接替代;
  • 不同域名,仍会建立独立的 HTTP/2 连接(与 HTTP/1.x 一致,因跨域连接无法共享)。

总结

HTTP/2 的 “单个连接” 是推荐的最佳实践(为了最大化多路复用的优势),但协议本身不限制连接数量。实际应用中,绝大多数场景会用单连接,仅在兼容性、故障恢复等特殊情况下使用多个连接。这种设计既保留了灵活性,又通过单连接默认策略解决了 HTTP/1.x 的核心性能问题。

三、HTTP2中,多路复用的原理是什么?

HTTP/2 的多路复用(Multiplexing)是其核心特性之一,本质是在单一 TCP 连接上同时处理多个请求 - 响应事务,解决了 HTTP/1.x 中 “队头阻塞”(Head-of-Line Blocking)和连接效率低下的问题。其实现原理可拆解为三个关键机制:

1. 帧(Frame):数据传输的最小单位

HTTP/2 将所有传输的数据(请求头、响应体等)拆分为二进制帧,每个帧大小固定(默认最大 16KB),并包含以下关键信息:

  • 流标识符(Stream ID) :标记该帧属于哪个 “流”(对应一个请求 - 响应);
  • 类型(Type) :区分帧的用途(如HEADERS帧承载请求头,DATA帧承载正文,SETTINGS帧配置参数等);
  • 长度(Length) :帧的实际数据大小;
  • 标志位(Flags) :附加控制信息(如END_STREAM标记流结束)。

二进制帧的设计相比 HTTP/1.x 的文本格式,不仅解析效率更高,更重要的是为 “交错传输” 奠定了基础。

2. 流(Stream):请求 - 响应的逻辑通道

每个请求 - 响应事务对应一个,流是 TCP 连接内的 “虚拟通道”,具有以下特性:

  • 双向性:一个流中可同时传输客户端到服务器(请求)和服务器到客户端(响应)的帧;
  • 唯一标识:每个流有唯一的Stream ID(客户端发起的流 ID 为奇数,服务器发起的为偶数);
  • 优先级:可通过PRIORITY帧指定流的优先级(如 CSS/JS 资源优先于图片),服务器据此调整帧的发送顺序;
  • 可中断与复用:流可被暂停、恢复或终止,释放的 ID 可被新流复用。

通过流的隔离,多个请求 - 响应的帧可以在同一 TCP 连接上 “交错传输”(如请求 A 的DATA帧和请求 B 的HEADERS帧交替发送),接收方再根据Stream ID将帧重新组装成完整的请求 / 响应。

3. 单一 TCP 连接的复用

HTTP/2 通过上述 “帧 + 流” 机制,实现了单一 TCP 连接上的多路复用

  • 所有请求 / 响应共享一个 TCP 连接,无需为每个请求建立新连接(减少三次握手 / 慢启动的开销);
  • 多个流的帧可并行传输,避免了 HTTP/1.x 中 “一个请求阻塞导致后续请求排队” 的队头阻塞问题;
  • 即使某个流因网络问题阻塞,其他流的帧仍可正常传输(仅影响单个流,不阻塞整个连接)。

对比 HTTP/1.x 的核心优势

HTTP/1.x HTTP/2 多路复用
多个请求需建立多个 TCP 连接(或串行复用同一连接) 所有请求共享单一 TCP 连接
文本格式传输,解析效率低 二进制帧传输,解析更快
一个请求阻塞会导致后续请求排队(队头阻塞) 流隔离,单个流阻塞不影响其他流

简言之,HTTP/2 的多路复用通过 “帧拆分 + 流标识 + 单连接复用”,彻底解决了 HTTP/1.x 的连接效率问题,大幅提升了高并发场景下的性能(如网页加载大量资源时)。

四、HTTP/1为啥会一个请求阻塞会导致后续请求排队(队头阻塞)?

HTTP/1.x 出现 “队头阻塞”(Head-of-Line Blocking)的核心原理,是由TCP 协议的 “按序交付” 特性HTTP/1.x 协议的 “串行请求 - 响应” 设计共同决定的,两者叠加导致了 “前一个请求阻塞后续所有请求” 的现象。

原理拆解:两层机制的叠加限制

1. 底层 TCP 协议的 “按序交付” 特性(根本原因)

TCP 是面向连接的 “可靠字节流协议”,其核心特性之一是 “按序交付”

  • 发送方会给每个数据包分配一个唯一的 “序号”,接收方必须按序号从小到大的顺序接收并组装数据;
  • 若中间某个数据包丢失(如网络波动),接收方会触发 “超时重传” 机制,等待发送方重新发送丢失的数据包;
  • 在丢失的数据包被重传并接收前,后续所有已到达的数据包即使完整,也会被暂存队列中,无法提交给应用层处理(因为顺序被打乱,无法保证数据完整性)。

这就是 “TCP 层的队头阻塞”—— 单个数据包的问题会阻塞后续所有数据的处理。

2. HTTP/1.x 协议的 “串行请求 - 响应” 设计(放大问题)

HTTP/1.x 运行在 TCP 之上,但其协议设计进一步放大了 TCP 的队头阻塞问题:

  • 无 “流标识” 机制:HTTP/1.x 没有像 HTTP/2 那样的 “流 ID” 来区分不同请求的数据包。接收方(如浏览器)只能通过 “请求发送顺序” 来匹配对应的响应。
  • 严格串行处理:在同一个 TCP 连接上,HTTP/1.x 要求:
    • 必须等前一个请求的完整响应被接收后,才能发送下一个请求;
    • 响应也必须按请求发送的顺序返回(否则接收方无法判断哪个响应对应哪个请求)。

最终导致队头阻塞的过程

假设在一个 TCP 连接上,浏览器按顺序发送 3 个请求:请求A → 请求B → 请求C,过程如下:

  1. 服务器正常返回响应A的部分数据,但中途某个数据包丢失;
  2. 由于 TCP 按序交付特性,接收方会等待丢失的数据包重传,此时响应A的后续数据和已到达的响应B响应C的完整数据,都会被暂存在 TCP 缓冲区中,无法提交给浏览器处理;
  3. 同时,由于 HTTP/1.x 的串行规则,浏览器必须等响应A完全接收后,才能处理响应B响应C—— 即使响应B响应C的数据早已到达,也只能排队等待。

最终,单个请求(A)的阻塞会像 “多米诺骨牌” 一样,导致后续所有请求(B、C)被卡住,这就是 HTTP/1.x 队头阻塞的完整原理。

总结

TCP 的 “按序交付” 导致单个数据包问题阻塞后续数据,而 HTTP/1.x 缺乏 “流标识” 和 “并行处理” 能力,只能通过 “串行请求 - 响应” 来保证数据匹配,两者叠加使得一个请求的延迟会阻塞同一连接上所有后续请求,这就是队头阻塞的本质。

JavaScript 列表转树(List to Tree)详解:前端面试中如何从递归 O(n²) 优化到一次遍历 O(n)

前言:Offer 是怎么没的?

在前端面试的江湖里,「列表转树(List to Tree)」 是一道妥妥的高频题。

很多同学一看到这道题,内心 OS 都是:

😎「简单啊,递归!」

代码写完,自信抬头。
面试官却慢悠悠地问了一句:

🤨「如果是 10 万条数据 呢?
👉 时间复杂度多少?
👉 会不会栈溢出?」

空气突然安静。

今天这篇文章,我们就把这道题彻底拆开:
从「能写」到「写得对」,再到「写得漂亮」。


一、为什么面试官总盯着这棵“树”?

因为在真实业务中,后端给你的几乎永远是扁平数据

例如:

const list = [
  { id: 1, parentId: 0, name: '北京市' },
  { id: 2, parentId: 1, name: '顺义区' },
  { id: 3, parentId: 1, name: '朝阳区' },
  { id: 4, parentId: 2, name: '后沙峪' },
  { id: 121, parentId: 0, name: '江西省' },
  { id: 155, parentId: 121, name: '抚州市' }
];

而前端组件(Menu、Tree、Cascader)要的却是👇

省
 └─ 市
     └─ 区

🎯 面试官的真实考点

  • 数据结构理解:是否真正理解 parentId
  • 递归意识 & 代价:不只会写,还要知道坑在哪
  • 性能优化能力:能否从 O(n²) 优化到 O(n)
  • JS 引用理解:是否理解对象在内存中的表现

二、第一重境界:递归法(能写,但不稳)

1️⃣ 最基础的递归写法

function list2tree(list, parentId = 0) {
  return list
    .filter(item => item.parentId === parentId)
    .map(item => ({
      ...item,
      children: list2tree(list, item.id)
    }));
}

逻辑非常直观:

  • 找当前 parentId 的所有子节点
  • 对每个子节点继续递归
  • 没有子节点时自然退出

三、进阶:ES6 优雅写法(看起来很高级)

如果你在面试中写出下面这段代码👇
面试官大概率会先点头。

const list2tree = (list, parentId = 0) =>
  list
    .filter(item => item.parentId === parentId)
    .map(item => ({
      ...item,              // 解构赋值,保持原对象纯净
      children: list2tree(list, item.id)
    }));

这一版代码:

  • ✅ 箭头函数
  • filter + map 链式调用
  • ✅ 解构赋值,不污染原数据
  • ✅ 可读性很好,看起来很“ES6”

👉 很多同学到这一步就觉得稳了。


🤔 面试官的经典追问

「这个方案,有什么问题?」


🎯 标准回答(一定要说出来)

「这个方案的本质是 嵌套循环
每一层递归,都会遍历一次完整的 list

👉 时间复杂度是 O(n²)
👉 如果层级过深,还可能导致 栈溢出(Stack Overflow) 。」

📌 一句话总结

ES6 写法只是“看起来优雅”,
性能问题不会因为代码好看就自动消失。


四、第二重境界:Map 优化(面试及格线)

既然慢,是因为反复遍历找父节点
那就用 Map 建立索引

👉 典型的:空间换时间


核心思路

  1. 第一遍:把所有节点放进 Map
  2. 第二遍:通过 parentId 直接挂载
  3. 利用 JS 对象引用,自动同步树结构

代码实现

function listToTreeWithMap(list) {
  const map = new Map();
  const tree = [];

  // 初始化
  for (const item of list) {
    map.set(item.id, { ...item, children: [] });
  }

  // 构建树
  for (const item of list) {
    const node = map.get(item.id);
    if (item.parentId === 0) {
      tree.push(node);
    } else {
      const parent = map.get(item.parentId);
      parent && parent.children.push(node);
    }
  }

  return tree;
}

⏱ 复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

📌 到这一步,已经可以应付大多数面试了。


五、终极奥义:一次遍历 + 引用魔法(Top Tier)

面试官:
「能不能只遍历一次?」

答案是:能,而且这才是天花板解法。


核心精髓:占位 + 引用同步

  • 子节点可能先于父节点出现
  • 先在 Map 里给父节点 占位
  • 后续再补全数据
  • 引用地址始终不变,树会“自己长好”

代码实现(一次遍历)

function listToTreePerfect(list) {
  const map = new Map();
  const tree = [];

  for (const item of list) {
    const { id, parentId } = item;

    if (!map.has(id)) {
      map.set(id, { children: [] });
    }

    const node = map.get(id);
    Object.assign(node, item);

    if (parentId === 0) {
      tree.push(node);
    } else {
      if (!map.has(parentId)) {
        map.set(parentId, { children: [] });
      }
      map.get(parentId).children.push(node);
    }
  }

  return tree;
}

🏆 为什么这是王者解法?

  • ✅ 一次遍历,O(n)
  • ✅ 支持乱序数据
  • ✅ 深度理解 JS 引用机制
  • ✅ 面试官一眼就懂你是“真会”

六、真实开发中的应用场景

  • 🔹 权限 / 菜单树(Ant Design / Element)
  • 🔹 省市区 / Cascader
  • 🔹 文件目录结构(云盘、编辑器)

七、面试总结 & 避坑指南

方案 时间复杂度 评价
递归 O(n²) 能写,但危险
Map 两次遍历 O(n) 面试合格
一次遍历 O(n) 面试加分

面试加分表达

  • 主动提 空间换时间
  • 点出 JS 对象是引用类型
  • 询问 parentId 是否可能为 null
  • 说明是否会修改原数据(必要时深拷贝)

结语

算法不是为了为难人,
而是为了在复杂业务中,
选出那条最稳、最优雅的路。

如果这篇文章对你有帮助👇
👍 点个赞
💬 评论区聊聊你在项目里遇到过的奇葩数据结构

工程化工具类:模块化系统全解析与实践

引言

在前端开发的演进历程中,模块化一直是工程化实践的核心。从早期的脚本标签堆砌到现代的ES Modules,模块化技术极大地提升了代码的可维护性、复用性和协作效率。本文将深入探讨模块化的各个方面,包括模块加载器实现、规范演化、Polyfill技术,并补充构建工具、性能优化等工程化实践,全面解析模块化在现代前端开发中的应用。

一、实现简单的模块加载器

在理解复杂模块系统之前,我们先实现一个简单的模块加载器,了解其核心原理。

1.1 基础模块加载器实现
// 简单的模块注册表
const moduleRegistry = {};
const moduleCache = {};

// 模块定义函数
function define(name, dependencies, factory) {
  if (!moduleRegistry[name]) {
    moduleRegistry[name] = {
      dependencies,
      factory,
      resolved: false,
      exports: null
    };
  }
}

// 模块加载函数
function require(name) {
  // 检查缓存
  if (moduleCache[name]) {
    return moduleCache[name];
  }
  
  const module = moduleRegistry[name];
  if (!module) {
    throw new Error(`Module ${name} not found`);
  }
  
  // 解析依赖
  const resolvedDeps = module.dependencies.map(dep => {
    if (dep === 'exports' || dep === 'module') {
      return null; // 特殊处理
    }
    return require(dep);
  });
  
  // 执行工厂函数获取模块导出
  const factoryResult = module.factory.apply(null, resolvedDeps);
  
  // 缓存模块导出
  moduleCache[name] = factoryResult || {};
  module.resolved = true;
  
  return moduleCache[name];
}

// 使用示例
define('math', [], function() {
  return {
    add: (a, b) => a + b,
    multiply: (a, b) => a * b
  };
});

define('calculator', ['math'], function(math) {
  return {
    calculate: (x, y) => math.multiply(math.add(x, y), 2)
  };
});

// 使用模块
const calculator = require('calculator');
console.log(calculator.calculate(2, 3)); // 10
1.2 异步模块加载器
class AsyncModuleLoader {
  constructor() {
    this.modules = new Map();
    this.loading = new Map();
  }
  
  // 定义模块
  define(name, deps, factory) {
    this.modules.set(name, {
      deps,
      factory,
      exports: null,
      resolved: false
    });
  }
  
  // 异步加载模块
  async require(name) {
    if (this.modules.get(name)?.resolved) {
      return this.modules.get(name).exports;
    }
    
    // 防止重复加载
    if (this.loading.has(name)) {
      return this.loading.get(name);
    }
    
    // 创建加载Promise
    const loadPromise = this._loadModule(name);
    this.loading.set(name, loadPromise);
    
    return loadPromise;
  }
  
  async _loadModule(name) {
    const module = this.modules.get(name);
    if (!module) {
      throw new Error(`Module ${name} not found`);
    }
    
    // 加载所有依赖
    const depPromises = module.deps.map(dep => this.require(dep));
    const deps = await Promise.all(depPromises);
    
    // 执行工厂函数
    const exports = module.factory.apply(null, deps);
    
    // 更新模块状态
    module.exports = exports || {};
    module.resolved = true;
    this.loading.delete(name);
    
    return module.exports;
  }
}

// 使用示例
const loader = new AsyncModuleLoader();

loader.define('utils', [], () => ({
  format: str => str.toUpperCase()
}));

loader.define('app', ['utils'], (utils) => {
  return {
    run: () => console.log(utils.format('hello'))
  };
});

loader.require('app').then(app => app.run()); // 输出: HELLO

二、AMD规范实现

AMD(Asynchronous Module Definition)规范是RequireJS推广的异步模块定义标准。

2.1 简化的AMD实现
(function(global) {
  // 模块缓存
  const modules = {};
  const inProgress = {};
  
  // 定义函数
  function define(id, dependencies, factory) {
    if (arguments.length === 2) {
      factory = dependencies;
      dependencies = [];
    }
    
    modules[id] = {
      id: id,
      dependencies: dependencies,
      factory: factory,
      exports: null,
      resolved: false
    };
    
    // 尝试解析模块
    resolveModule(id);
  }
  
  // 依赖解析
  function resolveModule(id) {
    const module = modules[id];
    if (!module || module.resolved) return;
    
    // 检查依赖是否都可用
    const deps = module.dependencies;
    const missingDeps = deps.filter(dep => 
      !modules[dep] || !modules[dep].resolved
    );
    
    if (missingDeps.length === 0) {
      // 所有依赖已就绪,执行工厂函数
      executeModule(id);
    } else {
      // 等待依赖
      missingDeps.forEach(dep => {
        if (!inProgress[dep]) {
          inProgress[dep] = [];
        }
        inProgress[dep].push(id);
      });
    }
  }
  
  // 执行模块
  function executeModule(id) {
    const module = modules[id];
    if (module.resolved) return;
    
    // 获取依赖的exports
    const depExports = module.dependencies.map(dep => {
      if (dep === 'exports') return {};
      if (dep === 'require') return createRequire();
      if (dep === 'module') return { id: module.id, exports: {} };
      return modules[dep].exports;
    });
    
    // 执行工厂函数
    const exports = module.factory.apply(null, depExports);
    
    // 设置exports
    module.exports = exports || 
      (depExports[module.dependencies.indexOf('exports')] || {});
    module.resolved = true;
    
    // 通知等待此模块的其他模块
    if (inProgress[id]) {
      inProgress[id].forEach(dependentId => resolveModule(dependentId));
      delete inProgress[id];
    }
  }
  
  // 创建require函数
  function createRequire() {
    return function(ids, callback) {
      if (typeof ids === 'string') ids = [ids];
      
      Promise.all(ids.map(loadModule))
        .then(modules => {
          if (callback) callback.apply(null, modules);
        });
    };
  }
  
  // 异步加载模块
  function loadModule(id) {
    return new Promise((resolve, reject) => {
      if (modules[id] && modules[id].resolved) {
        resolve(modules[id].exports);
        return;
      }
      
      // 动态加载脚本
      const script = document.createElement('script');
      script.src = id + '.js';
      script.onload = () => {
        // 等待模块解析
        const checkInterval = setInterval(() => {
          if (modules[id] && modules[id].resolved) {
            clearInterval(checkInterval);
            resolve(modules[id].exports);
          }
        }, 10);
      };
      script.onerror = reject;
      document.head.appendChild(script);
    });
  }
  
  // 暴露到全局
  global.define = define;
  global.require = createRequire();
  
})(typeof window !== 'undefined' ? window : global);

// 使用示例
define('math', [], function() {
  return {
    add: function(a, b) { return a + b; }
  };
});

define('app', ['math', 'require'], function(math, require) {
  return {
    calculate: function() {
      return math.add(1, 2);
    },
    loadExtra: function() {
      require(['utils'], function(utils) {
        console.log('Utils loaded');
      });
    }
  };
});

require(['app'], function(app) {
  console.log(app.calculate()); // 3
});

三、CMD规范实现

CMD(Common Module Definition)规范由Sea.js推广,强调就近依赖。

3.1 简化的CMD实现
(function(global) {
  const modules = {};
  const factories = {};
  const cache = {};
  
  // 模块状态
  const STATUS = {
    PENDING: 0,
    LOADING: 1,
    LOADED: 2,
    EXECUTING: 3,
    EXECUTED: 4
  };
  
  // 定义函数
  function define(factory) {
    // 获取当前脚本
    const scripts = document.getElementsByTagName('script');
    const currentScript = scripts[scripts.length - 1];
    const id = currentScript.src.replace(/\.js$/, '');
    
    factories[id] = factory;
    modules[id] = {
      id: id,
      factory: factory,
      deps: [],
      exports: null,
      status: STATUS.PENDING,
      callbacks: []
    };
    
    // 解析依赖
    parseDependencies(id);
  }
  
  // 解析依赖
  function parseDependencies(id) {
    const factory = factories[id];
    if (!factory) return;
    
    const source = factory.toString();
    const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
    const deps = [];
    let match;
    
    while ((match = requireRegex.exec(source)) !== null) {
      deps.push(match[1]);
    }
    
    modules[id].deps = deps;
  }
  
  // 异步加载模块
  function require(id, callback) {
    const module = modules[id];
    
    if (module && module.status === STATUS.EXECUTED) {
      // 模块已执行,直接返回
      if (callback) {
        callback(module.exports);
      }
      return module.exports;
    }
    
    // 模块未加载,开始加载
    if (!module || module.status === STATUS.PENDING) {
      return loadModule(id, callback);
    }
    
    // 模块加载中,添加回调
    if (module.status < STATUS.EXECUTED) {
      module.callbacks.push(callback);
    }
  }
  
  // 加载模块
  function loadModule(id, callback) {
    const module = modules[id] || (modules[id] = {
      id: id,
      deps: [],
      exports: null,
      status: STATUS.LOADING,
      callbacks: callback ? [callback] : []
    });
    
    // 创建script标签加载
    const script = document.createElement('script');
    script.src = id + '.js';
    script.async = true;
    
    script.onload = function() {
      module.status = STATUS.LOADED;
      executeModule(id);
    };
    
    script.onerror = function() {
      console.error(`Failed to load module: ${id}`);
    };
    
    document.head.appendChild(script);
    
    return null;
  }
  
  // 执行模块
  function executeModule(id) {
    const module = modules[id];
    if (!module || module.status >= STATUS.EXECUTING) return;
    
    module.status = STATUS.EXECUTING;
    
    // 收集依赖
    const deps = module.deps;
    const depValues = deps.map(depId => {
      const depModule = modules[depId];
      if (depModule && depModule.status === STATUS.EXECUTED) {
        return depModule.exports;
      }
      // 同步加载依赖(简化实现)
      return require(depId);
    });
    
    // 执行工厂函数
    const factory = factories[id];
    if (!factory) {
      throw new Error(`Factory not found for module: ${id}`);
    }
    
    // 提供require、exports、module参数
    const localRequire = function(depId) {
      return require(depId);
    };
    
    const localExports = {};
    const localModule = { exports: localExports };
    
    // 执行
    const result = factory.call(null, localRequire, localExports, localModule);
    
    // 设置exports
    module.exports = localModule.exports || result || localExports;
    module.status = STATUS.EXECUTED;
    
    // 执行回调
    module.callbacks.forEach(cb => cb(module.exports));
    module.callbacks = [];
  }
  
  // 暴露全局
  global.define = define;
  global.require = require;
  
})(typeof window !== 'undefined' ? window : global);

// 使用示例
// 文件: math.js
define(function(require, exports, module) {
  module.exports = {
    add: function(a, b) {
      return a + b;
    }
  };
});

// 文件: app.js
define(function(require, exports, module) {
  var math = require('math');
  
  exports.calculate = function() {
    return math.add(1, 2);
  };
});

// 主文件
require('app', function(app) {
  console.log(app.calculate()); // 3
});

四、ES Module的简单Polyfill

虽然现代浏览器支持ES Modules,但在某些场景下,我们仍需要Polyfill支持。

4.1 基础ESM Polyfill实现
// ES Module Polyfill
(function() {
  const moduleMap = new Map();
  const moduleCache = new Map();
  
  // 拦截import语句(通过动态import实现)
  window.importModule = async function(modulePath) {
    // 检查缓存
    if (moduleCache.has(modulePath)) {
      return moduleCache.get(modulePath);
    }
    
    // 加载模块代码
    const code = await fetchModule(modulePath);
    
    // 解析依赖
    const deps = extractDependencies(code);
    
    // 加载依赖
    const depPromises = deps.map(dep => 
      importModule(resolvePath(modulePath, dep))
    );
    const dependencies = await Promise.all(depPromises);
    
    // 执行模块
    const moduleExports = {};
    const module = {
      exports: moduleExports
    };
    
    // 创建包装函数
    const wrapper = createWrapper(code, dependencies);
    wrapper(
      moduleExports, // exports
      module,        // module
      modulePath     // __filename(模拟)
    );
    
    // 缓存结果
    const exports = module.exports === moduleExports ? 
      moduleExports : module.exports;
    moduleCache.set(modulePath, exports);
    
    return exports;
  };
  
  // 提取依赖
  function extractDependencies(code) {
    const importRegex = /import\s+.*?\s+from\s+['"](.*?)['"]/g;
    const dynamicImportRegex = /import\s*\(['"](.*?)['"]\)/g;
    const deps = new Set();
    
    let match;
    while ((match = importRegex.exec(code)) !== null) {
      deps.add(match[1]);
    }
    
    // 重置正则
    importRegex.lastIndex = 0;
    
    while ((match = dynamicImportRegex.exec(code)) !== null) {
      deps.add(match[1]);
    }
    
    return Array.from(deps);
  }
  
  // 创建包装函数
  function createWrapper(code, dependencies) {
    const wrapperCode = `
      (function(exports, module, __filename, __dirname) {
        // 注入依赖
        const [
          ${dependencies.map((_, i) => `__dep${i}`).join(', ')}
        ] = arguments[4];
        
        ${code}
        
        // 返回默认导出
        return module.exports && module.exports.default ?
          module.exports.default : module.exports;
      })
    `;
    
    return eval(wrapperCode);
  }
  
  // 解析路径
  function resolvePath(basePath, targetPath) {
    if (targetPath.startsWith('./') || targetPath.startsWith('../')) {
      const baseDir = basePath.substring(0, basePath.lastIndexOf('/'));
      return new URL(targetPath, baseDir + '/').pathname;
    }
    return targetPath;
  }
  
  // 获取模块代码
  async function fetchModule(path) {
    const response = await fetch(path);
    if (!response.ok) {
      throw new Error(`Failed to load module: ${path}`);
    }
    return response.text();
  }
  
  // 拦截script type="module"
  interceptModuleScripts();
  
  function interceptModuleScripts() {
    const originalCreateElement = document.createElement;
    
    document.createElement = function(tagName) {
      const element = originalCreateElement.call(document, tagName);
      
      if (tagName === 'script') {
        const originalSetAttribute = element.setAttribute.bind(element);
        
        element.setAttribute = function(name, value) {
          originalSetAttribute(name, value);
          
          if (name === 'type' && value === 'module') {
            // 拦截模块脚本
            const src = element.getAttribute('src');
            if (src) {
              element.type = 'text/javascript';
              importModule(src).then(() => {
                if (element.onload) element.onload();
              }).catch(err => {
                if (element.onerror) element.onerror(err);
              });
            }
          }
        };
      }
      
      return element;
    };
  }
})();

// 使用示例
// 模块文件: utils.js
export function capitalize(str) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

export default function greet(name) {
  return `Hello, ${capitalize(name)}!`;
}

// 主文件
importModule('./utils.js').then(utils => {
  console.log(utils.default('world')); // Hello, World!
  console.log(utils.capitalize('test')); // Test
});
4.2 支持Tree Shaking的ESM Polyfill
class ESMCompat {
  constructor() {
    this.modules = new Map();
    this.usedExports = new Set();
  }
  
  // 注册模块
  register(name, code) {
    const ast = this.parse(code);
    const exports = this.extractExports(ast);
    
    this.modules.set(name, {
      code,
      ast,
      exports,
      used: new Set()
    });
  }
  
  // 解析代码为AST(简化版)
  parse(code) {
    // 简化实现:实际应使用Babel等解析器
    const exportMatches = code.match(/export\s+(const|let|var|function|class|default)\s+(\w+)/g) || [];
    const imports = code.match(/import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g) || [];
    
    return {
      exports: exportMatches.map(match => ({
        type: match.split(' ')[1],
        name: match.split(' ')[2]
      })),
      imports: imports.map(match => {
        const parts = match.match(/import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/);
        return {
          specifiers: parts[1].split(',').map(s => s.trim()),
          source: parts[2]
        };
      })
    };
  }
  
  // 提取导出
  extractExports(ast) {
    return ast.exports.map(exp => exp.name);
  }
  
  // 使用模块(标记使用的导出)
  use(name, ...exports) {
    const module = this.modules.get(name);
    if (module) {
      exports.forEach(exp => {
        if (module.exports.includes(exp)) {
          module.used.add(exp);
        }
      });
    }
  }
  
  // 生成优化后的代码
  generateOptimized(name) {
    const module = this.modules.get(name);
    if (!module) return '';
    
    let code = module.code;
    
    // 移除未使用的导出(简化实现)
    module.exports.forEach(exp => {
      if (!module.used.has(exp)) {
        const regex = new RegExp(`export\\s+.*?\\b${exp}\\b[^;]*;`, 'g');
        code = code.replace(regex, '');
      }
    });
    
    return code;
  }
}

// 使用示例
const compat = new ESMCompat();

compat.register('math', `
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export function unusedFunction() { return 'unused'; }
`);

// 标记使用的导出
compat.use('math', 'PI', 'add');

// 生成优化代码
console.log(compat.generateOptimized('math'));
// 输出将只包含PI和add的导出

五、模块化构建工具集成

现代开发中,我们使用构建工具处理模块化。以下展示如何集成Webpack-like的简单打包器。

5.1 简易模块打包器
const fs = require('fs');
const path = require('path');
const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');

class SimpleBundler {
  constructor(entry) {
    this.entry = entry;
    this.modules = new Map();
    this.moduleId = 0;
  }
  
  // 构建
  build(outputPath) {
    const entryModule = this.collectDependencies(this.entry);
    const bundleCode = this.generateBundle(entryModule);
    
    fs.writeFileSync(outputPath, bundleCode);
    console.log(`Bundle generated: ${outputPath}`);
  }
  
  // 收集依赖
  collectDependencies(filePath) {
    const fileContent = fs.readFileSync(filePath, 'utf-8');
    const ast = parse(fileContent, {
      sourceType: 'module',
      plugins: ['jsx']
    });
    
    const dependencies = [];
    const dirname = path.dirname(filePath);
    
    // 遍历AST收集import语句
    traverse(ast, {
      ImportDeclaration: ({ node }) => {
        const importPath = node.source.value;
        const absolutePath = this.resolvePath(importPath, dirname);
        dependencies.push(absolutePath);
      },
      CallExpression: ({ node }) => {
        if (node.callee.type === 'Import') {
          const importPath = node.arguments[0].value;
          const absolutePath = this.resolvePath(importPath, dirname);
          dependencies.push(absolutePath);
        }
      }
    });
    
    const moduleId = this.moduleId++;
    const module = {
      id: moduleId,
      filePath,
      code: fileContent,
      dependencies,
      mapping: {}
    };
    
    this.modules.set(filePath, module);
    
    // 递归收集依赖
    dependencies.forEach(dep => {
      if (!this.modules.has(dep)) {
        this.collectDependencies(dep);
      }
    });
    
    return module;
  }
  
  // 解析路径
  resolvePath(importPath, baseDir) {
    if (importPath.startsWith('.')) {
      return path.resolve(baseDir, importPath);
    }
    // 处理node_modules(简化)
    const nodeModulePath = path.resolve(process.cwd(), 'node_modules', importPath);
    if (fs.existsSync(nodeModulePath)) {
      return nodeModulePath;
    }
    return importPath;
  }
  
  // 生成打包代码
  generateBundle(entryModule) {
    const modules = [];
    
    // 创建模块映射
    this.modules.forEach(module => {
      const transformedCode = this.transformModule(module);
      modules.push(`
        ${module.id}: {
          factory: function(require, module, exports) {
            ${transformedCode}
          },
          mapping: ${JSON.stringify(module.mapping)}
        }
      `);
    });
    
    // 生成运行时
    return `
      (function(modules) {
        const moduleCache = {};
        
        function require(id) {
          if (moduleCache[id]) {
            return moduleCache[id].exports;
          }
          
          const mod = modules[id];
          const localRequire = function(modulePath) {
            return require(mod.mapping[modulePath]);
          };
          
          const module = { exports: {} };
          mod.factory(localRequire, module, module.exports);
          
          moduleCache[id] = module;
          return module.exports;
        }
        
        // 启动入口模块
        require(0);
      })({
        ${modules.join(',\n')}
      });
    `;
  }
  
  // 转换模块代码
  transformModule(module) {
    const ast = parse(module.code, {
      sourceType: 'module'
    });
    
    // 构建路径映射
    let importIndex = 0;
    
    traverse(ast, {
      ImportDeclaration: ({ node }) => {
        const importPath = node.source.value;
        const depModule = this.modules.get(
          this.resolvePath(importPath, path.dirname(module.filePath))
        );
        
        if (depModule) {
          const importName = `__import_${importIndex++}`;
          module.mapping[importPath] = depModule.id;
          
          // 替换import语句
          const specifiers = node.specifiers.map(spec => {
            if (t.isImportDefaultSpecifier(spec)) {
              return t.variableDeclarator(
                spec.local,
                t.memberExpression(
                  t.identifier(importName),
                  t.identifier('default')
                )
              );
            } else {
              return t.variableDeclarator(
                spec.local,
                t.memberExpression(
                  t.identifier(importName),
                  spec.imported || spec.local
                )
              );
            }
          });
          
          return t.variableDeclaration('const', specifiers);
        }
      }
    });
    
    // 移除export语句
    traverse(ast, {
      ExportNamedDeclaration: ({ node, remove }) => {
        if (node.declaration) {
          return node.declaration;
        }
        remove();
      },
      ExportDefaultDeclaration: ({ node }) => {
        return t.expressionStatement(
          t.assignmentExpression(
            '=',
            t.memberExpression(
              t.identifier('module'),
              t.identifier('exports')
            ),
            t.objectExpression([
              t.objectProperty(
                t.identifier('default'),
                node.declaration
              )
            ])
          )
        );
      }
    });
    
    const { code } = generate(ast);
    return code;
  }
}

// 使用示例
const bundler = new SimpleBundler('./src/index.js');
bundler.build('./dist/bundle.js');

六、模块联邦与微前端架构

模块联邦(Module Federation)是Webpack 5引入的重要特性,支持跨应用共享模块。

6.1 简易模块联邦实现
// 模块联邦管理器
class ModuleFederation {
  constructor(config) {
    this.config = config;
    this.remotes = new Map();
    this.exposes = new Map();
    this.shared = new Map();
    this.init();
  }
  
  init() {
    // 初始化共享模块
    if (this.config.shared) {
      Object.entries(this.config.shared).forEach(([name, config]) => {
        this.shared.set(name, {
          module: require(name),
          version: config.version,
          singleton: config.singleton || false
        });
      });
    }
    
    // 初始化暴露模块
    if (this.config.exposes) {
      Object.entries(this.config.exposes).forEach(([name, modulePath]) => {
        this.exposes.set(name, require(modulePath));
      });
    }
  }
  
  // 注册远程应用
  async registerRemote(name, url) {
    try {
      const remoteManifest = await this.fetchRemoteManifest(url);
      this.remotes.set(name, {
        url,
        manifest: remoteManifest
      });
      console.log(`Remote ${name} registered`);
    } catch (error) {
      console.error(`Failed to register remote ${name}:`, error);
    }
  }
  
  // 获取远程清单
  async fetchRemoteManifest(url) {
    const response = await fetch(`${url}/federation-manifest.json`);
    return response.json();
  }
  
  // 获取模块
  async getModule(remoteName, moduleName) {
    // 检查共享模块
    if (this.shared.has(moduleName)) {
      return this.shared.get(moduleName).module;
    }
    
    // 检查本地暴露
    if (this.exposes.has(moduleName)) {
      return this.exposes.get(moduleName);
    }
    
    // 检查远程模块
    const remote = this.remotes.get(remoteName);
    if (remote) {
      return this.loadRemoteModule(remote, moduleName);
    }
    
    throw new Error(`Module ${moduleName} not found`);
  }
  
  // 加载远程模块
  async loadRemoteModule(remote, moduleName) {
    const moduleUrl = `${remote.url}/${moduleName}.js`;
    
    // 动态加载脚本
    return new Promise((resolve, reject) => {
      const script = document.createElement('script');
      script.src = moduleUrl;
      
      script.onload = () => {
        // 假设远程模块会暴露到全局
        const module = window[`${remote.name}_${moduleName}`];
        if (module) {
          resolve(module);
        } else {
          reject(new Error(`Module ${moduleName} not found in remote`));
        }
      };
      
      script.onerror = reject;
      document.head.appendChild(script);
    });
  }
  
  // 暴露模块
  expose(name, module) {
    this.exposes.set(name, module);
    // 暴露到全局(供远程访问)
    window[`${this.config.name}_${name}`] = module;
  }
}

// 使用示例
// App 1 配置
const federation1 = new ModuleFederation({
  name: 'app1',
  exposes: {
    './Button': './src/components/Button.js'
  },
  shared: {
    react: { singleton: true, version: '17.0.0' },
    'react-dom': { singleton: true, version: '17.0.0' }
  }
});

// App 2 配置
const federation2 = new ModuleFederation({
  name: 'app2',
  remotes: {
    app1: 'http://localhost:3001'
  },
  shared: {
    react: { singleton: true, version: '17.0.0' }
  }
});

// App2中使用App1的模块
federation2.getModule('app1', 'Button').then(Button => {
  // 使用远程Button组件
  console.log('Remote Button loaded:', Button);
});
七、模块化性能优化
7.1 代码分割与懒加载
class CodeSplitter {
  constructor() {
    this.chunks = new Map();
    this.loadedChunks = new Set();
  }
  
  // 定义代码分割点
  defineChunk(name, getChunk) {
    this.chunks.set(name, getChunk);
  }
  
  // 懒加载代码块
  async loadChunk(name) {
    if (this.loadedChunks.has(name)) {
      return;
    }
    
    const getChunk = this.chunks.get(name);
    if (!getChunk) {
      throw new Error(`Chunk ${name} not found`);
    }
    
    // 标记为加载中
    this.loadedChunks.add(name);
    
    try {
      await getChunk();
      console.log(`Chunk ${name} loaded`);
    } catch (error) {
      this.loadedChunks.delete(name);
      throw error;
    }
  }
  
  // 预加载代码块
  preloadChunk(name) {
    if (this.loadedChunks.has(name)) return;
    
    const link = document.createElement('link');
    link.rel = 'preload';
    link.as = 'script';
    
    const getChunk = this.chunks.get(name);
    if (getChunk && getChunk.chunkPath) {
      link.href = getChunk.chunkPath;
      document.head.appendChild(link);
    }
  }
}

// Webpack动态导入兼容
function dynamicImport(modulePath) {
  if (typeof __webpack_require__ !== 'undefined') {
    // Webpack环境
    return import(/* webpackChunkName: "[request]" */ modulePath);
  } else {
    // 原生环境
    return import(modulePath);
  }
}

// 使用示例
const splitter = new CodeSplitter();

// 定义代码块
splitter.defineChunk('dashboard', () => 
  dynamicImport('./Dashboard.js')
);

splitter.defineChunk('analytics', () => 
  dynamicImport('./Analytics.js')
);

// 路由懒加载
async function loadRoute(routeName) {
  switch (routeName) {
    case 'dashboard':
      await splitter.loadChunk('dashboard');
      break;
    case 'analytics':
      await splitter.loadChunk('analytics');
      break;
  }
}

// 预加载
window.addEventListener('mouseover', (e) => {
  if (e.target.href && e.target.href.includes('dashboard')) {
    splitter.preloadChunk('dashboard');
  }
});
7.2 模块缓存策略
class ModuleCache {
  constructor() {
    this.cache = new Map();
    this.ttl = 5 * 60 * 1000; // 5分钟
    this.maxSize = 100; // 最大缓存模块数
  }
  
  // 获取模块
  async get(key, fetchModule) {
    const cached = this.cache.get(key);
    
    // 检查缓存是否有效
    if (cached && Date.now() - cached.timestamp < this.ttl) {
      console.log(`Cache hit: ${key}`);
      return cached.module;
    }
    
    // 缓存失效或不存在,重新获取
    console.log(`Cache miss: ${key}`);
    const module = await fetchModule();
    
    // 更新缓存
    this.set(key, module);
    
    return module;
  }
  
  // 设置缓存
  set(key, module) {
    // 清理过期缓存
    this.cleanup();
    
    this.cache.set(key, {
      module,
      timestamp: Date.now()
    });
  }
  
  // 清理缓存
  cleanup() {
    const now = Date.now();
    
    // 清理过期
    for (const [key, value] of this.cache) {
      if (now - value.timestamp > this.ttl) {
        this.cache.delete(key);
      }
    }
    
    // 清理超出大小限制的(LRU策略)
    if (this.cache.size > this.maxSize) {
      const entries = Array.from(this.cache.entries());
      entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
      
      for (let i = 0; i < entries.length - this.maxSize; i++) {
        this.cache.delete(entries[i][0]);
      }
    }
  }
  
  // 清空缓存
  clear() {
    this.cache.clear();
  }
}

// 使用示例
const moduleCache = new ModuleCache();

async function loadModuleWithCache(modulePath) {
  return moduleCache.get(modulePath, async () => {
    const response = await fetch(modulePath);
    return response.text();
  });
}

八、模块化最佳实践与工程化

8.1 模块设计原则
// 1. 单一职责原则
// 不好的例子
class UserManager {
  // 混合了用户管理、验证、通知等多个职责
}

// 好的例子
class UserRepository {
  // 只负责数据访问
}

class UserValidator {
  // 只负责验证
}

class UserNotifier {
  // 只负责通知
}

// 2. 依赖注入
class UserService {
  constructor(userRepository, validator, notifier) {
    this.userRepository = userRepository;
    this.validator = validator;
    this.notifier = notifier;
  }
  
  async register(user) {
    if (!this.validator.validate(user)) {
      throw new Error('Invalid user');
    }
    
    await this.userRepository.save(user);
    await this.notifier.sendWelcome(user.email);
  }
}

// 3. 接口抽象
// 定义接口
class IStorage {
  async save(key, value) {}
  async get(key) {}
  async delete(key) {}
}

// 具体实现
class LocalStorage extends IStorage {
  async save(key, value) {
    localStorage.setItem(key, JSON.stringify(value));
  }
  
  async get(key) {
    return JSON.parse(localStorage.getItem(key));
  }
  
  async delete(key) {
    localStorage.removeItem(key);
  }
}

class APIService {
  constructor(storage) {
    if (!(storage instanceof IStorage)) {
      throw new Error('Invalid storage implementation');
    }
    this.storage = storage;
  }
}
8.2 模块版本管理与升级
class ModuleVersionManager {
  constructor() {
    this.versions = new Map();
    this.deprecations = new Map();
  }
  
  // 注册模块版本
  register(moduleName, version, module) {
    if (!this.versions.has(moduleName)) {
      this.versions.set(moduleName, new Map());
    }
    
    this.versions.get(moduleName).set(version, module);
  }
  
  // 获取模块(支持语义化版本)
  get(moduleName, versionRange = 'latest') {
    const moduleVersions = this.versions.get(moduleName);
    if (!moduleVersions) {
      throw new Error(`Module ${moduleName} not found`);
    }
    
    if (versionRange === 'latest') {
      const latestVersion = Array.from(moduleVersions.keys())
        .sort(this.compareVersions)
        .pop();
      return moduleVersions.get(latestVersion);
    }
    
    // 简化的版本范围解析
    const availableVersions = Array.from(moduleVersions.keys())
      .filter(v => this.satisfiesVersion(v, versionRange))
      .sort(this.compareVersions);
    
    if (availableVersions.length === 0) {
      throw new Error(`No version of ${moduleName} satisfies ${versionRange}`);
    }
    
    return moduleVersions.get(availableVersions.pop());
  }
  
  // 比较版本
  compareVersions(v1, v2) {
    const parts1 = v1.split('.').map(Number);
    const parts2 = v2.split('.').map(Number);
    
    for (let i = 0; i < 3; i++) {
      if (parts1[i] !== parts2[i]) {
        return parts1[i] - parts2[i];
      }
    }
    
    return 0;
  }
  
  // 检查版本是否满足范围
  satisfiesVersion(version, range) {
    // 简化实现,实际应使用semver库
    if (range === '*') return true;
    
    const [op, versionRange] = range.match(/^([>=<~^]*)(\d+\.\d+\.\d+)$/).slice(1);
    const vParts = version.split('.').map(Number);
    const rParts = versionRange.split('.').map(Number);
    
    switch (op) {
      case '^': // 兼容版本
        return vParts[0] === rParts[0] && vParts[1] >= rParts[1];
      case '~': // 近似版本
        return vParts[0] === rParts[0] && 
               vParts[1] === rParts[1] && 
               vParts[2] >= rParts[2];
      case '>=':
        return this.compareVersions(version, versionRange) >= 0;
      case '>':
        return this.compareVersions(version, versionRange) > 0;
      case '<=':
        return this.compareVersions(version, versionRange) <= 0;
      case '<':
        return this.compareVersions(version, versionRange) < 0;
      default:
        return version === versionRange;
    }
  }
  
  // 弃用通知
  deprecate(moduleName, version, message) {
    if (!this.deprecations.has(moduleName)) {
      this.deprecations.set(moduleName, new Map());
    }
    
    this.deprecations.get(moduleName).set(version, {
      message,
      deprecatedAt: new Date()
    });
    
    // 添加控制台警告
    console.warn(`Module ${moduleName}@${version} is deprecated: ${message}`);
  }
}

// 使用示例
const versionManager = new ModuleVersionManager();

// 注册不同版本
versionManager.register('utils', '1.0.0', {
  oldMethod: () => 'old'
});

versionManager.register('utils', '1.1.0', {
  oldMethod: () => 'old',
  newMethod: () => 'new'
});

versionManager.register('utils', '2.0.0', {
  newMethod: () => 'new',
  betterMethod: () => 'better'
});

// 标记弃用
versionManager.deprecate('utils', '1.0.0', '请升级到1.1.0+版本');

// 获取模块
const utilsV1 = versionManager.get('utils', '^1.0.0');
console.log(utilsV1); // 1.1.0版本

const utilsLatest = versionManager.get('utils');
console.log(utilsLatest); // 2.0.0版本

总结

模块化是现代前端工程化的基石,从前端的脚本标签到ES Modules,再到模块联邦等高级模式,模块化技术不断演进。本文从简单模块加载器实现开始,逐步深入AMD、CMD规范,探讨ES Module的Polyfill技术,并补充了构建工具集成、模块联邦、性能优化等工程化实践。

关键要点总结:

  1. 模块加载器核心原理: 依赖管理、缓存、异步加载
  2. 规范演进: 从AMD/CMD到ES Modules的统一
  3. 工程化实践: 代码分割、懒加载、版本管理、依赖注入
  4. 未来趋势: 模块联邦、微前端架构、Web Assembly模块化

模块化不仅仅是技术选择,更是一种设计哲学。良好的模块化设计能够提升代码的可维护性、可测试性和团队协作效率。在实际项目中,应根据团队规模、项目复杂度和技术栈选择合适的模块化方案,并不断优化模块边界和依赖关系。

随着前端技术的不断发展,模块化将继续演进,但核心原则——关注点分离、接口抽象、依赖管理——将始终保持不变。掌握模块化的核心原理和实践,能够帮助开发者构建更健壮、可维护的前端应用。

从零搭一个 Vue 小家:用 Vite + 路由轻松入门现代前端开发

从零开始,轻松走进 Vue 的世界:一个“全家桶”小项目的搭建之旅

如果你刚刚接触前端开发,听到“Vue”、“Vite”、“路由”这些词时是不是有点懵?别担心!我们可以把写代码想象成搭积木、装修房子、甚至安排一场家庭旅行。今天,我们就通过一个名为 all-vue 的小项目,带你一步步理解现代 Vue 应用是怎么“搭起来”的。


🏠 第一步:选好地基——用 Vite 快速建项目

什么是vite?

Vite(法语,意为“快”)是一个由 Vue.js 作者 尤雨溪(Evan You) 主导开发的现代化前端构建工具。它旨在解决传统打包工具(如 Webpack)在开发阶段启动慢、热更新(HMR)延迟高等问题,提供极速的开发体验。

想象你要盖一栋房子。传统方式可能要先打地基、砌砖、铺电线……繁琐又耗时。而 Vite 就像一位超级高效的建筑承包商,你只要说一句:“我要一个 Vue 房子”,它立刻给你搭好框架,连水电都通好了!

在终端里运行:

npm init vite@latest all-vue -- --template vue

几秒钟后,你就得到了一个结构清晰的项目目录。其中最关键的是:

  • index.html:这是你房子的“大门”,浏览器一打开就看到它。
  • src/main.js:这是整栋房子的“总开关”,负责启动整个应用。
  • src/App.vue:这是“客厅”,所有房间(页面)都要从这里进出。

Vite 的优势在于——修改代码后,浏览器几乎瞬间刷新,就像你换了个沙发,家人马上就能坐上去试舒服不舒服。


🏗️ 第二步:认识整栋楼——项目结构概览

运行 npm init vite@latest all-vue -- --template vue 后,你会得到这样一栋“数字公寓”:

项目结构简略预览:

/all-vue
├── public/            # 公共资源(如 logo.png)
├── src/
│   ├── assets/        # 图片、字体等静态资源
│   ├── components/    # 可复用的小部件(按钮、卡片等)
│   ├── views/         # 独立页面(首页、关于页等)
|   |     |—— About.vue # 关于页面的Vue组件
|   |     |—— Home.vue # 主页的vue组件
│   ├── router/        # 室内导航系统
|   |     |—— index.js # 路由总控
│   ├── App.vue        # 中央控制台(客厅)
│   └── main.js        # 智能钥匙
├── index.html         # 入户大门
├── package.json       # 公寓的“住户手册 + 装修清单”
└── vite.config.js     # 建筑规范说明书

其中,package.json 就像这栋楼的住户手册 + 装修材料清单。打开它,你会看到:

{
  "name": "all-vue",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.4.0",
    "vue-router": "^4.3.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.0",
    "vite": "^5.0.0"
  }
}
  • dependencies:这是“入住必需品”,比如 Vue 框架本身、路由系统——没有它们,房子没法正常运转;
  • devDependencies:这是“装修工具包”,只在开发时用(比如 Vite 构建工具),住户入住后就不需要了;
  • scripts:这是“快捷指令”,比如 npm run dev 就是“启动预览模式”,npm run build 是“打包交付”。

有了这份清单,任何开发者都能一键还原你的整套环境——就像照着宜家说明书组装家具一样可靠。


🚪 第三步:认识“大门”——index.html 的两个秘密

虽然现代 Vue 应用的逻辑几乎全在 JavaScript 和 .vue 文件里,但一切的起点,其实是这个看似简单的 index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>all-vue</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>

别小看这十几行代码,它藏着两个关键设计:

🔌 1. <div id="app"></div>:Vue 的“插座”

你可以把它想象成墙上预留的一个智能插座面板。它本身空无一物,但一旦通电(Vue 应用启动),就会自动“投影”出整个用户界面。

main.js 中,我们这样写:

createApp(App).mount('#app')

这句话的意思就是:“请把 App.vue 这个‘客厅’的内容,投射到 id 为 app 的那个插座上。”
没有这个插座,Vue 再厉害也无处施展;有了它,动态内容才能在静态 HTML 中生根发芽。

⚡ 2. <script type="module" src="/src/main.js"></script>:原生 ES 模块的魔法

注意这里的 type="module"。这是现代浏览器支持的一种原生模块加载方式。传统脚本是“一股脑全塞进来”,而模块化脚本则像快递包裹——每个文件独立打包,按需引用,互不干扰。

Vite 正是利用了这一特性,无需打包即可直接在浏览器中运行模块化的代码。这意味着:

  • 开发时启动飞快(冷启动快);
  • 修改文件后热更新极快(HMR 精准替换);
  • 代码结构清晰,符合现代工程规范。

所以,index.html 不仅是入口,更是连接静态 HTML 世界动态 Vue 世界的桥梁。


🔑 第四步:打造“钥匙”——main.js 如何启动应用

有了大门,就得有钥匙。main.js 就是这把精密的电子钥匙,负责激活整套智能家居系统:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './style.css'

createApp(App).use(router).mount('#app')

这段代码做了三件事,环环相扣:

  1. 引入核心模块:从 Vue 拿到“造房子”的工具(createApp),从本地拿到“客厅设计图”(App.vue)和“导航系统”(router);
  2. 组装系统:用 .use(router) 把导航插件装进主程序;
  3. 插入插座.mount('#app') 表示:“请把这套系统通电安装在 index.html 中 id 为 app 的插座上。”

没有这把钥匙,再漂亮的客厅也只是一堆图纸;有了它,整个房子才真正“活”起来。


💡 第五步:点亮客厅——根组件 App.vue

钥匙转动,门开了,我们走进 App.vue —— 这是所有功能的总控中心:

<template>
  <div id="app">
    <nav>
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </nav>
    <router-view />
  </div>
</template>

多人一开始会直接写 <div>Home | About</div>,但这只是静态文字。要让它们变成可点击的导航,就得用 Vue Router 提供的 <router-link> 组件。

这里有两个核心元素:

  • <router-link> :智能门把手,点击不刷新页面,只切换内容;
  • <router-view /> :魔法地板,当前该展示哪个房间,它就实时投影出来。

虽然原始文件只写了 HomeAbout,但正确的写法应如上所示——让文字变成可交互的导航。


🗺️ 第六步:装上导航系统——配置 Vue Router

路由,就像是你家里的智能导航系统。没有它,你只能待在客厅;有了它,你才能自由穿梭于各个房间。

我们在 src/router/index.js 中这样配置:

import { createRouter, createWebHashHistory } from 'vue-router';
import Home from '../views/Home.vue'
import About from '../views/About.vue'

const routes = [
  { path: '/', name: 'Home', component: Home },
  { path: '/about', name: 'About', component: About }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router

这段代码的意思是:

  • 当用户访问 /(也就是主页),就显示 Home.vue 这个房间;
  • 当用户访问 /about,就带他去 About.vue 那个房间。

注意这里用了 createWebHashHistory(),这意味着网址会变成 http://localhost:5173/#/about。那个 # 就像门牌号里的“分隔符”,告诉系统:“后面的部分是内部房间号,不是新地址”。


🛋️ 第七步:布置房间——编写页面组件

现在,我们来装修两个房间。

首页(Home.vue)

<template>
  <div>
    <h1>Home</h1>
  </div>
</template>

关于页(About.vue)

<template>
  <div>
    <h1>About</h1>
  </div>
</template>

每个 .vue 文件都是一个自包含的“功能单元”:有自己的结构(template)、逻辑(script)和样式(style)。它们彼此隔离,却能通过路由无缝切换。


🎨 第八步:美化家园——全局样式 style.css

虽然功能齐备,但房子还是灰扑扑的。这时候,style.css 就派上用场了。你可以在这里写:

body {
  font-family: 'Arial', sans-serif;
  background-color: #f5f5f5;
}

nav {
  padding: 1rem;
  background: white;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

就像给墙壁刷漆、给地板打蜡,让整个家更温馨舒适。


▶️ 最后一步:启动你的 Vue 家园!

现在,所有“装修材料”都已就位——地基打好了(Vite 项目)、大门装上了(index.html)、钥匙配好了(main.js)、客厅布置妥当(App.vue),连房间(Home.vueAbout.vue)和导航系统(Vue Router)也都调试完毕。是时候打开电闸,点亮整栋房子了!

请在终端(命令行)中依次执行以下两条命令(确保你已在 all-vue 项目目录下):

# 第一步:安装“住户手册”里列出的所有依赖(比如 Vue 和路由)
npm install

# 第二步:启动开发服务器——相当于按下“智能家居总开关”
npm run dev

运行成功后,你会看到类似这样的提示:

  VITE v5.0.0  ready in 320 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose

这时,只需打开浏览器,访问 http://localhost:5173/ (端口号可能略有不同),就能看到你的 Vue 小家啦!

image.png

  • 点击 Home,客厅中央显示 “Home”;
  • 点击 About,瞬间切换到 “About” 页面——全程无需刷新,就像在家自由走动一样丝滑。

🎉 恭喜你!你不仅看懂了代码,还亲手让它跑起来了!

这不再是一堆抽象的文件,而是一个真正能交互的 Web 应用。你已经完成了从“零”到“一”的飞跃——而这,正是所有伟大项目的起点。


🧩 总结:Vue 项目的“生活化”逻辑链

让我们用一次智能家居入住体验来串起全过程:

  1. Vite 是开发商:提供标准化精装修样板间;
  2. index.html 是入户门:设有智能插座(#app)和模块化接线口(type="module");
  3. main.js 是电子钥匙:插入后激活整套系统;
  4. App.vue 是中央控制台:集成导航与内容展示区;
  5. Vue Router 是室内导航图:定义各房间路径;
  6. Home.vue / About.vue 是功能房间:各自独立,按需进入;
  7. style.css 是全屋软装方案:统一视觉风格。

✨ 写在最后:你已经站在 Vue 的门口

这个 all-vue 项目虽小,却包含了现代 Vue 应用的核心骨架:组件化 + 路由 + 响应式 + 工程化构建。你不需要一开始就懂所有细节,就像学骑自行车,先扶稳车把,再慢慢蹬脚踏。

当你运行 npm run dev,看到浏览器里出现“Home”和“About”两个链接,并能自由切换时——恭喜你,你已经成功迈出了 Vue 开发的第一步!

接下来,你可以:

  • 在 Home 里加一张图片;
  • 在 About 里写一段自我介绍;
  • 用 CSS 让导航栏变彩色;
  • 甚至添加第三个页面……

编程不是魔法,而是一步步搭建的过程。而你,已经搭好了第一块积木。

爬楼梯?不,你在攀登算法的珠穆朗玛峰!

爬楼梯?不,你在攀登算法的珠穆朗玛峰!

一道看似“幼儿园难度”的面试题:
“每次能爬1阶或2阶,问爬到第n阶有几种方法?”
却暗藏递归、动态规划、记忆化、空间优化四大内功心法——
它不是考你会不会算数,而是看你有没有系统性思维


🧗‍♂️ 初见:天真递归 —— “我能行!”(然后爆栈了)

最直觉的解法?当然是递归!

function climbStairs(n) {
  if (n === 1) return 1;
  if (n === 2) return 2;
  return climbStairs(n - 1) + climbStairs(n - 2);
}

逻辑完美

  • 要到第 n 阶,要么从 n-1 上来,要么从 n-2 跳上来
  • 所以 f(n) = f(n-1) + f(n-2) —— 这不就是斐波那契?

但问题来了:
当你调用 climbStairs(45),电脑会疯狂重复计算:

  • f(43) 被算两次
  • f(42) 被算三次
  • ……
    时间复杂度 O(2ⁿ) —— 指数爆炸!

就像你让一个人背完整本字典来查一个词——可行,但荒谬。


🧠 进阶:记忆化递归 —— “我记住了!”

既然重复计算是罪魁祸首,那就把算过的答案存起来

const memo = {};
function climbStairs(n) {
  if (n === 1) return 1;
  if (n === 2) return 2;
  if (memo[n]) return memo[n]; // ← 关键:查缓存!
  memo[n] = climbStairs(n - 1) + climbStairs(n - 2);
  return memo[n];
}

效果:每个 f(k) 只算一次 → 时间复杂度 O(n)
思想空间换时间,典型的自顶向下动态规划(Top-down DP)

但有个小瑕疵:memo 是全局变量,容易被污染。


🔒 优雅封装:闭包 + 记忆化 —— “我的缓存,外人别碰!”

闭包memo 私有化,打造一个“智能函数”:

const climbStairs = (function() {
  const memo = {}; // ← 外部无法访问!
  return function climb(n) {
    if (n === 1) return 1;
    if (n === 2) return 2;
    if (memo[n]) return memo[n];
    memo[n] = climb(n - 1) + climb(n - 2);
    return memo[n];
  };
})();

优势

  • 多次调用共享缓存(越用越快)
  • 状态私有,安全可靠
  • 接口干净:用户只需 climbStairs(n)

这不是函数,这是一个会学习、有记忆、懂封装的智能体


🚀 终极优化:自底向上 + 滚动变量 —— “我不需要递归!”

其实,我们根本不需要递归,也不需要存所有中间值!

观察规律:

f(1) = 1
f(2) = 2
f(3) = f(2) + f(1) = 3
f(4) = f(3) + f(2) = 5
...

只需要两个变量,就能滚动计算出结果:

function climbStairs(n) {
  if (n === 1) return 1;
  if (n === 2) return 2;
  
  let prevPrev = 1; // f(i-2)
  let prev = 2;     // f(i-1)
  
  for (let i = 3; i <= n; i++) {
    const current = prev + prevPrev; // f(i)
    prevPrev = prev;   // 滚动窗口
    prev = current;
  }
  
  return prev;
}

时间复杂度:O(n)
空间复杂度:O(1) —— 极致优化!
无递归:避免调用栈溢出(n 很大时更安全)

这就是自底向上的动态规划(Bottom-up DP) —— 从已知出发,一步步推导未知。


📊 四种解法对比

方法 时间复杂度 空间复杂度 是否递归 适用场景
暴力递归 O(2ⁿ) O(n) 教学演示
记忆化递归 O(n) O(n) 中等规模,逻辑清晰
闭包记忆化 O(n) O(n) 需要缓存复用
滚动变量 O(n) O(1) 生产环境首选

💡 面试加分回答

当面试官问这道题,你可以这样说:

“我会根据场景选择方案:

  • 如果是教学或快速原型,用记忆化递归,逻辑直观;
  • 如果是高性能生产环境,用滚动变量的迭代法,O(1) 空间且无栈溢出风险。
    此外,我还会考虑边界情况(如 n ≤ 0)、类型校验,以及是否需要支持‘每次可爬1~k阶’的扩展。”

——瞬间从“会写代码”升级到“有工程思维”。


🌟 结语:小题大智慧

“爬楼梯”从来不是一道数学题,而是一面镜子:

  • 它照出你是否理解递归的本质
  • 它检验你是否掌握动态规划的思想
  • 它考验你能否在简洁、性能、可维护性之间做权衡

下次再有人说“这题太简单”,你可以微笑回应:

“是啊,简单到能写出四种境界。”

而这,正是优秀工程师和普通 coder 的分水岭。

🚀别再卷 Redux 了!Zustand 才是 React 状态管理的躺平神器

Zustand VS Redux

在文章开始前咱们先唠嗑一下,各位平时用哪个更多点呢?大数据不会骗人:

首先GitHub上的 Star 数量比较: image.png

image.png

其次每周的下载数量比较:

image.png

image.png

显然,想必用Zustand的可能大概也许应该会居多(单纯看数据来讲)。那么明明Redux才是大哥,为啥被Zustand这个小弟后来居上了?

给大家一个表:

对比项 Redux(老牌流程派) Zustand(新晋清爽党)
上手门槛 高:得记 action type、reducer、Provider 等一堆概念 低:会用 React Hook 就能写,几行代码起手
代码量 多:改个 count 得写 action、reducer 一堆模板代码 少:创建 store + 组件调用,加起来不到 20 行
组件里怎么用 得用 useSelector 取数据 + useDispatch 发动作 直接 useStore( state => state.xxx ) 一步到位
要不要包 Provider 必须包:得用 <Provider store={store}> 裹整个 App 不用包:组件直接调用 store,省一层嵌套
适合场景 大型复杂项目(多人协作、状态逻辑多) 中小型项目 / 快速开发(想少写代码、快速落地)

相信看完表大家已经很明了了,那么如果还想深入了解的可以自行去搜搜,我们唠嗑就到这,开始今天的学习。

具体资料大家去官网看:

www.npmjs.com/package/zus…

www.npmjs.com/package/rea…

前言

想象一下:你正在开发一个 React 项目,Home 组件要改个数字,About 组件得同步显示,List 组件还要从接口拉数据 —— 要是每个组件都自己存状态,代码早乱成一锅粥了!今天咱们就用 Zustand 这个躺平神器,把这些组件串成丝滑的整体,顺便解锁 React 全局状态的 “极简玩法”

一、先搭个 “状态仓库”:Zustand 初体验

Zustand 是啥?你可以把它理解成一个 “共享储物柜”:组件们不用再互相传 props,直接从这个柜子里拿数据、调方法就行。

首先你需要下载Zustand(在开篇的资料里也可以找到~):

image.png

先看我们的第一个 “储物格”——count.js(负责管理计数状态):

// src/store/count.js
import { create } from "zustand";

// 用 create 造一个“状态仓库”
const useCountStore = create((set) => ({
    // 存数据:初始计数是0,还有个默认年龄19
    count: 0,
    age: 19,
    // 存方法:点一下计数+1(set会自动更新视图)
    increase: () => set((state) => ({ count: state.count + 1 })),
    // 传个参数,计数直接减val
    decrease: (val) => set((state) => ({ count: state.count - val }))
}))

export default useCountStore;

就这么几行,一个能 “存数据 + 改数据” 的全局状态就搞定了 —— 比 Redux 轻量到没朋友!

二、组件 “抢着用”:状态共享原来这么丝滑

有了仓库,组件们就能 “按需取货” 了。先看 Home 组件(负责操作计数):

// src/components/Home.jsx
import useCountStore from '../store/count.js'

export default function Home() {
    // 从仓库里“拿”count数据
    let count = useCountStore((state) => state.count);
    // 从仓库里“拿”increase、decrease方法
    const increase = useCountStore((state) => state.increase);
    const decrease = useCountStore((state) => state.decrease);
    return (
        <div>
            {/* 点按钮直接调仓库里的方法,不用传参! */}
            <button onClick={increase}>发送-{count}</button>
            <button onClick={() => decrease(10)}>减少-{count}</button>
        </div>
    )
}

再看 About 组件(负责显示计数):

// src/components/About.jsx
import useCountStore from "../store/count"

export default function About() {
    // 同样从仓库拿count,Home改了这里自动更!
    let count = useCountStore((state) => state.count);
    return (
        <div>
            <h2>title-{count}</h2>
        </div>
    )
}

点击前:

image.png

点击10次发送后:

image.png

刷新然后点击10次减少后:

image.png

你看你看你看看看,Home 点按钮改了 count,About 里的标题直接同步更新 —— 连 props 都不用传,这丝滑感谁用谁知道!

三、进阶玩法:状态里塞接口请求

光存数字才哪到哪,还不够炫!咱们给仓库加个 “拉接口” 的功能。先写 list.js(负责管理列表数据):

// src/store/list.js
import { create } from "zustand";

// 先写个请求接口的函数
const fetchApi = async () => {
    const response = await fetch('https://mock.mengxuegu.com/mock/66585c4db462b81cb3916d3e/songer/songer');
    const res = await response.json();
    return res.data; // async函数的return会变成Promise的resolve值
}

// 造个存列表的仓库
const useListStore = create((set) => ({
    list: [], // 初始列表是空数组
    // 存个“拉列表”的方法,里面调用接口
    fetchList: async () => {
        const res = await fetchApi();
        set({ list: res }) // 拿到数据后更新list
    }
}))

export default useListStore;

然后让 List 组件 用这个仓库:

// src/components/List.jsx
import { useEffect } from "react";
import useListStore from "../store/list"

export default function List() {
    // 从仓库拿list数据和fetchList方法
    const list = useListStore((state) => state.list);
    const fetchList = useListStore((state) => state.fetchList);

    // 组件一加载就调用接口拉数据
    useEffect(() => {
        fetchList()
    }, [])

    return (
        <div>
            {/* 拿到数据直接map渲染 */}
            {list.map((item) => {
                return <div key={item.name}>{item.name}</div>
            })}
        </div>
    )
}

接口数据就出现在浏览器上啦:

image.png

打开页面,List 组件会自动拉接口、存数据、渲染列表 —— 状态管理 + 接口请求,一套流程直接在仓库里包圆了!

四、最后一步:把组件都塞进 App

最后在 App.jsx 里把这些组件拼起来:

import Home from "./components/Home"
import About from "./components/About"
import List from "./components/List"

export default function App() {
    return (
        <div>
            <Home></Home>
            <About></About>
            <List></List>
        </div>
    )
}

image.png

启动项目,你会看到:About 显示着计数,List 自动渲染接口数据 —— 这就是 Zustand 给 React 带来的 “状态自由”

总结

Zustand 堪称 React 状态管理的 “轻骑兵”:无需写冗余的 reducer、不用嵌套 Provider 包裹组件树,几行代码就能搭建全局状态仓库。它剥离了传统状态管理的繁琐仪式感,让我们彻底摆脱模板代码的束缚,聚焦业务本身。

结语

相比 Redux 的 “厚重” 和 Context API 在高频更新下的性能短板,Zustand 就像一把恰到好处的 “瑞士军刀”,轻巧却锋利,用最简单的方式解决了 React 组件间的状态共享难题,让开发者能把更多精力放在业务逻辑本身,而不是状态管理的 “套路” 里。

好的工具从来不是炫技的枷锁,而是让开发者回归创造本身的桥梁。

从零到一:彻底搞定面试高频算法——“列表转树”与“爬楼梯”全解析

在前端面试中,算法往往是决定能否拿高薪的关键。很多同学一听到“算法”就头大,觉得那是天才玩的游戏。其实,大多数面试算法题考察的不是你的数学造诣,而是你对递归(Recursion)和逻辑处理的理解。

今天,我们就通过两个非常经典的面试真题—— “列表转树(List to Tree)”和“爬楼梯(Climbing Stairs)” ,带你从小白视角拆解算法的奥秘。

第一部分:列表转树 —— 业务中的“常青树”

1. 为什么要学这个?

在实际开发中,后端返回给我们的数据往往是“扁平化”的。比如一个省市区选择器,或者一个后台管理系统的左侧菜单导航。为了存储方便,数据库通常会存储为如下结构:

id parentId name
1 0 中国
2 1 北京
3 1 上海
4 2 东城区

但前端 UI 组件(如 Element UI 的 Tree 组件)需要的是一个嵌套的树形对象。如何把上面的表格数据转换成包含 children 的树?这就是面试官考察你的数据结构处理能力。

2. 解法一:暴力递归(最符合人类直觉)

核心逻辑:

  1. 遍历列表,找到根节点(parentId === 0)。
  2. 对于每一个节点,再去列表里找谁的 parentId 等于我的 id
  3. 递归下去,直到找不到子节点。
// 代码参考
function list2tree(list, parentId = 0) {
  const result = []; 
  list.forEach(item => {
    if (item.parentId === parentId) {
      // 这里的递归就像是在问:谁是我的孩子?
      const children = list2tree(list, item.id);
      if (children.length) {
        item.children = children;
      }
      result.push(item);
    }
  });
  return result;
}

小白避坑指南:

这种方法的复杂度是 O(n2)O(n^2)。如果列表有 1000 条数据,最坏情况下要跑 100 万次循环。面试官此时会问:“有没有更优的方法?”

3. 解法二:优雅的 ES6 函数式写法

如果你想让代码看起来更“高级”,可以利用 filtermap

function list2tree(list, parentId = 0) {
  return list
    .filter(item => item.parentId === parentId) // 过滤出当前的子节点
    .map(item => ({
      ...item, // 展开原有属性
      children: list2tree(list, item.id) // 递归寻找后代
    }));
}

4. 解法三:空间换时间(面试官最爱)

为了把时间复杂度降到 O(n)O(n),我们可以利用 Map 对象。Map 的查询速度极快,像是一个“瞬移器”。

思路:

  1. 先遍历一遍列表,把所有节点存入 Map 中,以 id 为 Key。
  2. 再遍历一遍,根据 parentId 直接从 Map 里把父节点“揪”出来,把当前节点塞进父节点的 children 里。
// 代码参考
function listToTree(list) {
    const nodeMap = new Map();
    const tree = [];

    // 第一遍:建立映射表
    list.forEach(item => {
        nodeMap.set(item.id, { ...item, children: [] });
    });

    // 第二遍:建立父子关系
    list.forEach(item => {
        const node = nodeMap.get(item.id);
        if (item.parentId === 0) {
            tree.push(node); // 根节点入队
        } else {
            // 直接通过 parentId 找到父亲,把儿子塞进去
            nodeMap.get(item.parentId)?.children.push(node);
        }
    });
    return tree;
}

优点: 只遍历了两遍列表。无论数据有多少,速度依然飞快。

第二部分:爬楼梯 —— 掌握算法的“分水岭”

如果说“列表转树”考察的是业务能力,那“爬楼梯”考察的就是编程思维

题目描述: 假设你正在爬楼梯。需要 nn 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

1. 自顶向下:递归的艺术

我们站在第 nn 阶往回看:

  • 要到达第 10 阶,你只能从第 9 阶跨 1 步上来,或者从第 8 阶跨 2 步上来。
  • 所以:f(10)=f(9)+f(8)f(10) = f(9) + f(8)

这就是著名的斐波那契数列公式

// 基础版
function climbStairs(n) {
    if (n === 1) return 1;
    if (n === 2) return 2;
    return climbStairs(n - 1) + climbStairs(n - 2);
}

致命缺陷: 这个代码会跑死电脑。算 f(10)f(10) 时要算 f(9)f(9)f(8)f(8);算 f(9)f(9) 时又要算一遍 f(8)f(8)。大量的重复计算导致“爆栈”。

2. 优化:带备忘录的递归(记忆化)

我们可以准备一个“笔记本”(memo),算过的值就记下来,下次直接拿。

const memo = {};
function climbStairs(n) {
    if (n === 1) return 1;
    if (n === 2) return 2;
    if (memo[n]) return memo[n]; // 翻翻笔记,有就直接给
    memo[n] = climbStairs(n - 1) + climbStairs(n - 2);
    return memo[n];
}

3. 自底向上:动态规划(DP)

动态规划(Dynamic Programming)听起来很高大上,其实就是倒过来想。

我们不从 f(n)f(n) 往回找,而是从 f(1)f(1) 开始往后推:

  • f(1)=1f(1) = 1
  • f(2)=2f(2) = 2
  • f(3)=1+2=3f(3) = 1 + 2 = 3
  • f(4)=2+3=5f(4) = 2 + 3 = 5
function climbStairs(n) {
  if (n <= 2) return n;
  const dp = new Array(n + 1);
  dp[1] = 1;
  dp[2] = 2;
  for (let i = 3; i <= n; i++) {
    dp[i] = dp[i - 1] + dp[i - 2]; // 每一个结果都是前两个的和
  }
  return dp[n];
}

4. 极致优化:滚动变量

既然 f(n)f(n) 只依赖前两个数,那我们连数组都不需要了,只需要三个变量在手里“滚”起来。

function climbStairs(n) {
    if(n <= 2) return n;
    let prePrev = 1; // f(n-2)
    let prev = 2;    // f(n-1)
    let current;
    for(let i = 3; i <= n; i++){
        current = prev + prePrev;
        prePrev = prev;
        prev = current;
    }
    return current;
}

这时的空间复杂度降到了 O(1)O(1),几乎不占用额外内存。

总结:小白如何精进算法?

通过这两道题,你应该能发现算法学习的规律:

  1. 先画图,后写码: 不管是树状结构还是楼梯台阶,画出逻辑图比直接写代码重要得多。
  2. 寻找重复子问题: 递归和 DP 的核心都在于把大问题拆解成一样的小问题。
  3. 从暴力到优化: 别指望一步写出最优解。先用最笨的方法写出来,再去思考如何减少重复计算。

一个定时器,理清 JavaScript 里的 this

本文将从最基础的对象方法中this的指向说起,深入剖析定时器中this“不听话” 的原因,再逐一讲解几种常见的 “救回this” 的方法,包括经典的var that = this、灵活的call/apply、实用的bind,以及 ES6 中更优雅的箭头函数。通过清晰的案例对比和原理分析,帮你彻底理清this的绑定规律,从此不再被this的指向问题困扰。

一、从最普通的对象方法说起:this 指向当前对象

先从最正常的场景看起:一个对象,里面有个方法,方法里打印 

this 和 this.name

var obj = {
  name: 'Cherry',
  func1: function () {
    console.log(this);
    console.log(this.name);
  }
};
obj.func1();

image.png

在这种通过“对象.方法()”调用的场景下:

  • this 指向的是 obj 本身
  • this.name 就是 "Cherry"

也就是说,只要是“谁点出来的函数,

this 一般就指向谁”。

这一点很多人都懂,真正乱的是下面这种情况。

二、一进定时器,this 就不听话了

把上面的对象稍微改一下:再加一个方法,里面开个定时器。

var obj = {
  name: 'Cherry',
  func1: function () {
    console.log(this.name);
  },
  func2: function () {
    console.log(this);   // 这里的 this 还是 obj
    setTimeout(function () {
      console.log(this); // 这里的 this 是谁?
      this.func1();      // 这里很多人第一反应是“调用不到”
    }, 3000);
  }
};
obj.func2();

运行之后你会发现:

image.png

  • func2 里面第一行 console.log(this) 打印的是 obj
  • 但是定时器回调里的 console.log(this),却不再是 obj,而是全局对象(浏览器里是 window,严格模式下甚至可能是 undefined

原因是:谁调用这个函数,

this 就指向谁

  • obj.func2() 是“对象.方法调用”,所以 this === obj
  • setTimeout 回调是“普通函数调用”,真正执行时类似 window.callback(),所以 this 又回到了全局

于是 

this.func1() 就出现了典型错误:
你以为是调用 obj.func1,实际上是在全局环境下找 func1。

三、三种常见的“救回 this”姿势

为了在定时器里还能拿到“外层的那个对象”,常见有三种写法。

1. 老派写法:var that = this

最早接触到的方案一般是这个:

var obj = {
  name: 'Cherry',
  func1: function () {
    console.log(this.name);
  },
  func2: function () {
    var that = this;   // 先把外层的 this 存起来
    setTimeout(function () {
      console.log(this);      // 这里还是全局对象
      that.func1();           // 用 that 调用
    }, 3000);
  }
};
obj.func2();

image.png

思路很直白:

  • 外层 this 是我们想要的对象
  • 回调内部再用一个变量 that 把它“闭包”住
  • 不再依赖回调里的 this,而是用 that 去调用

优点:

  • 所有环境都支持,ES5 就可以用
    缺点:
  • 可读性一般,多层回调时会出现 that = this / self = this 满天飞

2. call / apply:立即执行并指定 this

第二种方案是利用 Function.prototype.call / apply,它们有两个关键点:

  • 都是立即调用函数
  • 第一个参数是要绑定的 this

例如:

function show() {
  console.log(this.name);
}
var obj = { name: 'Cherry' };
show();             // this => window / undefined
show.call(obj);     // this => obj
show.apply(obj);    // this => obj

call 和 apply 的区别只在于传参方式

  • call(fnThis, arg1, arg2, ...)
  • apply(fnThis, [arg1, arg2, ...])

在和定时器结合时,有一种稍微“绕”一点的写法,会先用 call 执行一次,然后把返回的函数交给定时器:

setTimeout(function () {
  console.log(this);  // 这里的 this 被 call 成 obj
  this.func1();
  
  return function () {
    console.log(this);  // 这个函数真正被 setTimeout 调用时,this 又回到全局
  };
}.call(obj), 2000);

分析一下这个写法的流程:

  • .call(obj) 先立刻执行这段函数,里面的 this 是 obj
  • 这个函数里 this.func1() 能正常调用到 obj.func1
  • 它 return 的那个内部函数才是真正交给 setTimeout 的
  • 这个内部函数在将来执行时,又是一次“普通函数调用”,于是 this 再次回到全局

这种写法属于“利用 call 硬拉一次 

this 过来”,但在实际项目里,更常见的做法不是这样用 call,而是第三种:bind

3. bind:先订婚,后结婚

bind 和 call/apply 很容易混:

  • call/apply马上执行,并临时指定一次 this
  • bind不执行,而是返回一个“this 永远被绑死”的新函数

用一个简单对比看差异:

var obj = {
  name: 'Cherry',
  func1: function () {
    console.log(this.name);
  }
};
console.log(obj.func1.bind(obj)); // 打印的是一个新函数
console.log(obj.func1.call(obj)); // 打印的是 func1 的返回值(这里是 undefined)
const f = obj.func1.bind(obj);
f(); // 始终以 obj 作为 this 调用

image.png

套用一个比较形象的说法:

  • call/apply闪婚,当场拍板,函数当场执行完事
  • bind先订婚,先约定好将来的 this,真正结婚(执行)是以后

因此在定时器这种“将来才会执行”的场景,bind 非常自然:

var obj = {
  name: 'Cherry',
  func1: function () {
    console.log(this.name);
  },
  func2: function () {
    setTimeout(this.func1.bind(this), 3000);
  }
};
obj.func2();
  • this.func1.bind(this) 立即返回了一个新函数
  • 这个新函数里 this 被固定成当前对象
  • setTimeout 三秒后再执行它时,this 依然是那个对象

相较于 that = this 和“花里胡哨的 call 写法”,bind 在这种场景下是最容易读懂的一种。

四、箭头函数:不再创建自己的 this

还有一种办法,是直接 **放弃回调自己的 **

this,而是用外层的。

这就是箭头函数的做法:箭头函数不会创建自己的执行上下文,它的 this 完全继承自外层作用域

箭头函数的核心是没有自己的 this,它的 this 是词法绑定(定义时继承外层作用域的 this),而非动态绑定。

把前面的定时器改成箭头函数版本:

var obj = {
  name: 'Cherry',
  func1: function () {
    console.log(this);       // obj
    console.log(this.name);  // Cherry
  },
  func2: function () {
    console.log(this);       // obj
    setTimeout(() => {
      console.log(this);     // 依然是 obj
      this.func1();          // 也能正常调用
    }, 3000);
  }
};
obj.func2();
obj.func1();

image.png

这里的关键点是:

  • func2 里的 this 是对象本身
  • 箭头函数的 this 直接沿用 func2 的 this
  • 所以在箭头函数里,this 没有发生“跳变”,始终是那个对象

再结合一个简单的对比例子,看得更清楚。

1. 普通函数和箭头函数的 this 对比

// 普通函数
function normal() {
  console.log(this);
}
// 箭头函数
const arrow = () => {
  console.log(this);
};
normal(); // 非严格模式下 this => window
arrow();  // this 继承自定义它时所在的作用域(全局里一般也是 window / undefined)

image.png

再注意一个常被问到的问题:

  • 箭头函数不能作为构造函数使用,也就是说不能 new 一个箭头函数
    在实际代码里,如果你尝试 new func()(func 是箭头函数),会直接报错

2. 顶层箭头函数的 this

在普通脚本里,如果写一个顶层箭头函数:

const func = () => {
  console.log(this);
};
func();

image.png

这里的 

this 继承自顶层作用域:

  • 浏览器非模块脚本中,一般是 window
  • 严格模式 / ES 模块中,顶层 this 往往是 undefined

这也再次说明:箭头函数的 

this 完全取决于它被定义时所处的环境,而不是被谁调用。

3. 继承外层作用域的 this

const obj = {
  name: 'Cherry',
  func: function () {          // 普通函数,this === obj
    console.log('外层 this:', this);
    setTimeout(() => {         // 箭头函数,this 继承外层
      console.log('箭头函数 this:', this);
      console.log('name:', this.name);
    }, 1000);
  }
};
obj.func();

image.png

把 setTimeout 里的箭头函数换成普通函数,this 会丢失

setTimeout 中的回调函数是被 JavaScript 引擎 “独立调用” 的,而非作为某个对象的方法调用

五、小结 & 使用建议

把上面的内容串一下,可以得到这样一份“速记表”:


  • this 的基础规律

    • 谁调用,指向谁obj.method() 里 this === obj
    • 普通函数直接调用:fn() 里 this 是全局对象 / undefined(严格模式)
  • setTimeout / 回调里的

    this

    • 回调是普通函数调用,this 默认指向全局
    • 所以在对象方法里直接写 setTimeout(function () { this.xxx }),往往拿不到我们想要的对象
  • 三种修复方式的对比

    • var that = this

      • 利用闭包保存外层 this
      • 兼容最好,但代码略显“老派”
    • call/apply

      • 立即执行函数
      • 第一个参数用来指定 this
      • 适合“当场就要执行一次”的场景
    • bind

      • 返回“this 被锁死”的新函数,而不是立即执行
      • 非常适合“定时器、事件监听、回调”这些“稍后再执行”的情形
  • 箭头函数

    • 不创建自己的 this,只继承外层
    • 在对象方法中配合回调使用,能有效避免 this 跳来跳去
    • 不适合作为构造函数(不能 new
❌