阅读视图

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

V8 与 JavaScript 执行:从字节码、Ignition 到 TurboFan JIT 的完整管线

一、整体管线概览

V8 执行 JavaScript 的大致流程是:源码解析(Parse) 得到 AST → 预解析(PreParse) 可延迟未用到的函数 → 全解析 + 字节码生成Ignition 解释执行字节码 → 热点TurboFan 编译成优化机器码;同时 内联缓存(IC) 在运行中收集类型反馈,指导 TurboFan 做类型特化与去虚化。理解这条管线,能解释「为什么某段代码先慢后快」「为什么类型稳定有利于优化」等常见现象。

二、解析与 AST

Parser 把 JS 源码转成 抽象语法树(AST)。V8 里对应 ParseProgram / ParseFunction 等。预解析(PreParse) 只做语法检查、不生成完整 AST 和字节码,用于顶层未立即执行的函数,减少启动时间;当该函数首次被调用时再 全解析(FullParse) 并生成字节码。这样大型应用不会因为「所有代码都先编译」而卡在启动阶段。

三、字节码与 Ignition

Ignition 是 V8 的解释器:它不直接执行 AST,而是先把 AST 编译成字节码(bytecode),再逐条执行字节码。字节码是介于 AST 和机器码之间的中间表示,指令紧凑、便于解释、也便于后续 TurboFan 做优化编译。每条字节码对应一个** handler**(一段机器码),解释执行就是「取指令 → 查 handler → 跳转执行」。字节码的生成由 BytecodeGenerator 完成,会遍历 AST 并发出 LdaNamedProperty、Add、Star 等指令;同时会为内联缓存(IC) 预留 slot,在首次执行时由 IC 记录类型反馈(如「该 load 来自哪个 shape」),供 TurboFan 使用。

四、内联缓存(Inline Cache, IC)

每次对对象属性的访问、二元运算等,在字节码执行时会走 IC:第一次执行时未命中,会走一段慢路径(查 Hidden Class、可能做类型推断)并更新 IC 状态;后续若类型与上次一致则命中,直接走快路径(如偏移量固定的内存访问)。若类型多变(多态/ megamorphic),IC 会退化为通用查找。因此 「类型稳定」(同一变量始终是同一 shape)能提高 IC 命中率,进而让 TurboFan 生成更优的机器码。

五、TurboFan 与优化编译

当某段字节码(通常是一个函数)被重复执行到一定次数或被标记为热点TurboFan 会将其编译成优化后的机器码。TurboFan 的输入是字节码 + 类型反馈(来自 IC),经过 Sea of Nodes 图、多种优化 pass(如内联、逃逸分析、类型窄化、去虚化),最后 Instruction Selector 生成目标架构的汇编。优化后的代码假设「类型与反馈一致」;若运行时违反(如本来一直是 number 的变量突然变成 object),会 deoptimize 回字节码解释执行,并可能丢弃该函数的优化版本。所以「避免类型变化」「避免在热路径上多态」能减少 deopt,提升稳定性能。

六、Hidden Class 与快速属性访问

V8 用 Hidden Class(Shape) 描述对象的「结构」:相同键、相同顺序的对象共享同一个 Hidden Class;新增/删除属性会触发 transition,形成 Hidden Class 链。属性在对象内的偏移存在 Hidden Class 里,所以一旦知道 Hidden Class,属性访问就是「基址 + 偏移」的一次内存读,无需查表。若代码里频繁创建「结构相同」的对象,能很好利用这一机制;若结构多变或动态增删属性,会导致 transition 链过长或属性退化为「字典模式」,访问变慢。

七、总结与性能建议

管线:Parse → Bytecode → Ignition 解释 + IC 收集反馈 → TurboFan 优化编译。写高性能 JS 时:保持类型稳定(同一变量少变 type)、热路径上少多态(避免同一调用点多种 shape)、避免在热函数里动态增删属性或改 prototype大对象/数组考虑 TypedArray 或固定结构。需要深入时可看 V8 的 --trace-opt / --trace-deopt--print-bytecode 输出,对照源码理解 Ignition 与 TurboFan 的边界。

八、延伸阅读

  • V8 博客:Ignition、TurboFan 的官方介绍与优化案例。
  • 源码:src/ignition/src/compiler/src/ic/ 等目录。
  • 《JavaScript 引擎进阶》等书对 AST、字节码、JIT 有更系统讲解。

九、实践:如何观察字节码与优化

在 Node 或 Chrome 中可通过 --print-bytecode(V8 标志)在控制台打印生成的字节码,便于对照 Ignition 指令理解执行流程。--trace-opt 会打印哪些函数被 TurboFan 优化、--trace-deopt 会打印哪些发生了 deoptimize 及原因(如 type feedback 不匹配)。在写高性能 JS 时,可先用 trace 看热点与 deopt,再针对性做类型稳定与结构稳定;避免在热路径上使用 evalwithdelete 等难以优化的结构。结合本文的 IC 与 TurboFan 管线,能更理性地做性能调优而非盲目「优化」。

React Fiber 调度器源码解析:从 workLoop 到 commit 的完整渲染链路

一、为什么要引入 Fiber

React 15 及之前, Reconciler 基于递归:从根组件一路向下调 mount/update,一旦开始就跑到底再提交 DOM。树大或组件重时,主线程长时间被占,导致输入卡顿、掉帧。Fiber 把「一棵组件树」拆成以** Fiber 节点为单位的可调度工作单元:每个 Fiber 对应一个组件或 DOM 节点, Reconciler 按 Fiber 逐个处理,并可在每个单元结束后让出主线程**,让高优先级更新(如用户输入)插队,从而实现可中断的并发渲染

二、Fiber 节点与双缓冲

每个 Fiber 上保存了:type(组件类型或 DOM 标签)、keyreturn / child / sibling(树形链表结构)、alternate(指向另一棵树上对应节点,用于双缓冲)、pendingProps / memoizedPropsmemoizedStateflags(增删改等副作用的标记)、lanes(优先级相关)等。双缓冲:当前屏对应一棵 current 树,正在计算的更新对应一棵 workInProgress 树;Reconciler 只改 workInProgress,算完后一次性 commit 把 workInProgress 换为 current,避免半成品 UI 暴露。

三、Scheduler 与优先级

React 的调度层 Scheduler 不依赖 React 自身:它维护一个按优先级排序的任务队列,在浏览器空闲时(或按时间片)执行任务,并可取消/暂停低优先级任务。React 把「一次更新」封装成 Scheduler 的 task,并赋予 laneexpirationTime 表示优先级;高优先级(如 input)会打断或抢占低优先级(如 list 渲染)。Scheduler 暴露 scheduleCallbackcancelCallbackshouldYield 等,Fiber 的 workLoop 里在每处理完一个单元后调用 shouldYield(),若需要让出则暂停并稍后继续。

四、workLoop 与 beginWork / completeWork

workLoopConcurrent(或 workLoopSync)从 root 开始,循环调用 performUnitOfWork:每次处理一个 Fiber。performUnitOfWork 内对当前 Fiber 调 beginWork(根据 tag 做 mount/update 的 diff、打 effect 标记、递归子节点),若没有子节点则 completeUnitOfWork,否则继续向下。completeWork 在「子节点都处理完」时执行:对 HostComponent 做 DOM 的增删改属性、对类组件做 ref 等;然后根据 sibling 找兄弟,没有则 return 到父节点再 complete。整棵 workInProgress 树就这样以深度优先、先子后兄再父的方式被遍历一遍,同时把 effectList(或依赖 flags 的 effect 链)串起来,供 commit 阶段消费。

五、commit 阶段:commitRoot

当 workLoop 把整棵 workInProgress 树算完,会调用 commitRoot。commit 阶段不可中断,分三个子阶段:commitBeforeMutationEffects(如 getSnapshotBeforeUpdate)、commitMutationEffects(对 DOM 增删改、执行 useLayoutEffect 的 destroy)、commitLayoutEffects(执行 useLayoutEffect 的 create、ref 回调、componentDidMount/Update)。之后把 root.current 指向 workInProgress,完成双缓冲切换;再在下一帧或微任务里触发 useEffect 的调度(异步)。这样保证用户看到的始终是「一整帧完整更新」,而不会看到半成品。

六、与 React 18 并发特性的关系

useTransitionuseDeferredValueSuspense 都建立在这套 Fiber + Scheduler 之上:过渡更新被标记为低优先级,可被高优先级打断;Suspense 的「挂起」会中断当前子树渲染并显示 fallback,等 Promise resolve 后再重新调度。理解 workLoop 的「可让出」和 commit 的「一次性提交」,就能更好理解并发模式下的行为与边界。

七、总结与阅读建议

Fiber = 可调度的工作单元 + 双缓冲 + 优先级;Scheduler 负责「何时跑」、Reconciler 负责「怎么 diff」、commit 负责「怎么落 DOM」。读源码时建议从 performSyncWorkOnRoot / performConcurrentWorkOnRoot 入口跟到 workLoop → beginWork/completeWork,再跟 commitRoot 三子阶段;配合 React DevTools 的 Profiler 与「Highlight updates」观察优先级与打断效果,印象会更深。

八、关键数据结构与源码路径(React 18)

  • Fiberpackages/react-reconciler/src/ReactFiber.old.js 中的 Fiber 类型定义;FiberRootReactFiberRoot.old.js
  • Schedulerpackages/scheduler/src/Scheduler.js(任务循环)、SchedulerPriorities.js(优先级常量)。
  • workLooppackages/react-reconciler/src/ReactFiberWorkLoop.old.jsperformConcurrentWorkOnRoot / performSyncWorkOnRootworkLoopConcurrentperformUnitOfWork
  • beginWork / completeWorkReactFiberBeginWork.old.jsReactFiberCompleteWork.old.js
  • commitReactFiberCommitWork.old.jscommitRoot 内对 mutation/layout 的遍历。

打开 React 仓库按上述路径跳转,再结合打断点单步,能快速对应到本文描述的流程。

九、常见问题

  • 为什么我的 useEffect 执行了两次? 在 React 18 Strict Mode 下会故意双调用于发现副作用问题;生产构建不会。
  • useTransition 没感觉变快? 它不减少计算量,只是把更新标记为可打断,避免阻塞输入;若本身没有重计算,体感差异不大。
  • commit 阶段为什么不能打断? 一旦改 DOM 就要原子完成,否则会出现半帧状态;只有「算 Fiber」阶段可让出。

十、调试与性能分析建议

在 Chrome DevTools 的 Performance 里录制一次交互,可看到主线程上 Recalc StyleLayoutJS 的占比;若 React 更新占大头,可再用 React DevTools Profiler 看是哪些组件 render 多、commit 耗时高。Scheduler 的 task 可在 Performance 的 JS 调用栈里看到 workLoopConcurrentperformUnitOfWork 等;结合 Scheduling Profiler(实验性)可观察任务优先级与打断。源码阅读时建议从 createRootupdateContainer 跟到 scheduleUpdateOnFiber,再跟到 ensureRootIsScheduled 与 workLoop,这样能把「一次 setState 如何驱动整条链路」串起来,对理解并发与优先级大有帮助。

现代前端构建:从 AST、依赖图到产物分块的完整管线解析

一、构建管线在解决什么问题

前端工程里,源码往往以多模块、多格式(TS、JSX、Vue、CSS)存在,且存在依赖关系;浏览器无法直接跑 TS、无法按「裸模块」请求 node_modules。构建管线要完成:解析(把各格式转成 AST)、转换(AST 变换、降级、CSS 处理)、依赖分析(从入口建模块图)、打包/分块(合并或按策略拆 chunk)、代码生成(AST → 目标代码 + sourcemap)。理解这条管线,能更好配置 Webpack/Vite/Rollup、写 Babel/PostCSS 插件、排构建慢与产物异常。

二、解析阶段:从源码到 AST

解析器 把源码变成 AST(抽象语法树)。不同语言用不同解析器:JS/TS 常用 acorn@babel/parserswc;CSS 用 postcss 的解析;Vue SFC 先拆成 script/style/template 再分别解析。解析结果是一棵带节点类型的树,后续 转换生成 都基于 AST,不直接操作字符串。解析阶段会报语法错误、可产出 location 信息供 sourcemap 与报错定位。

三、转换与 Loader / 插件

转换 在 AST 上做增删改:Babel 做语法降级、JSX 转 JS、TS 擦除类型;PostCSS 做 autoprefixer、嵌套;Vue/React 的 loader 可能做 SFC 拆块或 JSX 编译。在 Webpack 里,Loader 是「单文件进单文件出」的转换管道;在 Rollup/Vite 里,插件transform 钩子对模块内容做转换。共同点是:输入是源码或 AST,输出是下一阶段可消费的代码(或 AST)。转换顺序通常由配置顺序决定,可串联多个 loader/插件。

四、模块图与依赖分析

入口(如 main.js)开始,根据 import/require 静态分析依赖,递归解析每个模块,得到一张模块图(Module Graph):节点是模块,边是依赖关系。图中会记录模块的绝对路径、类型(JS/CSS)、依赖列表、解析后的内容。动态 import(import())会生成异步边界:依赖的模块单独成 chunk,在运行时再加载。模块图是后续 Tree-shaking分块代码生成 的基础;图错了(如循环依赖未处理、动态路径未展开)会导致打包结果错误或冗余。

五、Tree-shaking 与 Dead Code Elimination

Tree-shaking 指利用 ESM 的静态结构,在模块图上做可达性分析:从入口出发,只保留「被引用」的 export;未被引用的 export 及其内部未使用代码可视为 dead code 并在生成时去掉。前提是:模块必须是 ESM(CommonJS 的 require 是动态的,难以静态分析);无副作用或通过 package.json 的 sideEffects 声明。Rollup 是 Tree-shaking 的典型实现者;Webpack 在 production 模式下也会做类似优化。写库时注意 export 粒度sideEffects 配置,能减少业务方打包体积。

六、分块(Chunk)策略与代码分割

分块 决定哪些模块打进同一个 bundle、哪些单独成 chunk。常见策略:按入口(多页应用每个入口一个 chunk)、按动态 import(每个 import() 边界一个 async chunk)、按 vendor(把 node_modules 打成单独 chunk 利于缓存)、manualChunks(手动指定某类模块进某 chunk)。分块会影响请求数缓存命中首屏体积;过细会请求多,过粗会单包过大。需要结合 preload/prefetch懒加载时机 做权衡。

七、代码生成与运行时

代码生成模块图 + 分块结果转成最终的可执行代码:每个 chunk 对应一个运行时(如 Webpack 的 runtime:模块 id 与 chunk 加载、模块缓存)加模块内容(可能被包装成函数、按 id 注册)。要保证:依赖顺序正确(被依赖的模块先执行)、全局变量/IIFE 不冲突sourcemap 正确映射回源码。产物格式可以是 IIFEESMCJS,由配置的 output.format 等决定。

八、总结与工具对照

整条管线:解析 → 转换(Loader/插件)→ 模块图 → Tree-shaking → 分块 → 代码生成。Webpack 的 loader 链对应「解析+转换」、Module Graph 对应模块图、SplitChunks 对应分块;Vite 开发时跳过打包、按需编译,生产用 Rollup 走完整管线。写插件或排错时,抓住「当前处在哪一阶段、输入输出是什么」,就能快速定位问题。

九、延伸阅读

  • Webpack 文档:Concepts、Module Graph、Code Splitting。
  • Rollup 文档:Plugin API、output options。
  • Babel 插件手册:AST 节点类型与 visit 写法。

十、实践:写一个简单的 Babel 转换插件

理解 AST 后,可以写一个最小 Babel 插件:用 @babel/parser 解析得到 AST,用 @babel/traversevisitor 遍历并修改节点(如把某个函数名全部替换),再用 @babel/generator 生成代码。插件形式是导出一个函数,返回带 visitor 的对象;在 Babel 配置的 plugins 里引用即可。这样能直观感受「解析 → 转换 → 生成」的闭环,并推广到 PostCSS、Rollup 的 transform 钩子:本质都是在 AST 或中间表示上做变换,构建管线只是把这些步骤串起来并加上模块图与分块。动手写一个小插件后,再回头看 Webpack/Vite 的文档,会更容易抓住重点。

CSS 滚动驱动动画(scroll-timeline):无 JS 实现滚动特效

一、传统方案的痛点

以前实现滚动动画需要 JavaScript 监听 scroll 事件:

window.addEventListener('scroll', () => {
  const scrollTop = window.scrollY;
  const progress = scrollTop / (document.body.scrollHeight - window.innerHeight);
  
  // 更新进度条
  progressBar.style.width = `${progress * 100}%`;
  
  // 视差效果
  parallaxElement.style.transform = `translateY(${scrollTop * 0.5}px)`;
});

问题:

  • 性能开销大(频繁触发)
  • 需要手动计算
  • 代码复杂

二、scroll-timeline 的解决方案

CSS 滚动驱动动画让元素的动画进度与滚动位置绑定,无需 JavaScript。

基础语法

@keyframes fade-in {
  from { opacity: 0; transform: translateY(50px); }
  to { opacity: 1; transform: translateY(0); }
}

.element {
  animation: fade-in linear;
  animation-timeline: scroll();  /* 绑定到滚动 */
}

三、scroll() 函数

scroll() 创建一个滚动时间线,将动画进度与滚动位置关联。

语法

animation-timeline: scroll(<scroller> <axis>);

参数:

  • scroller: 滚动容器(nearest | root | self)
  • axis: 滚动方向(block | inline | y | x)
/* 最近的滚动祖先,垂直方向 */
animation-timeline: scroll(nearest block);

/* 根滚动容器,水平方向 */
animation-timeline: scroll(root inline);

/* 元素自身,垂直方向 */
animation-timeline: scroll(self y);

四、实战案例

案例 1:滚动进度条

<div class="progress-bar"></div>
.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  height: 4px;
  background: linear-gradient(to right, #4caf50, #2196f3);
  transform-origin: left;
  
  animation: grow-progress linear;
  animation-timeline: scroll(root block);
}

@keyframes grow-progress {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

效果:进度条宽度随页面滚动增长。

案例 2:滚动淡入

.fade-in-section {
  opacity: 0;
  transform: translateY(50px);
  
  animation: fade-in linear;
  animation-timeline: view();  /* 元素进入视口时触发 */
}

@keyframes fade-in {
  from {
    opacity: 0;
    transform: translateY(50px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

案例 3:视差滚动

.parallax-bg {
  position: fixed;
  top: 0;
  width: 100%;
  height: 100vh;
  z-index: -1;
  
  animation: parallax linear;
  animation-timeline: scroll(root block);
}

@keyframes parallax {
  to {
    transform: translateY(50%);
  }
}

案例 4:图片缩放

.hero-image {
  width: 100%;
  height: 100vh;
  object-fit: cover;
  
  animation: zoom-out linear;
  animation-timeline: scroll(root block);
}

@keyframes zoom-out {
  from {
    transform: scale(1.2);
  }
  to {
    transform: scale(1);
  }
}

五、view() 函数

view() 创建一个视图时间线,当元素进入/离开视口时触发动画。

语法

animation-timeline: view(<axis> <inset>);
/* 元素进入视口时触发 */
.element {
  animation: fade-in linear;
  animation-timeline: view();
}

/* 设置触发范围 */
.element {
  animation: fade-in linear;
  animation-timeline: view(block 20% 20%);
  /* 在视口上下 20% 的范围内触发 */
}

六、animation-range

控制动画在滚动范围内的哪个阶段执行。

.element {
  animation: fade-in linear;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
  /* 只在进入阶段执行动画 */
}

范围关键字:

  • entry: 元素进入视口
  • exit: 元素离开视口
  • contain: 元素完全在视口内
  • cover: 整个过程
/* 进入时淡入 */
.fade-in {
  animation: fade linear;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

/* 离开时淡出 */
.fade-out {
  animation: fade linear reverse;
  animation-timeline: view();
  animation-range: exit 0% exit 100%;
}

七、组合使用

多个动画阶段

.card {
  animation: 
    slide-in linear,
    rotate linear;
  animation-timeline: view();
  animation-range:
    entry 0% entry 50%,
    entry 50% entry 100%;
}

@keyframes slide-in {
  from { transform: translateX(-100%); }
  to { transform: translateX(0); }
}

@keyframes rotate {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

视差分层

.layer-1 {
  animation: parallax-slow linear;
  animation-timeline: scroll();
}

.layer-2 {
  animation: parallax-medium linear;
  animation-timeline: scroll();
}

.layer-3 {
  animation: parallax-fast linear;
  animation-timeline: scroll();
}

@keyframes parallax-slow {
  to { transform: translateY(20%); }
}

@keyframes parallax-medium {
  to { transform: translateY(40%); }
}

@keyframes parallax-fast {
  to { transform: translateY(60%); }
}

八、浏览器支持

滚动驱动动画在现代浏览器中支持:

  • Chrome 115+
  • Edge 115+
  • Safari 尚未支持
  • Firefox 尚未支持

特性检测

@supports (animation-timeline: scroll()) {
  /* 支持滚动动画 */
  .element {
    animation: fade-in linear;
    animation-timeline: scroll();
  }
}

@supports not (animation-timeline: scroll()) {
  /* 降级方案:使用 JavaScript */
  .element {
    opacity: 1;
  }
}

Polyfill

<script src="https://flackr.github.io/scroll-timeline/dist/scroll-timeline.js"></script>

九、性能优化

1. 使用 transform 和 opacity

/* ✅ 性能好:只触发合成 */
@keyframes good {
  from {
    opacity: 0;
    transform: translateY(50px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* ❌ 性能差:触发重排 */
@keyframes bad {
  from {
    top: 50px;
    width: 100px;
  }
  to {
    top: 0;
    width: 200px;
  }
}

2. 使用 will-change

.animated-element {
  will-change: transform, opacity;
  animation: fade-in linear;
  animation-timeline: scroll();
}

3. 限制动画元素数量

/* 只对可见区域的元素应用动画 */
.element {
  animation: fade-in linear;
  animation-timeline: view();
  animation-range: entry 0% cover 100%;
}

十、实用技巧

1. 数字滚动计数

@property --num {
  syntax: '<integer>';
  initial-value: 0;
  inherits: false;
}

.counter {
  counter-reset: num var(--num);
  animation: count linear;
  animation-timeline: view();
}

.counter::after {
  content: counter(num);
}

@keyframes count {
  from { --num: 0; }
  to { --num: 100; }
}

2. 文字逐字显示

.text {
  animation: reveal linear;
  animation-timeline: view();
}

@keyframes reveal {
  from {
    clip-path: inset(0 100% 0 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}

3. 图片模糊到清晰

.image {
  filter: blur(10px);
  animation: unblur linear;
  animation-timeline: view();
}

@keyframes unblur {
  to {
    filter: blur(0);
  }
}

十一、与 JavaScript 对比

特性 CSS scroll-timeline JavaScript
性能 ✅ 更好(GPU 加速) ⚠️ 依赖实现
代码量 ✅ 少 ❌ 多
灵活性 ⚠️ 有限 ✅ 强大
浏览器支持 ⚠️ 较新 ✅ 广泛
复杂逻辑 ❌ 不支持 ✅ 支持

ahooks useMemoizedFn:解决 useCallback 的依赖地狱

一、useCallback 的痛点

在 React 中,我们经常用 useCallback 来缓存函数,避免子组件不必要的重渲染。

function Parent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  const handleClick = useCallback(() => {
    console.log(count);
  }, [count]);  // 依赖 count

  return <Child onClick={handleClick} />;
}

问题:每次 count 变化,handleClick 都会重新创建,Child 还是会重渲染。


二、useMemoizedFn 的解决方案

useMemoizedFn 返回的函数引用永远不变,但内部总是能访问到最新的 state 和 props。

import { useMemoizedFn } from 'ahooks';

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useMemoizedFn(() => {
    console.log(count);  // 总是最新的 count
  });

  return <Child onClick={handleClick} />;
}

无论 count 如何变化,handleClick 的引用都不变,Child 不会重渲染。


三、与 useCallback 对比

// useCallback:依赖变化时函数重新创建
const fn1 = useCallback(() => {
  doSomething(a, b, c);
}, [a, b, c]);  // 依赖地狱

// useMemoizedFn:引用永远不变
const fn2 = useMemoizedFn(() => {
  doSomething(a, b, c);  // 无需依赖数组
});
特性 useCallback useMemoizedFn
依赖数组 必须 不需要
引用稳定性 依赖变化时改变 永远不变
访问最新值 需要加入依赖 自动访问
使用复杂度

四、实战场景

场景 1:表单提交

function Form() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [loading, setLoading] = useState(false);

  // ❌ useCallback:依赖太多
  const handleSubmit = useCallback(async () => {
    setLoading(true);
    await submitForm({ name, email });
    setLoading(false);
  }, [name, email]);  // 每次输入都会重新创建

  // ✅ useMemoizedFn:无需依赖
  const handleSubmit = useMemoizedFn(async () => {
    setLoading(true);
    await submitForm({ name, email });
    setLoading(false);
  });

  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={e => setName(e.target.value)} />
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <button type="submit">提交</button>
    </form>
  );
}

场景 2:事件监听

function ScrollTracker() {
  const [scrollTop, setScrollTop] = useState(0);

  // ❌ useCallback:每次 scrollTop 变化都重新绑定
  const handleScroll = useCallback(() => {
    console.log('当前位置:', scrollTop);
  }, [scrollTop]);

  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [handleScroll]);  // handleScroll 变化导致重新绑定

  // ✅ useMemoizedFn:只绑定一次
  const handleScroll = useMemoizedFn(() => {
    console.log('当前位置:', scrollTop);
  });

  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);  // 空依赖,只绑定一次
}

场景 3:传递给子组件

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all');

  // ✅ 引用稳定,子组件不会重渲染
  const handleDelete = useMemoizedFn((id) => {
    setTodos(todos.filter(t => t.id !== id));
  });

  const handleToggle = useMemoizedFn((id) => {
    setTodos(todos.map(t => 
      t.id === id ? { ...t, done: !t.done } : t
    ));
  });

  return (
    <>
      <Filter value={filter} onChange={setFilter} />
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          {...todo}
          onDelete={handleDelete}  // 引用不变
          onToggle={handleToggle}  // 引用不变
        />
      ))}
    </>
  );
}

// 子组件用 memo 包裹,只在 props 变化时重渲染
const TodoItem = memo(({ id, text, done, onDelete, onToggle }) => {
  console.log('TodoItem render:', id);
  return (
    <div>
      <input
        type="checkbox"
        checked={done}
        onChange={() => onToggle(id)}
      />
      <span>{text}</span>
      <button onClick={() => onDelete(id)}>删除</button>
    </div>
  );
});

五、原理解析

useMemoizedFn 的核心思路:用 ref 存储最新的函数,返回一个永远不变的包装函数。

function useMemoizedFn(fn) {
  const fnRef = useRef(fn);
  
  // 每次渲染都更新 ref
  fnRef.current = fn;
  
  // 返回的函数引用永远不变
  const memoizedFn = useRef((...args) => {
    return fnRef.current(...args);
  });
  
  return memoizedFn.current;
}

关键点:

  1. fnRef 存储最新的函数
  2. memoizedFn 是包装函数,引用不变
  3. 调用时通过 fnRef.current 访问最新函数

六、注意事项

1. 不要在循环中使用

// ❌ 错误:每次循环都创建新的 Hook
todos.map(todo => {
  const handleClick = useMemoizedFn(() => {
    deleteTodo(todo.id);
  });
  return <button onClick={handleClick}>删除</button>;
});

// ✅ 正确:在组件顶层创建
const handleDelete = useMemoizedFn((id) => {
  deleteTodo(id);
});

todos.map(todo => (
  <button onClick={() => handleDelete(todo.id)}>删除</button>
));

2. 异步函数也适用

const fetchData = useMemoizedFn(async () => {
  const data = await api.getData();
  setData(data);
});

useEffect(() => {
  fetchData();
}, []);  // 空依赖,fetchData 引用不变

3. 配合 useEffect

// ❌ useCallback:依赖变化导致 effect 重新执行
const fetchUser = useCallback(() => {
  return api.getUser(userId);
}, [userId]);

useEffect(() => {
  fetchUser().then(setUser);
}, [fetchUser]);  // fetchUser 变化导致重新请求

// ✅ useMemoizedFn:只在 userId 变化时请求
const fetchUser = useMemoizedFn(() => {
  return api.getUser(userId);
});

useEffect(() => {
  fetchUser().then(setUser);
}, [userId]);  // 只依赖 userId

七、性能测试

function PerformanceTest() {
  const [count, setCount] = useState(0);
  const renderCount = useRef(0);

  // useCallback 版本
  const handleClick1 = useCallback(() => {
    console.log(count);
  }, [count]);

  // useMemoizedFn 版本
  const handleClick2 = useMemoizedFn(() => {
    console.log(count);
  });

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>
        Count: {count}
      </button>
      
      {/* 每次 count 变化,Child1 都会重渲染 */}
      <Child1 onClick={handleClick1} />
      
      {/* Child2 永远不会重渲染 */}
      <Child2 onClick={handleClick2} />
    </>
  );
}

const Child1 = memo(({ onClick }) => {
  console.log('Child1 render');
  return <button onClick={onClick}>Click</button>;
});

const Child2 = memo(({ onClick }) => {
  console.log('Child2 render');
  return <button onClick={onClick}>Click</button>;
});

结果:

  • Child1:每次 count 变化都重渲染
  • Child2:只渲染一次

八、何时使用

适合使用 useMemoizedFn

  1. 函数需要传递给子组件
  2. 函数作为 useEffect 的依赖
  3. 函数需要绑定到 DOM 事件
  4. 函数依赖很多 state/props

不需要使用

  1. 函数不传递给子组件
  2. 函数内部没有闭包变量
  3. 性能不是瓶颈
// 不需要:函数不传递给子组件
const handleClick = () => {
  console.log('clicked');
};

// 需要:传递给子组件
const handleClick = useMemoizedFn(() => {
  console.log('clicked');
});
return <Child onClick={handleClick} />;

九、与其他方案对比

vs useCallback

// useCallback:需要维护依赖
const fn = useCallback(() => {
  doSomething(a, b, c);
}, [a, b, c]);

// useMemoizedFn:无需依赖
const fn = useMemoizedFn(() => {
  doSomething(a, b, c);
});

vs useEvent (React RFC)

React 团队提出的 useEvent 与 useMemoizedFn 思路类似,但还未正式发布。

// React useEvent (未来)
const handleClick = useEvent(() => {
  console.log(count);
});

// ahooks useMemoizedFn (现在可用)
const handleClick = useMemoizedFn(() => {
  console.log(count);
});

十、最佳实践

  1. 优先使用 useMemoizedFn:在需要缓存函数时,优先考虑 useMemoizedFn

  2. 配合 memo 使用:子组件用 memo 包裹,才能体现性能优势

const Child = memo(({ onClick }) => {
  return <button onClick={onClick}>Click</button>;
});
  1. 不要过度优化:如果组件渲染本身很快,不需要优化

  2. 统一团队规范:在团队中统一使用 useMemoizedFn 或 useCallback

React Hooks 避坑指南:那些让你 debug 到凌晨的陷阱

一、凌晨三点的 Bug

上周五晚上十点,准备下班。突然测试同学发来消息:「你这个计数器有问题,点了半天还是 0。」

我心想,一个计数器能有什么问题?打开代码一看:

function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setTimeout(() => {
      setCount(count + 1);
    }, 3000);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>+1 (延迟 3 秒)</button>
    </div>
  );
}

看起来没问题啊。但实际运行时,快速点击 5 次按钮,3 秒后 count 只变成 1,而不是 5。

这就是 Hooks 的第一个大坑:闭包陷阱

那天晚上我 debug 到凌晨三点,才把所有 Hooks 的坑都踩了一遍。今天就来聊聊这些让人头秃的陷阱,以及怎么避开它们。

二、闭包陷阱:useState 的「时光机」

2.1 问题重现

function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setTimeout(() => {
      setCount(count + 1);  // 这里的 count 是点击时的值
    }, 3000);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>+1 (延迟 3 秒)</button>
    </div>
  );
}

现象

  1. 快速点击 5 次按钮
  2. 3 秒后,count 只变成 1(而不是 5)

原因

每次点击时,handleClick 函数都会捕获当时的 count 值(闭包)。5 次点击时 count 都是 0,所以 5 个 setTimeout 都执行 setCount(0 + 1),最终结果是 1。

2.2 解决方案

方案 1:函数式更新

const handleClick = () => {
  setTimeout(() => {
    setCount(prevCount => prevCount + 1);  // 使用最新的 count
  }, 3000);
};

方案 2:使用 useRef

function Counter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count;
  }, [count]);

  const handleClick = () => {
    setTimeout(() => {
      setCount(countRef.current + 1);
    }, 3000);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>+1 (延迟 3 秒)</button>
    </div>
  );
}

方案 3:使用 useReducer

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  const handleClick = () => {
    setTimeout(() => {
      dispatch({ type: 'increment' });  // 不依赖闭包
    }, 3000);
  };

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={handleClick}>+1 (延迟 3 秒)</button>
    </div>
  );
}

2.3 更隐蔽的闭包陷阱

function SearchBox() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  const handleSearch = () => {
    fetchResults(query).then(data => {
      setResults(data);
    });
  };

  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Current query:', query);  // 永远打印初始值
    }, 1000);

    return () => clearInterval(timer);
  }, []);  // 空依赖数组导致闭包

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <button onClick={handleSearch}>Search</button>
    </div>
  );
}

问题setInterval 里的 query 永远是初始值(空字符串)。

原因useEffect 的依赖数组是空的,effect 只执行一次,闭包捕获的是初始的 query

解决

useEffect(() => {
  const timer = setInterval(() => {
    console.log('Current query:', query);
  }, 1000);

  return () => clearInterval(timer);
}, [query]);  // 添加 query 依赖

或者用 useRef

const queryRef = useRef(query);

useEffect(() => {
  queryRef.current = query;
}, [query]);

useEffect(() => {
  const timer = setInterval(() => {
    console.log('Current query:', queryRef.current);
  }, 1000);

  return () => clearInterval(timer);
}, []);

三、useEffect 的依赖地狱

3.1 缺失依赖导致的 Bug

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, []);  // 缺少 userId 依赖

  return <div>{user?.name}</div>;
}

问题userId 变化时,不会重新获取用户数据。

解决

useEffect(() => {
  fetchUser(userId).then(setUser);
}, [userId]);  // 添加 userId 依赖

3.2 对象/数组依赖导致的无限循环

function DataList() {
  const [data, setData] = useState([]);
  const filters = { status: 'active', type: 'user' };  // 每次渲染都是新对象

  useEffect(() => {
    fetchData(filters).then(setData);
  }, [filters]);  // filters 每次都变,导致无限循环

  return <ul>{data.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}

问题filters 每次渲染都是新对象,导致 effect 无限执行。

解决方案 1:提取到组件外

const DEFAULT_FILTERS = { status: 'active', type: 'user' };

function DataList() {
  const [data, setData] = useState([]);

  useEffect(() => {
    fetchData(DEFAULT_FILTERS).then(setData);
  }, []);  // 依赖空数组

  return <ul>{data.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}

解决方案 2:useMemo

function DataList() {
  const [data, setData] = useState([]);
  const filters = useMemo(() => ({ status: 'active', type: 'user' }), []);

  useEffect(() => {
    fetchData(filters).then(setData);
  }, [filters]);

  return <ul>{data.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}

解决方案 3:依赖具体的值

function DataList() {
  const [data, setData] = useState([]);
  const status = 'active';
  const type = 'user';

  useEffect(() => {
    fetchData({ status, type }).then(setData);
  }, [status, type]);  // 依赖原始值,不依赖对象

  return <ul>{data.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}

3.3 函数依赖导致的问题

function SearchBox() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  const handleSearch = () => {
    fetchResults(query).then(setResults);
  };

  useEffect(() => {
    handleSearch();
  }, [handleSearch]);  // handleSearch 每次渲染都是新函数,导致无限循环

  return (
    <input value={query} onChange={e => setQuery(e.target.value)} />
  );
}

解决方案 1:useCallback

const handleSearch = useCallback(() => {
  fetchResults(query).then(setResults);
}, [query]);

useEffect(() => {
  handleSearch();
}, [handleSearch]);

解决方案 2:直接在 effect 中调用

useEffect(() => {
  fetchResults(query).then(setResults);
}, [query]);  // 不依赖函数,直接依赖 query

解决方案 3:useEffectEvent(React 19)

import { useEffectEvent } from 'react';

function SearchBox() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  const handleSearch = useEffectEvent(() => {
    fetchResults(query).then(setResults);
  });

  useEffect(() => {
    handleSearch();  // 不需要添加到依赖数组
  }, []);

  return (
    <input value={query} onChange={e => setQuery(e.target.value)} />
  );
}

四、useCallback 和 useMemo 的误用

4.1 过度使用导致的性能下降

function TodoList({ todos }) {
  // ❌ 过度优化:简单组件不需要 memo
  const renderItem = useCallback((todo) => {
    return <li key={todo.id}>{todo.title}</li>;
  }, []);

  // ❌ 过度优化:简单计算不需要 useMemo
  const count = useMemo(() => todos.length, [todos]);

  // ❌ 过度优化:每个函数都 useCallback
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  return (
    <div>
      <p>Total: {count}</p>
      <ul>{todos.map(renderItem)}</ul>
      <button onClick={handleClick}>Log</button>
    </div>
  );
}

问题

  • useCallbackuseMemo 本身有成本(创建闭包、比较依赖)
  • 简单计算的成本可能低于 useMemo 的成本
  • 过度使用反而降低性能

原则

  • 只在真正需要时使用(子组件用了 React.memo、计算成本高)
  • 先测量,再优化
  • 简单组件不需要优化

正确示例

function TodoList({ todos }) {
  // ✅ 简单计算,不需要 useMemo
  const count = todos.length;

  // ✅ 子组件没用 memo,不需要 useCallback
  const handleClick = () => {
    console.log('clicked');
  };

  return (
    <div>
      <p>Total: {count}</p>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
      <button onClick={handleClick}>Log</button>
    </div>
  );
}

4.2 useCallback 的依赖陷阱

function SearchBox() {
  const [query, setQuery] = useState('');
  const [filters, setFilters] = useState({ status: 'all' });

  // ❌ 缺少 filters 依赖
  const handleSearch = useCallback(() => {
    fetchResults(query, filters).then(setResults);
  }, [query]);  // filters 变化时不会更新

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <button onClick={handleSearch}>Search</button>
    </div>
  );
}

解决

const handleSearch = useCallback(() => {
  fetchResults(query, filters).then(setResults);
}, [query, filters]);  // 添加所有依赖

但这又导致新问题:filters 是对象,每次渲染都变,useCallback 失效。

更好的解决方案

function SearchBox() {
  const [query, setQuery] = useState('');
  const [status, setStatus] = useState('all');  // 用原始值代替对象

  const handleSearch = useCallback(() => {
    fetchResults(query, { status }).then(setResults);
  }, [query, status]);  // 依赖原始值

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <select value={status} onChange={e => setStatus(e.target.value)}>
        <option value="all">All</option>
        <option value="active">Active</option>
      </select>
      <button onClick={handleSearch}>Search</button>
    </div>
  );
}

4.3 useMemo 的计算时机

function ExpensiveComponent({ data }) {
  console.log('Component rendered');

  const result = useMemo(() => {
    console.log('Computing result');
    return data.map(item => item.value * 2);
  }, [data]);

  return <div>{result.join(', ')}</div>;
}

误解:很多人以为 useMemo 会阻止组件重渲染。

真相

  • useMemo 只缓存计算结果,不阻止组件渲染
  • 组件每次渲染都会执行,只是跳过 useMemo 内部的计算
  • 要阻止组件渲染,需要用 React.memo

正确理解

// 组件每次都渲染,但计算只在 data 变化时执行
function ExpensiveComponent({ data }) {
  console.log('Component rendered');  // 每次都打印

  const result = useMemo(() => {
    console.log('Computing result');  // 只在 data 变化时打印
    return data.map(item => item.value * 2);
  }, [data]);

  return <div>{result.join(', ')}</div>;
}

// 要阻止组件渲染,需要 React.memo
const ExpensiveComponent = React.memo(function ExpensiveComponent({ data }) {
  console.log('Component rendered');  // 只在 data 变化时打印

  const result = useMemo(() => {
    console.log('Computing result');
    return data.map(item => item.value * 2);
  }, [data]);

  return <div>{result.join(', ')}</div>;
});

五、useRef 的常见误区

5.1 useRef 不会触发重渲染

function Counter() {
  const countRef = useRef(0);

  const handleClick = () => {
    countRef.current += 1;
    console.log('Count:', countRef.current);  // 打印正确的值
  };

  return (
    <div>
      <p>Count: {countRef.current}</p>  {/* 页面不更新 */}
      <button onClick={handleClick}>+1</button>
    </div>
  );
}

问题countRef.current 变化不会触发重渲染,页面显示的值不变。

原因useRef 返回的是一个可变对象,修改 .current 不会触发重渲染。

何时使用 useRef

  • 存储不需要触发渲染的值(定时器 ID、DOM 引用、上一次的值)
  • 在多次渲染间保持引用稳定

何时使用 useState

  • 存储需要触发渲染的值

5.2 useRef 的初始化陷阱

function VideoPlayer({ src }) {
  const videoRef = useRef(null);

  useEffect(() => {
    // ❌ 错误:videoRef.current 可能还是 null
    videoRef.current.play();
  }, []);

  return <video ref={videoRef} src={src} />;
}

问题useEffect 执行时,videoRef.current 可能还没有赋值。

解决

function VideoPlayer({ src }) {
  const videoRef = useRef(null);

  useEffect(() => {
    // ✅ 检查是否已赋值
    if (videoRef.current) {
      videoRef.current.play();
    }
  }, []);

  return <video ref={videoRef} src={src} />;
}

或者用回调 ref:

function VideoPlayer({ src }) {
  const handleRef = useCallback((node) => {
    if (node) {
      node.play();
    }
  }, []);

  return <video ref={handleRef} src={src} />;
}

5.3 useRef 存储上一次的值

这是 useRef 的经典用法:

function usePrevious(value) {
  const ref = useRef();

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
}

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

六、自定义 Hook 的陷阱

6.1 忘记返回清理函数

// ❌ 错误:没有清理定时器
function useInterval(callback, delay) {
  useEffect(() => {
    const timer = setInterval(callback, delay);
    // 忘记返回清理函数
  }, [callback, delay]);
}

问题:组件卸载时定时器没有清除,导致内存泄漏。

解决

function useInterval(callback, delay) {
  useEffect(() => {
    const timer = setInterval(callback, delay);
    return () => clearInterval(timer);  // 清理函数
  }, [callback, delay]);
}

6.2 依赖不稳定导致的问题

// ❌ 错误:callback 每次渲染都变
function useInterval(callback, delay) {
  useEffect(() => {
    const timer = setInterval(callback, delay);
    return () => clearInterval(timer);
  }, [callback, delay]);  // callback 变化导致定时器重启
}

// 使用
function Counter() {
  const [count, setCount] = useState(0);

  useInterval(() => {
    console.log('Count:', count);  // 每次 count 变化,定时器都重启
  }, 1000);

  return <div>{count}</div>;
}

解决方案 1:useRef 存储 callback

function useInterval(callback, delay) {
  const savedCallback = useRef(callback);

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    const timer = setInterval(() => {
      savedCallback.current();
    }, delay);
    return () => clearInterval(timer);
  }, [delay]);  // 只依赖 delay
}

解决方案 2:useEffectEvent(React 19)

import { useEffectEvent } from 'react';

function useInterval(callback, delay) {
  const onTick = useEffectEvent(callback);

  useEffect(() => {
    const timer = setInterval(onTick, delay);
    return () => clearInterval(timer);
  }, [delay]);
}

6.3 条件调用 Hook

function useUser(userId) {
  if (!userId) {
    return null;  // ❌ 错误:条件返回
  }

  const [user, setUser] = useState(null);  // Hook 在条件语句后

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  return user;
}

问题:违反了 Hooks 规则(Hooks 必须在顶层调用)。

解决

function useUser(userId) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    if (!userId) {
      setUser(null);
      return;
    }

    fetchUser(userId).then(setUser);
  }, [userId]);

  return user;
}

七、内存泄漏的常见场景

7.1 异步操作未取消

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(user => {
      setUser(user);  // 组件卸载后仍然执行,导致警告
    });
  }, [userId]);

  return <div>{user?.name}</div>;
}

问题:组件卸载后,fetchUser 的回调仍然执行,尝试更新已卸载组件的状态。

解决方案 1:使用标志位

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let cancelled = false;

    fetchUser(userId).then(user => {
      if (!cancelled) {
        setUser(user);
      }
    });

    return () => {
      cancelled = true;
    };
  }, [userId]);

  return <div>{user?.name}</div>;
}

解决方案 2:使用 AbortController

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then(res => res.json())
      .then(setUser)
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error(err);
        }
      });

    return () => {
      controller.abort();
    };
  }, [userId]);

  return <div>{user?.name}</div>;
}

7.2 事件监听器未移除

function WindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEffect(() => {
    const handleResize = () => {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    };

    window.addEventListener('resize', handleResize);
    // ❌ 忘记移除监听器
  }, []);

  return <div>{size.width} x {size.height}</div>;
}

解决

useEffect(() => {
  const handleResize = () => {
    setSize({ width: window.innerWidth, height: window.innerHeight });
  };

  window.addEventListener('resize', handleResize);
  
  return () => {
    window.removeEventListener('resize', handleResize);  // 清理
  };
}, []);

7.3 定时器未清除

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    // ❌ 忘记清除定时器
  }, []);

  return <div>{count}</div>;
}

问题:组件卸载后定时器仍在运行。

解决

useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);

  return () => clearInterval(timer);  // 清理
}, []);

八、useContext 的性能陷阱

8.1 Context 导致的全局重渲染

const AppContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [settings, setSettings] = useState({});

  const value = { user, setUser, theme, setTheme, settings, setSettings };

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

function UserName() {
  const { user } = useContext(AppContext);
  return <div>{user?.name}</div>;
}

function ThemeToggle() {
  const { theme, setTheme } = useContext(AppContext);
  return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle</button>;
}

问题theme 变化时,UserName 组件也会重渲染(虽然它只用到 user)。

解决方案 1:拆分 Context

const UserContext = createContext();
const ThemeContext = createContext();

function UserName() {
  const { user } = useContext(UserContext);  // 只订阅 user
  return <div>{user?.name}</div>;
}

function ThemeToggle() {
  const { theme, setTheme } = useContext(ThemeContext);  // 只订阅 theme
  return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle</button>;
}

解决方案 2:使用 useMemo 稳定 value

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');

  const value = useMemo(() => ({
    user, setUser, theme, setTheme
  }), [user, theme]);

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

解决方案 3:使用状态管理库

// 使用 Zustand
import create from 'zustand';

const useStore = create((set) => ({
  user: null,
  theme: 'light',
  setUser: (user) => set({ user }),
  setTheme: (theme) => set({ theme })
}));

function UserName() {
  const user = useStore(state => state.user);  // 只订阅 user
  return <div>{user?.name}</div>;
}

function ThemeToggle() {
  const theme = useStore(state => state.theme);  // 只订阅 theme
  const setTheme = useStore(state => state.setTheme);
  return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle</button>;
}

九、useReducer 的常见问题

9.1 reducer 中的副作用

function reducer(state, action) {
  switch (action.type) {
    case 'fetch_success':
      // ❌ 错误:在 reducer 中执行副作用
      localStorage.setItem('data', JSON.stringify(action.payload));
      return { ...state, data: action.payload };
    default:
      return state;
  }
}

问题:reducer 应该是纯函数,不应该有副作用。

解决:副作用放在 useEffect

function reducer(state, action) {
  switch (action.type) {
    case 'fetch_success':
      return { ...state, data: action.payload };
    default:
      return state;
  }
}

function Component() {
  const [state, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
    if (state.data) {
      localStorage.setItem('data', JSON.stringify(state.data));
    }
  }, [state.data]);
}

9.2 dispatch 的稳定性

function Parent() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return <Child onUpdate={dispatch} />;  // dispatch 是稳定的,不需要 useCallback
}

const Child = React.memo(function Child({ onUpdate }) {
  return <button onClick={() => onUpdate({ type: 'increment' })}>+1</button>;
});

好消息dispatch 函数是稳定的,不会在重渲染时改变,不需要用 useCallback 包裹。

十、调试 Hooks 的工具与技巧

10.1 React DevTools

查看 Hooks 状态

  1. 打开 React DevTools
  2. 选择组件
  3. 右侧面板显示所有 Hooks 的值

查看重渲染原因

  1. 打开 Profiler
  2. 录制操作
  3. 查看「Why did this render」

10.2 自定义调试 Hook

function useWhyDidYouUpdate(name, props) {
  const previousProps = useRef();

  useEffect(() => {
    if (previousProps.current) {
      const allKeys = Object.keys({ ...previousProps.current, ...props });
      const changedProps = {};

      allKeys.forEach(key => {
        if (previousProps.current[key] !== props[key]) {
          changedProps[key] = {
            from: previousProps.current[key],
            to: props[key]
          };
        }
      });

      if (Object.keys(changedProps).length > 0) {
        console.log('[why-did-you-update]', name, changedProps);
      }
    }

    previousProps.current = props;
  });
}

// 使用
function MyComponent(props) {
  useWhyDidYouUpdate('MyComponent', props);
  return <div>...</div>;
}

10.3 ESLint 插件

安装 eslint-plugin-react-hooks

npm install eslint-plugin-react-hooks --save-dev

配置 .eslintrc.js

module.exports = {
  plugins: ['react-hooks'],
  rules: {
    'react-hooks/rules-of-hooks': 'error',  // 检查 Hooks 规则
    'react-hooks/exhaustive-deps': 'warn'   // 检查依赖数组
  }
};

会自动检测:

  • Hooks 是否在顶层调用
  • 依赖数组是否完整
  • 是否在条件语句中调用 Hooks

十一、Hooks 最佳实践总结

11.1 useState

推荐

  • 用函数式更新避免闭包陷阱
  • 相关的状态合并成一个对象
  • 简单状态用 useState,复杂状态用 useReducer

避免

  • 在异步回调中直接使用 state
  • 过度拆分状态(导致多次渲染)
  • 用 state 存储可计算的值

11.2 useEffect

推荐

  • 依赖数组包含所有使用的外部变量
  • 返回清理函数
  • 一个 effect 只做一件事

避免

  • 空依赖数组 + 使用外部变量
  • 对象/数组作为依赖
  • 在 effect 中修改依赖的值(导致无限循环)

11.3 useCallback / useMemo

推荐

  • 子组件用了 React.memo 时使用
  • 计算成本高时使用
  • 先测量,再优化

避免

  • 过度使用(反而降低性能)
  • 依赖数组不完整
  • 用于简单计算

11.4 useRef

推荐

  • 存储不需要触发渲染的值
  • 存储 DOM 引用
  • 存储上一次的值

避免

  • 用 ref 存储需要触发渲染的值
  • 在渲染期间修改 ref.current

11.5 自定义 Hook

推荐

  • 提取可复用的逻辑
  • 返回清理函数
  • 使用 use 前缀命名

避免

  • 条件调用 Hook
  • 忘记清理副作用
  • 依赖不稳定

十二、总结

React Hooks 的常见陷阱:

  1. 闭包陷阱:异步操作中使用 state,用函数式更新解决
  2. 依赖地狱:对象/数组依赖导致无限循环,用 useMemo 或原始值解决
  3. 过度优化:不要给所有东西都加 useCallback/useMemo
  4. 内存泄漏:忘记清理定时器、事件监听器、异步操作
  5. Context 性能:拆分 Context 或使用状态管理库
  6. useRef 误用:记住它不会触发重渲染

避坑指南:

  • 使用 ESLint 插件自动检查
  • 用 React DevTools 调试
  • 先测量,再优化
  • 遵循 Hooks 规则
  • 写清理函数
  • 依赖数组要完整

记住:Hooks 很强大,但也很容易踩坑。理解原理比记住规则更重要。

如果这篇文章对你有帮助,欢迎点赞收藏。有问题欢迎评论区讨论,我会尽量回复。

附录:常用自定义 Hooks

// 1. useDebounce
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// 2. useLocalStorage
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const item = localStorage.getItem(key);
    return item ? JSON.parse(item) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

// 3. useOnClickOutside
function useOnClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) {
        return;
      }
      handler(event);
    };

    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener);

    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);
    };
  }, [ref, handler]);
}

// 4. useAsync
function useAsync(asyncFunction, immediate = true) {
  const [status, setStatus] = useState('idle');
  const [value, setValue] = useState(null);
  const [error, setError] = useState(null);

  const execute = useCallback(() => {
    setStatus('pending');
    setValue(null);
    setError(null);

    return asyncFunction()
      .then(response => {
        setValue(response);
        setStatus('success');
      })
      .catch(error => {
        setError(error);
        setStatus('error');
      });
  }, [asyncFunction]);

  useEffect(() => {
    if (immediate) {
      execute();
    }
  }, [execute, immediate]);

  return { execute, status, value, error };
}

JavaScript 的 Symbol.iterator:手写一个可迭代对象

一、什么是可迭代对象

在 JavaScript 中,可迭代对象是指实现了 Symbol.iterator 方法的对象,可以被 for...of 遍历。

// 数组是可迭代的
const arr = [1, 2, 3];
for (const item of arr) {
  console.log(item);  // 1, 2, 3
}

// 字符串是可迭代的
const str = 'hello';
for (const char of str) {
  console.log(char);  // h, e, l, l, o
}

// 普通对象不可迭代
const obj = { a: 1, b: 2 };
for (const item of obj) {  // ❌ TypeError: obj is not iterable
  console.log(item);
}

二、迭代器协议

要让对象可迭代,需要实现迭代器协议:

  1. 对象必须有 Symbol.iterator 方法
  2. 该方法返回一个迭代器对象
  3. 迭代器对象必须有 next() 方法
  4. next() 返回 { value, done } 格式的对象
const iterable = {
  [Symbol.iterator]() {
    let step = 0;
    return {
      next() {
        step++;
        if (step <= 3) {
          return { value: step, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

for (const num of iterable) {
  console.log(num);  // 1, 2, 3
}

三、手写可迭代对象

示例 1:Range 对象

class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    
    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

// 使用
const range = new Range(1, 5);
for (const num of range) {
  console.log(num);  // 1, 2, 3, 4, 5
}

// 也可以用扩展运算符
console.log([...range]);  // [1, 2, 3, 4, 5]

示例 2:斐波那契数列

class Fibonacci {
  constructor(limit) {
    this.limit = limit;
  }

  [Symbol.iterator]() {
    let prev = 0, curr = 1, count = 0;
    const limit = this.limit;
    
    return {
      next() {
        if (count++ < limit) {
          const value = prev;
          [prev, curr] = [curr, prev + curr];
          return { value, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

const fib = new Fibonacci(10);
console.log([...fib]);  // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

四、使用生成器函数

生成器函数是创建迭代器的更简洁方式。

基础语法

function* generator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = generator();
console.log(gen.next());  // { value: 1, done: false }
console.log(gen.next());  // { value: 2, done: false }
console.log(gen.next());  // { value: 3, done: false }
console.log(gen.next());  // { value: undefined, done: true }

用生成器重写 Range

class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  *[Symbol.iterator]() {
    for (let i = this.start; i <= this.end; i++) {
      yield i;
    }
  }
}

const range = new Range(1, 5);
console.log([...range]);  // [1, 2, 3, 4, 5]

简洁很多!

用生成器重写斐波那契

function* fibonacci(limit) {
  let prev = 0, curr = 1;
  for (let i = 0; i < limit; i++) {
    yield prev;
    [prev, curr] = [curr, prev + curr];
  }
}

console.log([...fibonacci(10)]);  // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

五、实战应用

应用 1:分页数据迭代

class PaginatedAPI {
  constructor(url, pageSize = 10) {
    this.url = url;
    this.pageSize = pageSize;
  }

  async *[Symbol.asyncIterator]() {
    let page = 1;
    let hasMore = true;

    while (hasMore) {
      const response = await fetch(
        `${this.url}?page=${page}&size=${this.pageSize}`
      );
      const data = await response.json();
      
      for (const item of data.items) {
        yield item;
      }
      
      hasMore = data.hasMore;
      page++;
    }
  }
}

// 使用
const api = new PaginatedAPI('/api/users');
for await (const user of api) {
  console.log(user);
}

应用 2:树结构遍历

class TreeNode {
  constructor(value, children = []) {
    this.value = value;
    this.children = children;
  }

  // 深度优先遍历
  *[Symbol.iterator]() {
    yield this.value;
    for (const child of this.children) {
      yield* child;  // 委托给子节点的迭代器
    }
  }

  // 广度优先遍历
  *bfs() {
    const queue = [this];
    while (queue.length > 0) {
      const node = queue.shift();
      yield node.value;
      queue.push(...node.children);
    }
  }
}

const tree = new TreeNode(1, [
  new TreeNode(2, [
    new TreeNode(4),
    new TreeNode(5)
  ]),
  new TreeNode(3)
]);

console.log([...tree]);  // DFS: [1, 2, 4, 5, 3]
console.log([...tree.bfs()]);  // BFS: [1, 2, 3, 4, 5]

应用 3:无限序列

function* naturalNumbers() {
  let n = 1;
  while (true) {
    yield n++;
  }
}

// 取前 10 个自然数
function take(iterable, n) {
  const result = [];
  for (const item of iterable) {
    if (result.length >= n) break;
    result.push(item);
  }
  return result;
}

console.log(take(naturalNumbers(), 10));  // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

六、for...of 的原理

for...of 循环的本质是调用对象的 Symbol.iterator 方法。

// for...of 循环
for (const item of iterable) {
  console.log(item);
}

// 等价于
const iterator = iterable[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
  const item = result.value;
  console.log(item);
  result = iterator.next();
}

七、可迭代对象的应用场景

1. 扩展运算符

const range = new Range(1, 5);
const arr = [...range];  // [1, 2, 3, 4, 5]

2. 解构赋值

const [first, second, ...rest] = range;
console.log(first, second, rest);  // 1 2 [3, 4, 5]

3. Array.from

const arr = Array.from(range);  // [1, 2, 3, 4, 5]

4. Promise.all

async function* asyncGenerator() {
  yield Promise.resolve(1);
  yield Promise.resolve(2);
  yield Promise.resolve(3);
}

const promises = [...asyncGenerator()];
const results = await Promise.all(promises);
console.log(results);  // [1, 2, 3]

八、迭代器的高级技巧

1. 迭代器组合

function* concat(...iterables) {
  for (const iterable of iterables) {
    yield* iterable;
  }
}

const combined = concat([1, 2], [3, 4], [5, 6]);
console.log([...combined]);  // [1, 2, 3, 4, 5, 6]

2. 迭代器过滤

function* filter(iterable, predicate) {
  for (const item of iterable) {
    if (predicate(item)) {
      yield item;
    }
  }
}

const numbers = new Range(1, 10);
const evens = filter(numbers, n => n % 2 === 0);
console.log([...evens]);  // [2, 4, 6, 8, 10]

3. 迭代器映射

function* map(iterable, mapper) {
  for (const item of iterable) {
    yield mapper(item);
  }
}

const numbers = new Range(1, 5);
const squares = map(numbers, n => n * n);
console.log([...squares]);  // [1, 4, 9, 16, 25]

九、性能注意事项

  1. 避免在迭代器中做重计算
// ❌ 每次都重新计算
*[Symbol.iterator]() {
  for (let i = 0; i < this.expensiveCalculation(); i++) {
    yield i;
  }
}

// ✅ 缓存计算结果
*[Symbol.iterator]() {
  const limit = this.expensiveCalculation();
  for (let i = 0; i < limit; i++) {
    yield i;
  }
}
  1. 注意内存泄漏
// ❌ 无限迭代器可能导致内存泄漏
const infinite = naturalNumbers();
const arr = [...infinite];  // 永远不会结束!

// ✅ 使用 take 限制数量
const arr = take(infinite, 100);

前端性能优化终极清单:从 3 秒到 0.5 秒的实战经验

一、为什么要优化性能

1.1 数据说话

  • 页面加载时间每增加 1 秒,转化率下降 7%
  • 53% 的移动用户会放弃加载超过 3 秒的页面
  • Google 将页面速度作为搜索排名因素

1.2 真实案例

我们优化了一个电商网站:

  • 优化前:首屏 3.2 秒,跳出率 45%
  • 优化后:首屏 0.8 秒,跳出率 28%
  • 结果:转化率提升 35%,营收增加 ¥200 万/月

二、性能指标

2.1 核心 Web Vitals

Google 定义的三个关键指标:

指标 含义 目标
LCP 最大内容绘制 < 2.5s
FID 首次输入延迟 < 100ms
CLS 累积布局偏移 < 0.1

2.2 其他重要指标

  • FCP(首次内容绘制):< 1.8s
  • TTI(可交互时间):< 3.8s
  • TBT(总阻塞时间):< 200ms
  • Speed Index(速度指数):< 3.4s

2.3 测量工具

  • Lighthouse:Chrome 内置,综合评分
  • WebPageTest:详细的瀑布图和视频
  • Chrome DevTools:Performance 面板
  • Real User Monitoring:真实用户数据(如 Google Analytics)

三、资源优化

3.1 图片优化

选择合适的格式

JPEG:照片、复杂图像
PNG:需要透明背景
WebP:现代浏览器,体积小 30%
AVIF:最新格式,体积更小,但兼容性差
SVG:图标、Logo

响应式图片

<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="描述" loading="lazy">
</picture>

<!-- 或用 srcset -->
<img
  srcset="small.jpg 480w, medium.jpg 800w, large.jpg 1200w"
  sizes="(max-width: 600px) 480px, (max-width: 1000px) 800px, 1200px"
  src="medium.jpg"
  alt="描述"
>

懒加载

<img src="image.jpg" loading="lazy" alt="描述">

或用 Intersection Observer:

const images = document.querySelectorAll('img[data-src]');

const imageObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      imageObserver.unobserve(img);
    }
  });
});

images.forEach(img => imageObserver.observe(img));

压缩工具

  • 在线:TinyPNG、Squoosh
  • CLI:imagemin、sharp
  • 构建工具:webpack 的 image-webpack-loader

3.2 字体优化

使用 font-display

@font-face {
  font-family: 'MyFont';
  src: url('font.woff2') format('woff2');
  font-display: swap; /* 立即显示备用字体 */
}

预加载关键字体

<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>

子集化

只包含需要的字符:

# 用 glyphhanger 生成子集
npx glyphhanger --subset=font.ttf --formats=woff2 --whitelist="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"

3.3 JavaScript 优化

代码分割

// 路由级别分割
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));

// 组件级别分割
const HeavyComponent = lazy(() => import('./components/HeavyComponent'));

<Suspense fallback={<Loading />}>
  <HeavyComponent />
</Suspense>

Tree Shaking

// ❌ 差:导入整个库
import _ from 'lodash';

// ✅ 好:只导入需要的
import debounce from 'lodash/debounce';

压缩和混淆

// webpack.config.js
module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true, // 移除 console
          },
        },
      }),
    ],
  },
};

3.4 CSS 优化

移除未使用的 CSS

# 用 PurgeCSS
npm install @fullhuman/postcss-purgecss
// postcss.config.js
module.exports = {
  plugins: [
    require('@fullhuman/postcss-purgecss')({
      content: ['./src/**/*.html', './src/**/*.js'],
    }),
  ],
};

关键 CSS 内联

<style>
  /* 首屏关键样式 */
  .header { ... }
  .hero { ... }
</style>

<link rel="stylesheet" href="main.css">

CSS 压缩

// webpack.config.js
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [new CssMinimizerPlugin()],
  },
};

四、加载策略

4.1 预加载

<!-- 预加载关键资源 -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="hero.jpg" as="image">
<link rel="preload" href="app.js" as="script">

4.2 预连接

<!-- 提前建立连接 -->
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">

4.3 预获取

<!-- 空闲时加载下一页资源 -->
<link rel="prefetch" href="next-page.js">

4.4 异步和延迟

<!-- 异步加载,不阻塞解析 -->
<script src="analytics.js" async></script>

<!-- 延迟到 DOMContentLoaded 后执行 -->
<script src="non-critical.js" defer></script>

五、渲染优化

5.1 避免布局抖动

// ❌ 差:强制同步布局
for (let i = 0; i < elements.length; i++) {
  const height = elements[i].offsetHeight; // 读
  elements[i].style.height = height + 10 + 'px'; // 写
}

// ✅ 好:批量读写
const heights = elements.map(el => el.offsetHeight); // 批量读
elements.forEach((el, i) => {
  el.style.height = heights[i] + 10 + 'px'; // 批量写
});

5.2 使用 CSS Transform

/* ❌ 差:触发重排 */
.box {
  left: 100px;
  top: 100px;
}

/* ✅ 好:只触发合成 */
.box {
  transform: translate(100px, 100px);
}

5.3 虚拟滚动

import { FixedSizeList } from 'react-window';

<FixedSizeList
  height={600}
  itemCount={10000}
  itemSize={50}
  width="100%"
>
  {({ index, style }) => (
    <div style={style}>Item {index}</div>
  )}
</FixedSizeList>

5.4 防抖和节流

// 防抖:延迟执行
const debounce = (fn, delay) => {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
};

// 节流:限制频率
const throttle = (fn, delay) => {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last >= delay) {
      fn(...args);
      last = now;
    }
  };
};

// 使用
window.addEventListener('scroll', throttle(() => {
  console.log('滚动');
}, 200));

六、网络优化

6.1 HTTP/2

启用 HTTP/2,支持多路复用、头部压缩。

# Nginx 配置
server {
  listen 443 ssl http2;
  # ...
}

6.2 CDN

将静态资源部署到 CDN:

<script src="https://cdn.example.com/app.js"></script>

6.3 Gzip / Brotli 压缩

# Nginx 配置
gzip on;
gzip_types text/plain text/css application/json application/javascript;

# Brotli(更高压缩率)
brotli on;
brotli_types text/plain text/css application/json application/javascript;

6.4 缓存策略

# 静态资源长期缓存
location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2)$ {
  expires 1y;
  add_header Cache-Control "public, immutable";
}

# HTML 不缓存
location ~* \.html$ {
  expires -1;
  add_header Cache-Control "no-cache, no-store, must-revalidate";
}

七、框架优化

7.1 React

使用 memo 和 useMemo

const ExpensiveComponent = memo(({ data }) => {
  return <div>{data}</div>;
});

const result = useMemo(() => {
  return expensiveCalculation(data);
}, [data]);

虚拟化长列表

import { Virtuoso } from 'react-virtuoso';

<Virtuoso
  data={items}
  itemContent={(index, item) => <div>{item.name}</div>}
/>

懒加载路由

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));

7.2 Vue

使用 v-once 和 v-memo

<!-- 只渲染一次 -->
<div v-once>{{ staticContent }}</div>

<!-- 条件缓存 -->
<div v-memo="[value]">{{ expensiveComputation }}</div>

异步组件

const AsyncComponent = defineAsyncComponent(() =>
  import('./components/Heavy.vue')
);

7.3 Next.js

使用 ISR

export async function getStaticProps() {
  return {
    props: { data },
    revalidate: 60, // 60 秒后重新生成
  };
}

图片优化

import Image from 'next/image';

<Image
  src="/hero.jpg"
  width={800}
  height={600}
  priority // 预加载
  placeholder="blur" // 模糊占位
/>

八、监控和分析

8.1 性能监控

// 使用 Performance API
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry.name, entry.startTime);
  }
});

observer.observe({ entryTypes: ['navigation', 'resource', 'paint'] });

8.2 错误监控

// 使用 Sentry
import * as Sentry from '@sentry/react';

Sentry.init({
  dsn: 'your-dsn',
  tracesSampleRate: 1.0,
});

8.3 用户体验监控

// Web Vitals
import { getCLS, getFID, getLCP } from 'web-vitals';

getCLS(console.log);
getFID(console.log);
getLCP(console.log);

九、优化清单

资源优化

  • 图片使用 WebP/AVIF 格式
  • 图片懒加载
  • 图片响应式(srcset)
  • 字体子集化
  • 字体 font-display: swap
  • JavaScript 代码分割
  • Tree Shaking
  • 移除未使用的 CSS
  • 关键 CSS 内联

加载策略

  • 预加载关键资源
  • 预连接第三方域名
  • 异步加载非关键脚本
  • 延迟加载非首屏内容

渲染优化

  • 避免布局抖动
  • 使用 CSS Transform
  • 虚拟滚动长列表
  • 防抖节流高频事件

网络优化

  • 启用 HTTP/2
  • 使用 CDN
  • Gzip/Brotli 压缩
  • 合理的缓存策略

监控

  • 性能监控(Lighthouse)
  • 错误监控(Sentry)
  • 用户体验监控(Web Vitals)

十、实战案例

案例:电商首页优化

优化前:

  • LCP: 4.2s
  • FID: 180ms
  • CLS: 0.25
  • 总分: 45/100

优化措施:

  1. 图片转 WebP,体积减少 40%
  2. 首屏图片预加载,LCP 降到 1.8s
  3. 代码分割,首包体积从 800KB 降到 200KB
  4. 关键 CSS 内联,FCP 降到 0.9s
  5. 固定图片尺寸,CLS 降到 0.05

优化后:

  • LCP: 1.8s
  • FID: 50ms
  • CLS: 0.05
  • 总分: 92/100

总结

性能优化的核心:

  • 资源优化:图片、字体、JS、CSS 都要压缩和优化
  • 加载策略:预加载、预连接、异步、延迟
  • 渲染优化:避免重排、虚拟滚动、防抖节流
  • 网络优化:HTTP/2、CDN、压缩、缓存
  • 持续监控:用数据驱动优化

记住:性能优化不是一次性的,而是持续的过程。

从今天开始,用这份清单优化你的网站吧!

你不会使用 CSS 函数 clamp()?那你太 low 了😀

一、clamp() 是什么

clamp() 是 CSS 的一个数学函数,用于在一个范围内限制值。语法:

clamp(最小值, 首选值, 最大值)

浏览器会:

  1. 计算首选值
  2. 如果首选值 < 最小值,返回最小值
  3. 如果首选值 > 最大值,返回最大值
  4. 否则返回首选值

简单来说:在最小值和最大值之间,取一个动态的值

二、为什么要用 clamp()

2.1 传统方案的痛点

响应式字体大小,传统做法:

.title {
  font-size: 16px;
}

@media (min-width: 768px) {
  .title {
    font-size: 20px;
  }
}

@media (min-width: 1024px) {
  .title {
    font-size: 24px;
  }
}

@media (min-width: 1440px) {
  .title {
    font-size: 28px;
  }
}

问题:

  • 代码冗长,难维护
  • 断点之间是跳跃的,不够平滑
  • 需要写多个媒体查询

2.2 用 clamp() 的方案

.title {
  font-size: clamp(16px, 4vw, 28px);
}

一行搞定!

  • 最小 16px(小屏幕)
  • 最大 28px(大屏幕)
  • 中间根据视口宽度(4vw)动态变化

三、基础用法

3.1 响应式字体

h1 {
  font-size: clamp(1.5rem, 5vw, 3rem);
}

p {
  font-size: clamp(1rem, 2.5vw, 1.25rem);
}

效果:

  • 小屏幕(320px):h1 = 24px,p = 16px
  • 中屏幕(768px):h1 = 38.4px,p = 19.2px
  • 大屏幕(1920px):h1 = 48px(最大值),p = 20px

3.2 响应式间距

.container {
  padding: clamp(1rem, 5vw, 3rem);
  gap: clamp(0.5rem, 2vw, 2rem);
}

3.3 响应式宽度

.card {
  width: clamp(300px, 50vw, 600px);
}

卡片宽度:

  • 最小 300px(不会太窄)
  • 最大 600px(不会太宽)
  • 中间占视口宽度的 50%

四、进阶技巧

4.1 结合 calc() 使用

.title {
  font-size: clamp(1rem, 1rem + 2vw, 3rem);
}

解释:

  • 基础大小 1rem
  • 加上 2vw 的动态增量
  • 最大不超过 3rem

4.2 用 rem 和 vw 混合

.text {
  font-size: clamp(1rem, 0.875rem + 0.5vw, 1.5rem);
}

好处:

  • 基础大小用 rem(尊重用户字体设置)
  • 动态部分用 vw(响应视口)
  • 最大值用 rem(保持可读性)

4.3 负值和小数

.element {
  margin-top: clamp(-2rem, -5vw, 0);
  opacity: clamp(0.5, 0.5 + 0.1vw, 1);
}

4.4 多个属性共用一个变量

:root {
  --fluid-spacing: clamp(1rem, 3vw, 3rem);
}

.container {
  padding: var(--fluid-spacing);
  gap: var(--fluid-spacing);
}

五、实战案例

案例 1:流式排版

:root {
  --font-size-sm: clamp(0.875rem, 0.8rem + 0.3vw, 1rem);
  --font-size-base: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
  --font-size-lg: clamp(1.25rem, 1rem + 1vw, 2rem);
  --font-size-xl: clamp(1.5rem, 1.2rem + 1.5vw, 3rem);
}

body {
  font-size: var(--font-size-base);
}

h1 {
  font-size: var(--font-size-xl);
}

h2 {
  font-size: var(--font-size-lg);
}

small {
  font-size: var(--font-size-sm);
}

案例 2:响应式卡片网格

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(clamp(250px, 30vw, 400px), 1fr));
  gap: clamp(1rem, 3vw, 2rem);
}

效果:

  • 卡片最小 250px,最大 400px
  • 间距最小 1rem,最大 2rem
  • 自动换行,响应式布局

案例 3:流式容器

.container {
  width: clamp(320px, 90vw, 1200px);
  margin: 0 auto;
  padding: clamp(1rem, 5vw, 3rem);
}

案例 4:动态行高

p {
  font-size: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
  line-height: clamp(1.5, 1.4 + 0.2vw, 1.8);
}

六、计算公式

6.1 如何确定首选值

公式:

首选值 = 基础值 + (增量 × vw)

示例:想要字体在 375px 屏幕上是 16px,在 1920px 屏幕上是 24px。

增量 = (24 - 16) / (1920 - 375) × 100 = 0.518vw
基础值 = 16 - (375 × 0.518 / 100) = 14.06px

首选值 = 14.06px + 0.518vw

CSS:

font-size: clamp(16px, 14.06px + 0.518vw, 24px);

6.2 在线计算器

推荐工具:

输入最小值、最大值、视口范围,自动生成 clamp() 代码。

七、浏览器兼容性

浏览器 版本
Chrome 79+
Firefox 75+
Safari 13.1+
Edge 79+

兼容性很好,覆盖 95%+ 的用户。

7.1 降级方案

.title {
  font-size: 20px; /* 降级 */
  font-size: clamp(16px, 4vw, 28px);
}

不支持 clamp() 的浏览器会用 20px。

八、常见误区

误区 1:滥用 vw

/* ❌ 差:在小屏幕上太小 */
font-size: clamp(10px, 5vw, 30px);

/* ✅ 好:保证最小可读性 */
font-size: clamp(16px, 5vw, 30px);

误区 2:忽略可访问性

/* ❌ 差:用户放大字体时不生效 */
font-size: clamp(16px, 4vw, 28px);

/* ✅ 好:用 rem,尊重用户设置 */
font-size: clamp(1rem, 1rem + 1vw, 1.75rem);

误区 3:过度复杂

/* ❌ 差:难以理解和维护 */
font-size: clamp(
  calc(1rem + 0.5vw - 2px),
  calc(1rem + 1vw + 0.2vh),
  calc(2rem - 0.3vw + 5px)
);

/* ✅ 好:简洁明了 */
font-size: clamp(1rem, 1rem + 1vw, 2rem);

九、与其他方案对比

9.1 vs 媒体查询

方案 优点 缺点
媒体查询 精确控制断点 代码冗长,跳跃式变化
clamp() 代码简洁,平滑过渡 不适合复杂布局

结论:简单响应式用 clamp(),复杂布局用媒体查询。

9.2 vs min() / max()

/* min():取最小值 */
width: min(500px, 100%);  /* 最大 500px */

/* max():取最大值 */
width: max(300px, 50%);   /* 最小 300px */

/* clamp():限制范围 */
width: clamp(300px, 50%, 500px);  /* 300px ~ 500px */

clamp()min()max() 的结合。

十、实用代码片段

片段 1:全局流式排版

:root {
  --step--2: clamp(0.69rem, 0.66rem + 0.18vw, 0.84rem);
  --step--1: clamp(0.83rem, 0.78rem + 0.29vw, 1.05rem);
  --step-0: clamp(1rem, 0.91rem + 0.43vw, 1.31rem);
  --step-1: clamp(1.2rem, 1.07rem + 0.63vw, 1.64rem);
  --step-2: clamp(1.44rem, 1.26rem + 0.89vw, 2.05rem);
  --step-3: clamp(1.73rem, 1.48rem + 1.24vw, 2.56rem);
  --step-4: clamp(2.07rem, 1.73rem + 1.70vw, 3.20rem);
  --step-5: clamp(2.49rem, 2.03rem + 2.31vw, 4.00rem);
}

片段 2:流式间距

:root {
  --space-3xs: clamp(0.25rem, 0.23rem + 0.11vw, 0.31rem);
  --space-2xs: clamp(0.5rem, 0.46rem + 0.21vw, 0.63rem);
  --space-xs: clamp(0.75rem, 0.68rem + 0.32vw, 0.94rem);
  --space-s: clamp(1rem, 0.91rem + 0.43vw, 1.25rem);
  --space-m: clamp(1.5rem, 1.36rem + 0.64vw, 1.88rem);
  --space-l: clamp(2rem, 1.82rem + 0.85vw, 2.50rem);
  --space-xl: clamp(3rem, 2.73rem + 1.28vw, 3.75rem);
  --space-2xl: clamp(4rem, 3.64rem + 1.70vw, 5.00rem);
  --space-3xl: clamp(6rem, 5.45rem + 2.55vw, 7.50rem);
}

片段 3:响应式容器

.container {
  width: clamp(320px, 90vw, 1200px);
  margin-inline: auto;
  padding-inline: clamp(1rem, 5vw, 3rem);
}

十一、调试技巧

技巧 1:用 CSS 变量方便调试

:root {
  --min: 1rem;
  --val: 1rem + 1vw;
  --max: 2rem;
}

.title {
  font-size: clamp(var(--min), var(--val), var(--max));
}

技巧 2:用浏览器开发者工具

Chrome DevTools 会显示 clamp() 的计算结果:

font-size: clamp(16px, 4vw, 28px)
计算值: 24px

技巧 3:用注释标注

.title {
  /* 小屏 16px,大屏 28px,中间平滑过渡 */
  font-size: clamp(16px, 1rem + 1vw, 28px);
}

十二、最佳实践

  1. 优先用 rem:尊重用户字体设置
  2. 保证最小可读性:字体最小 16px,间距最小 0.5rem
  3. 避免过度复杂:首选值尽量简单
  4. 用 CSS 变量:方便维护和调试
  5. 测试多种屏幕:320px、768px、1920px 都要看
  6. 提供降级方案:不支持的浏览器用固定值

总结

clamp() 的核心价值:

  • 一行代码搞定响应式
  • 平滑过渡,不跳跃
  • 代码简洁,易维护

常用场景:

  • 响应式字体:clamp(1rem, 1rem + 1vw, 2rem)
  • 响应式间距:clamp(1rem, 3vw, 3rem)
  • 响应式宽度:clamp(300px, 50vw, 600px)

记住公式:

clamp(最小值, 基础值 + 动态值, 最大值)

不会用 clamp()?那你真的太 low 了😀

现在学还不晚,赶紧用起来!

babel-loader:让你的 JS 代码兼容所有浏览器

一、为什么需要 Babel

现代 JavaScript 有很多新特性(ES6+),但旧浏览器不支持。

// ES6+ 代码
const greet = (name) => `Hello, ${name}!`;
class Person {
  constructor(name) {
    this.name = name;
  }
}

// 旧浏览器需要转换为 ES5
var greet = function(name) {
  return 'Hello, ' + name + '!';
};
function Person(name) {
  this.name = name;
}

Babel 就是用来做这个转换的,babel-loader 让 webpack 能够使用 Babel。


二、基础配置

安装

npm install --save-dev babel-loader @babel/core @babel/preset-env

webpack 配置

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  }
};

三、Babel 配置文件

推荐使用独立的配置文件,而不是写在 webpack 配置中。

babel.config.js(推荐)

module.exports = {
  presets: [
    ['@babel/preset-env', {
      targets: {
        browsers: ['> 1%', 'last 2 versions', 'not dead']
      },
      useBuiltIns: 'usage',
      corejs: 3
    }]
  ],
  plugins: []
};

.babelrc

{
  "presets": [
    ["@babel/preset-env", {
      "targets": "> 0.25%, not dead"
    }]
  ]
}

四、Preset 详解

@babel/preset-env

最常用的 preset,根据目标环境自动确定需要的转换。

{
  presets: [
    ['@babel/preset-env', {
      // 目标环境
      targets: {
        chrome: '58',
        ie: '11',
        node: '12'
      },
      
      // 或使用 browserslist 查询
      targets: '> 0.25%, not dead',
      
      // 模块转换:'auto' | 'amd' | 'umd' | 'systemjs' | 'commonjs' | false
      modules: false,  // webpack 已处理模块,设为 false
      
      // polyfill 策略
      useBuiltIns: 'usage',  // 'usage' | 'entry' | false
      corejs: 3
    }]
  ]
}

@babel/preset-react

转换 JSX 语法。

npm install --save-dev @babel/preset-react
{
  presets: [
    '@babel/preset-env',
    ['@babel/preset-react', {
      runtime: 'automatic'  // React 17+ 不需要 import React
    }]
  ]
}

@babel/preset-typescript

转换 TypeScript。

npm install --save-dev @babel/preset-typescript
{
  presets: [
    '@babel/preset-env',
    '@babel/preset-typescript'
  ]
}

五、Polyfill 策略

useBuiltIns: 'usage'(推荐)

根据代码中实际使用的特性自动引入 polyfill。

npm install --save core-js@3
// 源代码
const promise = Promise.resolve();
const arr = [1, 2, 3].includes(2);

// Babel 自动添加需要的 polyfill
import "core-js/modules/es.promise";
import "core-js/modules/es.array.includes";

useBuiltIns: 'entry'

手动在入口引入,Babel 根据目标环境替换为需要的 polyfill。

// 入口文件
import 'core-js/stable';
import 'regenerator-runtime/runtime';

// Babel 会替换为具体需要的 polyfill

useBuiltIns: false

不自动引入 polyfill,需要手动管理。


六、常用插件

1. 类属性

npm install --save-dev @babel/plugin-proposal-class-properties
// 支持类属性语法
class MyClass {
  count = 0;  // 实例属性
  static version = '1.0';  // 静态属性
  
  handleClick = () => {  // 箭头函数自动绑定 this
    this.count++;
  }
}

2. 装饰器

npm install --save-dev @babel/plugin-proposal-decorators
{
  plugins: [
    ['@babel/plugin-proposal-decorators', { legacy: true }],
    ['@babel/plugin-proposal-class-properties', { loose: true }]
  ]
}
@connect(mapStateToProps)
class MyComponent extends React.Component {
  // ...
}

3. 可选链和空值合并

npm install --save-dev @babel/plugin-proposal-optional-chaining
npm install --save-dev @babel/plugin-proposal-nullish-coalescing-operator
// 可选链
const name = user?.profile?.name;

// 空值合并
const count = value ?? 0;

七、性能优化

1. 缓存

{
  test: /\.js$/,
  exclude: /node_modules/,
  use: {
    loader: 'babel-loader',
    options: {
      cacheDirectory: true,  // 开启缓存
      cacheCompression: false  // 不压缩缓存
    }
  }
}

2. 限制转换范围

{
  test: /\.js$/,
  // 只转换 src 目录
  include: path.resolve(__dirname, 'src'),
  // 排除 node_modules
  exclude: /node_modules/,
  use: 'babel-loader'
}

3. 使用 thread-loader

npm install --save-dev thread-loader
{
  test: /\.js$/,
  use: [
    'thread-loader',  // 多线程编译
    'babel-loader'
  ]
}

八、环境区分

开发环境

// babel.config.js
module.exports = (api) => {
  const isDev = api.env('development');
  
  return {
    presets: [
      ['@babel/preset-env', {
        targets: isDev ? { node: 'current' } : '> 0.25%, not dead',
        modules: false
      }],
      '@babel/preset-react'
    ],
    plugins: [
      isDev && 'react-refresh/babel'  // 开发环境热更新
    ].filter(Boolean)
  };
};

生产环境

{
  presets: [
    ['@babel/preset-env', {
      targets: '> 0.25%, not dead',
      useBuiltIns: 'usage',
      corejs: 3
    }]
  ],
  plugins: [
    ['transform-remove-console', {  // 移除 console
      exclude: ['error', 'warn']
    }]
  ]
}

九、常见问题

1. async/await 不工作?

需要 regenerator-runtime:

npm install --save regenerator-runtime
// 入口文件
import 'regenerator-runtime/runtime';

或使用 @babel/plugin-transform-runtime:

npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
{
  plugins: [
    ['@babel/plugin-transform-runtime', {
      regenerator: true
    }]
  ]
}

2. 某些 ES6+ 特性不转换?

检查 targets 配置,可能目标环境已支持该特性。

3. 打包体积太大?

  • 使用 useBuiltIns: 'usage' 按需引入 polyfill
  • 检查是否转换了 node_modules
  • 使用 @babel/plugin-transform-runtime 避免重复代码

十、完整配置示例

// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', {
      targets: {
        browsers: ['> 1%', 'last 2 versions', 'not dead']
      },
      useBuiltIns: 'usage',
      corejs: 3,
      modules: false
    }],
    ['@babel/preset-react', {
      runtime: 'automatic'
    }],
    '@babel/preset-typescript'
  ],
  plugins: [
    ['@babel/plugin-proposal-decorators', { legacy: true }],
    ['@babel/plugin-proposal-class-properties', { loose: true }],
    '@babel/plugin-proposal-optional-chaining',
    '@babel/plugin-proposal-nullish-coalescing-operator',
    ['@babel/plugin-transform-runtime', {
      corejs: false,
      helpers: true,
      regenerator: true
    }]
  ]
};
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx|ts|tsx)$/,
        include: path.resolve(__dirname, 'src'),
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true,
            cacheCompression: false
          }
        }
      }
    ]
  }
};

lodash 到 lodash-es 多的不仅仅是后缀!深入源码看 ES Module 带来的性能与体积优化

一、这东西是什么

lodashlodash-es 都是 JavaScript 实用工具库,提供数组、对象、字符串等数据类型的操作函数。它们的关系是:

  • lodash:基于 CommonJS 模块系统,主要使用 require() 导入
  • lodash-es:基于 ES Module 模块系统,使用 import 导入

核心差异:lodash-es 不是简单的格式转换,而是从源码层面重构了模块化结构,让现代打包工具(Webpack、Rollup、Vite)能够进行 Tree Shaking(摇树优化),只打包用到的函数。

二、这东西有什么用

适用场景

  • 现代前端项目(Vue、React、Angular)
  • 需要按需引入工具函数的场景
  • 对打包体积敏感的项目(移动端、性能要求高的应用)

能带来什么收益

  1. 体积优化:从几百 KB 降到几 KB
  2. Tree Shaking:自动移除未使用的代码
  3. 更好的静态分析:IDE 和打包工具能更准确分析依赖
  4. 未来兼容性:ES Module 是 JavaScript 标准模块系统

三、官方链接

四、从源码看差异

lodash 源码结构(CommonJS)

// lodash 的 _.debounce 函数
module.exports = function debounce(func, wait, options) {
  // ... 实现代码
  return debounced;
};

// 整个 lodash 导出
module.exports = {
  debounce: require('./debounce'),
  throttle: require('./throttle'),
  // ... 几百个函数
};

lodash-es 源码结构(ES Module)

// lodash-es 的 debounce.js
export default function debounce(func, wait, options) {
  // ... 实现代码
  return debounced;
}

// 每个函数独立文件,支持按需导入
// debounce.js, throttle.js, cloneDeep.js 等

关键区别:lodash 将所有函数打包在一个大对象里,lodash-es 将每个函数放在独立文件中。

五、如何做一个 demo 出来

1. 环境要求

  • Node.js 14+
  • 现代打包工具(Webpack 4+、Rollup、Vite)

2. 安装命令

# 安装 lodash-es
npm install lodash-es

# 或者安装特定函数
npm install lodash.debounce lodash.throttle

3. 目录结构说明

project/
├── src/
│   ├── main.js      # 主入口文件
│   └── utils.js     # 工具函数
├── package.json
└── webpack.config.js

4. 最小可运行示例

使用 lodash(传统方式)

// 导入整个 lodash(几百 KB)
const _ = require('lodash');

// 只使用 debounce 函数,但打包了整个 lodash
const debouncedFunc = _.debounce(() => {
  console.log('防抖函数');
}, 300);

使用 lodash-es(现代方式)

// 按需导入特定函数(Webpack 会自动 Tree Shaking)
import { debounce, throttle } from 'lodash-es';

// 或者只导入需要的函数
import debounce from 'lodash-es/debounce';
import throttle from 'lodash-es/throttle';

const debouncedFunc = debounce(() => {
  console.log('防抖函数');
}, 300);

const throttledFunc = throttle(() => {
  console.log('节流函数');
}, 300);

5. Webpack 配置示例

// webpack.config.js
module.exports = {
  mode: 'production',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  optimization: {
    usedExports: true,  // 启用 Tree Shaking
    minimize: true      // 代码压缩
  }
};

6. 打包体积对比

创建测试文件:

// test-lodash.js
const _ = require('lodash');
console.log(_.debounce);

// test-lodash-es.js  
import { debounce } from 'lodash-es';
console.log(debounce);

运行打包命令:

# 打包 lodash 版本
npx webpack --entry ./test-lodash.js --output-filename bundle-lodash.js

# 打包 lodash-es 版本  
npx webpack --entry ./test-lodash-es.js --output-filename bundle-lodash-es.js

# 查看文件大小
ls -lh dist/*.js

预期结果

  • bundle-lodash.js:~70KB(整个 lodash)
  • bundle-lodash-es.js:~2KB(仅 debounce 函数)

六、Tree Shaking 原理深入

1. 静态分析

ES Module 的 importexport静态的,打包工具可以在编译时分析:

  • 哪些函数被导入了
  • 哪些函数被使用了
  • 哪些函数可以安全移除

2. 源码对比分析

查看 lodash-es 的 cloneDeep 函数源码:

// lodash-es/cloneDeep.js
import baseClone from './.internal/baseClone.js';

/** 用于标识深拷贝 */
const CLONE_DEEP_FLAG = 1;
const CLONE_SYMBOLS_FLAG = 4;

function cloneDeep(value) {
  return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG);
}

export default cloneDeep;

关键点:每个函数都是独立的 ES Module,有自己的依赖关系图。

3. 打包工具如何工作

// Webpack 的 Tree Shaking 过程
1. 解析 import 语句 → 找到 lodash-es/debounce
2. 分析 debounce.js 的依赖 → 找到内部依赖
3. 标记使用到的函数 → debounce 被标记为 used
4. 移除未标记的函数 → 其他函数被移除
5. 生成最终 bundle → 只包含 debounce 及其依赖

七、性能实测对比

测试代码

// performance-test.js
import { debounce, throttle, cloneDeep } from 'lodash-es';
// 对比
const _ = require('lodash');

// 测试函数
function testDebounce() {
  const start = performance.now();
  for (let i = 0; i < 10000; i++) {
    const fn = debounce(() => {}, 100);
    fn();
  }
  return performance.now() - start;
}

// 运行测试
console.log('lodash-es debounce:', testDebounce(), 'ms');

体积对比表

使用场景 lodash 体积 lodash-es 体积 优化比例
只使用 debounce 72KB 1.8KB 97.5%↓
使用 5 个常用函数 72KB 8.2KB 88.6%↓
使用 10 个函数 72KB 15.4KB 78.6%↓
使用全部函数 72KB 72KB 0%

八、周边生态推荐

1. 相关工具库

  • lodash-webpack-plugin:Webpack 插件,进一步优化 lodash
  • babel-plugin-lodash:Babel 插件,自动转换 lodash 导入
  • eslint-plugin-lodash:ESLint 插件,检查 lodash 使用

2. 最佳实践

// 推荐:按需导入特定函数
import debounce from 'lodash-es/debounce';
import throttle from 'lodash-es/throttle';

// 不推荐:导入整个库
import _ from 'lodash-es';

// 特殊情况:需要很多函数时
import { debounce, throttle, cloneDeep, isEqual, memoize } from 'lodash-es';

3. 迁移指南

从 lodash 迁移到 lodash-es

// 之前
const _ = require('lodash');
_.debounce(func, 300);

// 之后
import debounce from 'lodash-es/debounce';
debounce(func, 300);

// 或者批量替换
import { debounce, throttle, cloneDeep } from 'lodash-es';

九、常见坑与注意事项

1. Node.js 环境

// Node.js 需要启用 ES Module
// package.json
{
  "type": "module"  // 添加这一行
}

// 或���使用 .mjs 扩展名
import debounce from 'lodash-es/debounce.mjs';

2. TypeScript 配置

// tsconfig.json
{
  "compilerOptions": {
    "module": "esnext",      // 使用 ES Module
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true
  }
}

3. 浏览器直接使用

<!-- 需要支持 type="module" 的浏览器 -->
<script type="module">
  import debounce from 'https://unpkg.com/lodash-es/debounce.js';
  
  const debounced = debounce(() => {
    console.log('Hello from lodash-es!');
  }, 300);
</script>

4. 构建工具兼容性

  • Webpack 4+:原生支持
  • Rollup:原生支持
  • Vite:原生支持
  • Parcel:需要配置

十、总结

lodash 到 lodash-es 的升级,远不止是后缀变化:

  1. 模块化革命:从 CommonJS 大对象到 ES Module 独立文件
  2. 体积优化:Tree Shaking 让打包体积减少 90%+
  3. 性能提升:更快的导入速度,更好的缓存策略
  4. 未来兼容:ES Module 是 JavaScript 标准

迁移建议

  • 新项目直接使用 lodash-es
  • 老项目逐步迁移,从高频函数开始
  • 配合构建工具,最大化 Tree Shaking 效果

最后提醒:lodash-es 不是银弹,如果项目需要大量 lodash 函数,直接导入整个库可能更合适。但对于大多数现代前端项目,lodash-es + Tree Shaking 是最佳选择。

如果对你有用,欢迎点赞、收藏、关注! 下一篇我们将深入分析 antd 组件的源码实现。

参考资料

antd 组件也做了同款效果!深入源码看设计模式在前端组件库的应用

一、这东西是什么

antd(Ant Design)是阿里巴巴开源的 React UI 组件库,提供丰富的企业级 UI 组件。但 antd 的价值不止于组件本身,更在于其优秀的设计模式应用

核心观点:antd 和 lodash-es 虽然领域不同,但都应用了相似的设计模式:

  • 模块化设计:组件独立,支持按需引入
  • 组合模式:通过 props 组合实现复杂功能
  • 装饰器模式:高阶组件增强功能
  • 工厂模式:统一创建相似组件

二、这东西有什么用

适用场景

  • React 项目开发
  • 需要高质量 UI 组件的企业应用
  • 学习前端设计模式的开发者
  • 需要自定义组件库的团队

能带来什么收益

  1. 代码复用:减少重复代码,提高开发效率
  2. 可维护性:清晰的架构让代码更易维护
  3. 一致性:统一的设计模式保证代码风格一致
  4. 扩展性:易于添加新功能或修改现有功能

三、官方链接

四、从源码看设计模式

1. 模块化设计(与 lodash-es 同款)

// antd 的模块化结构
// 每个组件独立目录,支持按需引入
import { Button, Modal, Form } from 'antd';

// 或者按需引入特定组件
import Button from 'antd/es/button';
import Modal from 'antd/es/modal';

源码结构

antd/
├── es/                    # ES Module 版本
│   ├── button/
│   │   ├── index.js      # 入口文件
│   │   ├── button.js     # 主组件
│   │   └── style/        # 样式文件
│   ├── modal/
│   └── ...
├── lib/                  # CommonJS 版本
└── dist/                 # UMD 版本

2. 组合模式(Composition Pattern)

antd 的 Form 组件是组合模式的典型应用:

// antd Form 组件使用组合模式
import { Form, Input, Button } from 'antd';

const MyForm = () => (
  <Form>
    <Form.Item name="username" rules={[{ required: true }]}>
      <Input placeholder="用户名" />
    </Form.Item>
    <Form.Item name="password" rules={[{ required: true }]}>
      <Input.Password placeholder="密码" />
    </Form.Item>
    <Form.Item>
      <Button type="primary" htmlType="submit">
        提交
      </Button>
    </Form.Item>
  </Form>
);

源码分析:Form.Item 作为容器,组合了表单控件和验证逻辑。

3. 装饰器模式(Decorator Pattern)

antd 使用高阶组件(HOC)实现装饰器模式:

// antd 的 withConfigConsumer 高阶组件
import { ConfigConsumer } from '../config-provider/context';

function withConfigConsumer(config) {
  return function withConfigConsumerFunc(Component) {
    return function WrappedComponent(props) {
      return (
        <ConfigConsumer>
          {context => <Component {...config} {...props} {...context} />}
        </ConfigConsumer>
      );
    };
  };
}

// 使用装饰器增强组件
const EnhancedButton = withConfigConsumer({
  prefixCls: 'ant-btn'
})(Button);

4. 工厂模式(Factory Pattern)

antd 的 notification 组件使用工厂模式:

// notification 工厂函数
import Notification from './notification';

// 创建不同类型的通知
const notification = {
  success: (config) => Notification.success(config),
  error: (config) => Notification.error(config),
  info: (config) => Notification.info(config),
  warning: (config) => Notification.warning(config),
  open: (config) => Notification.open(config),
};

// 使用
notification.success({
  message: '操作成功',
  description: '数据已保存',
});

五、如何做一个 demo 出来

1. 环境要求

  • Node.js 14+
  • React 16.8+
  • TypeScript(可选)

2. 安装命令

# 创建 React 项目
npx create-react-app antd-pattern-demo --template typescript

# 安装 antd
cd antd-pattern-demo
npm install antd

# 安装分析工具
npm install --save-dev @types/react @types/react-dom

3. 目录结构说明

antd-pattern-demo/
├── src/
│   ├── components/
│   │   ├── MyButton/      # 自定义按钮组件
│   │   ├── MyForm/        # 自定义表单组件
│   │   └── MyModal/       # 自定义弹窗组件
│   ├── patterns/          # 设计模式示例
│   │   ├── composition/   # 组合模式
│   │   ├── decorator/     # 装饰器模式
│   │   └── factory/       # 工厂模式
│   ├── App.tsx
│   └── index.tsx
├── package.json
└── tsconfig.json

4. 最小可运行示例

组合模式示例

// src/patterns/composition/FormDemo.tsx
import React from 'react';
import { Form, Input, Button, Select } from 'antd';

const { Option } = Select;

const FormDemo: React.FC = () => {
  const onFinish = (values: any) => {
    console.log('表单值:', values);
  };

  return (
    <Form
      name="basic"
      initialValues={{ remember: true }}
      onFinish={onFinish}
      layout="vertical"
      {/* 组合 Input 和验证规则 */}
      <Form.Item
        label="用户名"
        name="username"
        rules={[
          { required: true, message: '请输入用户名' },
          { min: 3, message: '至少3个字符' }
        ]}
        <Input placeholder="请输入用户名" />
      </Form.Item>

      {/* 组合 Select 和选项 */}
      <Form.Item
        label="角色"
        name="role"
        rules={[{ required: true, message: '请选择角色' }]}
        <Select placeholder="请选择角色">
          <Option value="admin">管理员</Option>
          <Option value="user">普通用户</Option>
          <Option value="guest">访客</Option>
        </Select>
      </Form.Item>

      {/* 组合 Button 和提交逻辑 */}
      <Form.Item>
        <Button type="primary" htmlType="submit">
          提交
        </Button>
      </Form.Item>
    </Form>
  );
};

export default FormDemo;

装饰器模式示例

// src/patterns/decorator/withLoading.tsx
import React, { ComponentType, useState, useEffect } from 'react';
import { Spin } from 'antd';

// 高阶组件:为组件添加加载状态
function withLoading<P extends object>(
  WrappedComponent: ComponentType<P>
): React.FC<P & { isLoading?: boolean }> {
  return function WithLoadingComponent(props) {
    const [loading, setLoading] = useState(props.isLoading || false);

    // 模拟异步加载
    useEffect(() => {
      if (props.isLoading) {
        setLoading(true);
        const timer = setTimeout(() => {
          setLoading(false);
        }, 2000);
        return () => clearTimeout(timer);
      }
    }, [props.isLoading]);

    if (loading) {
      return (
        <div style={{ padding: '50px', textAlign: 'center' }}>
          <Spin size="large" />
          <div style={{ marginTop: '16px' }}>加载中...</div>
        </div>
      );
    }

    return <WrappedComponent {...props as P} />;
  };
}

// 使用装饰器
const UserList: React.FC<{ users: string[] }> = ({ users }) => (
  <ul>
    {users.map((user, index) => (
      <li key={index}>{user}</li>
    ))}
  </ul>
);

const UserListWithLoading = withLoading(UserList);

// 在组件中使用
const App: React.FC = () => {
  const users = ['张三', '李四', '王五'];
  
  return (
    <div>
      <h2>用户列表(带加载效果)</h2>
      <UserListWithLoading users={users} isLoading={true} />
    </div>
  );
};

工厂模式示例

// src/patterns/factory/NotificationFactory.tsx
import React from 'react';
import { Button, Space } from 'antd';
import { notification } from 'antd';

// 通知工厂
class NotificationFactory {
  static create(type: 'success' | 'error' | 'info' | 'warning', config: any) {
    const methods = {
      success: notification.success,
      error: notification.error,
      info: notification.info,
      warning: notification.warning,
    };

    return methods[type]({
      duration: 3,
      placement: 'topRight',
      ...config,
    });
  }

  // 预定义的通知类型
  static success(message: string, description?: string) {
    return this.create('success', { message, description });
  }

  static error(message: string, description?: string) {
    return this.create('error', { message, description });
  }

  static info(message: string, description?: string) {
    return this.create('info', { message, description });
  }

  static warning(message: string, description?: string) {
    return this.create('warning', { message, description });
  }
}

// 使用工厂
const NotificationDemo: React.FC = () => {
  const showNotification = (type: 'success' | 'error' | 'info' | 'warning') => {
    const messages = {
      success: '操作成功!',
      error: '操作失败!',
      info: '这是提示信息',
      warning: '请注意警告',
    };

    NotificationFactory[type](messages[type], '详细描述信息');
  };

  return (
    <Space>
      <Button type="primary" onClick={() => showNotification('success')}>
        成功通知
      </Button>
      <Button danger onClick={() => showNotification('error')}>
        错误通知
      </Button>
      <Button onClick={() => showNotification('info')}>
        信息通知
      </Button>
      <Button type="dashed" onClick={() => showNotification('warning')}>
        警告通知
      </Button>
    </Space>
  );
};

5. 运行项目

# 启动开发服务器
npm start

# 访问 http://localhost:3000

六、设计模式在前端开发中的应用场景

1. 组合模式(Composition)

适用场景

  • 表单组件(Form + Form.Item + Input)
  • 布局组件(Layout + Header + Content + Footer)
  • 导航菜单(Menu + Menu.Item + SubMenu)

antd 源码示例

// antd/es/form/Form.tsx
const Form: React.FC<FormProps> = (props) => {
  return (
    <FormProvider>
      <FormContext.Provider value={formContextValue}>
        <FormComponent {...props} />
      </FormContext.Provider>
    </FormProvider>
  );
};

// Form.Item 作为子组件
Form.Item = FormItem;

2. 装饰器模式(Decorator)

适用场景

  • 权限控制(withAuth)
  • 数据加载(withLoading)
  • 错误处理(withErrorBoundary)
  • 样式增强(withStyles)

antd 源码示例

// antd/es/config-provider/context.tsx
export const ConfigConsumer = ConfigContext.Consumer;

// 使用 ConfigConsumer 装饰组件
export function withConfigConsumer<C extends React.ComponentType<any>>(
  config: ConsumerConfig
) {
  return function withConfigConsumerFunc(
    Component: C
  ): React.ComponentType<any> {
    // 返回装饰后的组件
    return (props: any) => (
      <ConfigConsumer>
        {(context) => (
          <Component {...config} {...props} {...context} />
        )}
      </ConfigConsumer>
    );
  };
}

3. 工厂模式(Factory)

适用场景

  • 创建不同类型的弹窗(Modal.success/error/info)
  • 创建不同类型的消息(message.success/error)
  • 创建不同类型的通知(notification.success/error)

antd 源码示例

// antd/es/modal/confirm.tsx
export default function confirm(config: ModalFuncProps) {
  // 创建确认对话框的工厂函数
  const div = document.createElement('div');
  document.body.appendChild(div);
  
  let currentConfig = { ...config, close, visible: true };
  
  function destroy() {
    // 销毁逻辑
  }
  
  function render(props: any) {
    // 渲染逻辑
  }
  
  function update(newConfig: ModalFuncProps) {
    // 更新逻辑
  }
  
  function close() {
    // 关闭逻辑
  }
  
  render(currentConfig);
  
  return {
    destroy: close,
    update,
  };
}

// 工厂方法
Modal.confirm = (props: ModalFuncProps) => confirm(props);
Modal.success = (props: ModalFuncProps) => confirm({ ...props, icon: <CheckCircleOutlined /> });
Modal.error = (props: ModalFuncProps) => confirm({ ...props, icon: <CloseCircleOutlined /> });

七、性能优化与最佳实践

1. 按需引入(与 lodash-es 同款)

// 推荐:按需引入
import Button from 'antd/es/button';
import Form from 'antd/es/form';
import 'antd/es/button/style';
import 'antd/es/form/style';

// 不推荐:全量引入
import { Button, Form } from 'antd';
import 'antd/dist/antd.css';

2. 使用 babel-plugin-import

// .babelrc 或 babel.config.js
{
  "plugins": [
    ["import", {
      "libraryName": "antd",
      "libraryDirectory": "es",
      "style": true
    }]
  ]
}

// 现在可以这样写,插件会自动转换
import { Button } from 'antd';
// 转换为 ↓
import Button from 'antd/es/button';
import 'antd/es/button/style';

3. 组件性能优化

// 使用 React.memo 避免不必要的重渲染
import React, { memo } from 'react';
import { Button } from 'antd';

const MyButton = memo(({ onClick, children }) => {
  console.log('MyButton 渲染');
  return <Button onClick={onClick}>{children}</Button>;
});

// 使用 useCallback 缓存函数
const App = () => {
  const handleClick = useCallback(() => {
    console.log('按钮点击');
  }, []);
  
  return <MyButton onClick={handleClick}>点击我</MyButton>;
};

八、与 lodash-es 的对比分析

特性 lodash-es antd 共同点
模块化 ES Module 独立文件 ES Module 独立组件 都支持按需引入
Tree Shaking 支持 支持 都依赖静态分析
设计模式 函数式编程 面向对象设计模式 都注重代码组织
使用场景 工具函数 UI 组件 都提供高质量代码

核心相似点:都通过优秀的架构设计,解决了代码复用性能优化的问题。

九、常见坑与注意事项

1. 样式问题

// 错误:忘记引入样式
import { Button } from 'antd';
// 缺少:import 'antd/es/button/style';

// 正确:使用 babel-plugin-import 或手动引入
import Button from 'antd/es/button';
import 'antd/es/button/style';

2. 版本兼容性

// package.json
{
  "dependencies": {
    "antd": "^4.0.0",  // 注意主版本号
    "react": "^16.8.0", // 需要 React 16.8+
    "react-dom": "^16.8.0"
  }
}

3. TypeScript 配置

// tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  }
}

4. 自定义主题

// craco.config.js(Create React App)
const CracoLessPlugin = require('craco-less');

module.exports = {
  plugins: [
    {
      plugin: CracoLessPlugin,
      options: {
        lessLoaderOptions: {
          lessOptions: {
            modifyVars: {
              '@primary-color': '#1DA57A', // ��改主题色
            },
            javascriptEnabled: true,
          },
        },
      },
    },
  ],
};

十、总结

antd 和 lodash-es 虽然解决不同问题,但都体现了优秀前端库的共同特点:

  1. 模块化设计:支持按需引入,减少打包体积
  2. 设计模式应用:组合、装饰器、工厂等模式提升代码质量
  3. 性能优化:Tree Shaking、Memoization 等技术
  4. 开发者体验:清晰的 API、完整的文档、TypeScript 支持

学习建议

  1. 阅读优秀开源库的源码,理解设计思想
  2. 在实际项目中应用设计模式
  3. 关注性能优化,特别是打包体积
  4. 保持代码的可维护性和可扩展性

最后:优秀的前端工程师不仅要会使用工具,更要理解工具背后的设计思想。antd 和 lodash-es 都是学习前端架构的绝佳教材。

如果对你有用,欢迎点赞、收藏、关注! 下一篇我们将深入分析 Vue KeepAlive 的源码实现。

参考资料

xx.d.ts 文件有什么用,为什么不引入都能生效?


一、从一个现象说起

你有没有遇到过这种情况:

// 没有任何 import
const app = express();  // ✅ 类型正常
const router = Router();  // ✅ 类型正常

// 但是这些类型是哪来的?

打开 node_modules/@types/express/index.d.ts,发现:

declare function express(): Express.Application;
declare namespace Express {
  interface Application {}
}

疑问: 1. 为什么不需要 import 就能用? 2. declare 关键字是什么意思? 3. .d.ts 文件是如何工作的?

今天就来彻底搞懂这些问题。

二、.d.ts 文件是什么

2.1 定义

.d.ts 文件是 TypeScript 的类型声明文件(Type Declaration File),用于描述 JavaScript 代码的类型信息。

作用: - 为 JavaScript 库提供类型定义 - 让 TypeScript 理解 JavaScript 代码 - 提供代码提示和类型检查

2.2 为什么需要 .d.ts?

JavaScript 本身没有类型信息:

// math.js
export function add(a, b) {
  return a + b;
}

TypeScript 不知道 add 的参数和返回值类型:

import { add } from './math';
add(1, 2);  // ❌ TypeScript 不知道类型

解决方案:创建 .d.ts 文件

// math.d.ts
export function add(a: number, b: number): number;

现在 TypeScript 就知道类型了:

import { add } from './math';
add(1, 2);  // ✅ 类型正确
add('1', '2');  // ❌ 类型错误

三、declare 关键字

3.1 declare 的作用

declare 告诉 TypeScript:「这个东西在运行时存在,但我只是声明它的类型,不提供实现」。

// 声明全局变量
declare const API_URL: string;

// 声明全局函数
declare function fetchData(url: string): Promise<any>;

// 声明全局类
declare class User {
  name: string;
  age: number;
}

使用时不需要 import

// 直接使用,TypeScript 知道类型
console.log(API_URL);  // ✅
fetchData('/api/users');  // ✅
const user = new User();  // ✅

3.2 declare 的场景

场景 1:全局变量

// global.d.ts
declare const __DEV__: boolean;
declare const process: {
  env: {
    NODE_ENV: string;
  };
};

// 使用
if (__DEV__) {
  console.log('Development mode');
}

场景 2:第三方库

// jquery.d.ts
declare const $: {
  (selector: string): any;
  ajax(options: any): any;
};

// 使用
$('#app').hide();
$.ajax({ url: '/api' });

场景 3:模块扩展

// express.d.ts
declare namespace Express {
  interface Request {
    user?: User;
  }
}

// 使用
app.get('/', (req, res) => {
  console.log(req.user);  // ✅ TypeScript 知道 user 属性
});

四、为什么不需要 import?

4.1 全局声明 vs 模块声明

.d.ts 文件有两种模式

模式 1:全局声明(没有 import/export)

// global.d.ts
declare const API_URL: string;
declare function fetchData(url: string): Promise<any>;

这些声明是全局的,不需要 import 就能用。

模式 2:模块声明(有 import/export)

// types.d.ts
export interface User {
  name: string;
  age: number;
}

export function getUser(id: string): Promise<User>;

这些声明需要 import 才能用:

import { User, getUser } from './types';

4.2 TypeScript 如何找到 .d.ts 文件?

TypeScript 会自动查找 .d.ts 文件:

查找顺序

1. 项目根目录*.d.ts 2. src 目录src/**/*.d.ts 3. node_modules/@typesnode_modules/@types/*/index.d.ts 4. tsconfig.json 的 types:指定的类型包

// tsconfig.json
{
  "compilerOptions": {
    "types": ["node", "jest", "express"]
  }
}

4.3 自动包含的 .d.ts 文件

TypeScript 会自动包含:

project/
├── src/
│   ├── index.ts
│   └── types.d.ts        # ✅ 自动包含
├── global.d.ts           # ✅ 自动包含
└── node_modules/
    └── @types/
        ├── node/         # ✅ 自动包含
        └── express/      # ✅ 自动包含

不需要手动 import,TypeScript 会自动加载。

五、实战案例

5.1 为第三方库添加类型

假设你用了一个没有类型定义的库:

// awesome-lib.js
export function doSomething(value) {
  return value * 2;
}

创建类型声明:

// awesome-lib.d.ts
declare module 'awesome-lib' {
  export function doSomething(value: number): number;
}

现在可以安全使用:

import { doSomething } from 'awesome-lib';
doSomething(5);  // ✅ 类型正确
doSomething('5');  // ❌ 类型错误

5.2 扩展全局对象

// global.d.ts
declare global {
  interface Window {
    myApp: {
      version: string;
      init(): void;
    };
  }
}

export {};  // 让文件成为模块

使用:

window.myApp.version;  // ✅
window.myApp.init();   // ✅

5.3 环境变量类型

// env.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    NODE_ENV: 'development' | 'production' | 'test';
    API_URL: string;
    API_KEY: string;
  }
}

使用:

const env = process.env.NODE_ENV;  // ✅ 类型是 'development' | 'production' | 'test'
const url = process.env.API_URL;   // ✅ 类型是 string

5.4 React 组件 Props

// components.d.ts
declare namespace JSX {
  interface IntrinsicElements {
    'my-component': {
      value: string;
      onChange: (value: string) => void;
    };
  }
}

使用:

<my-component value="hello" onChange={(v) => console.log(v)} />

六、常见问题

6.1 .d.ts 文件不生效?

原因 1:文件不在 TypeScript 的查找路径

// tsconfig.json
{
  "include": ["src/**/*"],  // 只包含 src 目录
  "exclude": ["node_modules"]
}

解决:把 .d.ts 文件放在 src 目录下,或修改 include

原因 2:文件有 import/export,变成了模块

// global.d.ts
import { Something } from 'somewhere';  // ❌ 变成模块了

declare const API_URL: string;  // 不再是全局声明

解决:使用 declare global

import { Something } from 'somewhere';

declare global {
  const API_URL: string;
}

6.2 如何调试类型问题?

// 查看类型
type Test = typeof API_URL;  // 鼠标悬停查看

// 强制类型检查
const _check: string = API_URL;  // 如果类型不对会报错

6.3 .d.ts 和 .ts 的区别?

特性 .ts .d.ts
包含实现
包含类型
编译成 .js
自动全局 ✅(无 import/export 时)

七、最佳实践

7.1 组织 .d.ts 文件

project/
├── src/
│   ├── types/
│   │   ├── global.d.ts      # 全局类型
│   │   ├── modules.d.ts     # 模块扩展
│   │   └── env.d.ts         # 环境变量
│   └── index.ts
└── tsconfig.json

7.2 命名规范

// ✅ 好的命名
global.d.ts
env.d.ts
express.d.ts

// ❌ 不好的命名
types.d.ts  // 太泛化
index.d.ts  // 不清楚内容

7.3 注释文档

/**
 * 全局 API 配置
 * @example
 * ```ts
 * console.log(API_URL);  // 'https://api.example.com'
 * ```
 */
declare const API_URL: string;

八、总结

.d.ts 文件的核心概念

1. 类型声明文件:只有类型,没有实现 2. declare 关键字:声明类型,不提供实现 3. 全局 vs 模块:无 import/export 是全局,有则是模块 4. 自动加载:TypeScript 自动查找并加载

为什么不需要 import?

- 全局声明的 .d.ts 文件会被 TypeScript 自动加载 - TypeScript 会扫描项目和 node_modules/@types - 全局声明对整个项目可见

使用场景

- 为 JavaScript 库添加类型 - 声明全局变量和函数 - 扩展第三方库的类型 - 定义环境变量类型

最佳实践

- 全局类型放在 global.d.ts - 模块类型使用 export - 添加注释文档 - 合理组织文件结构

如果这篇文章对你有帮助,欢迎点赞收藏。

万字解析 OpenClaw 源码架构:从入门到精通


一、OpenClaw 项目概览

OpenClaw 是一个现代化的 Web 应用框架,专注于提供高性能、可扩展的全栈解决方案。

核心特点: - 全栈 TypeScript,类型安全 - Monorepo 架构,模块化设计 - 插件化系统,易于扩展 - 高性能运行时 - 完善的开发工具链

二、项目结构深度解析

2.1 Monorepo 架构

openclaw/
├── packages/              # 核心包
│   ├── core/             # 框架核心
│   ├── cli/              # 命令行工具
│   ├── server/           # 服务端
│   ├── client/           # 客户端
│   ├── router/           # 路由系统
│   ├── state/            # 状态管理
│   └── utils/            # 工具库
├── examples/             # 示例项目
├── docs/                 # 文档
└── scripts/              # 构建脚本

为什么选择 Monorepo?

1. 代码共享:包之间直接引用,无需发布 2. 统一版本:依赖版本一致 3. 原子提交:跨包修改一次提交 4. 统一工具链:共享配置

工具选择: - pnpm:快速、节省空间 - Turborepo:增量构建

// turbo.json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    }
  }
}

2.2 核心包架构

@openclaw/core

packages/core/
├── src/
│   ├── app/              # 应用实例
│   ├── middleware/       # 中间件
│   ├── plugin/           # 插件系统
│   └── lifecycle/        # 生命周期
└── types/                # 类型定义

三、核心模块实现

3.1 Application 类

export class Application {
  private plugins: Plugin[] = [];
  private middleware: Middleware[] = [];

  use(plugin: Plugin): this {
    this.plugins.push(plugin);
    plugin.install(this);
    return this;
  }

  middleware(fn: MiddlewareFunction): this {
    this.middleware.push(fn);
    return this;
  }

  async start(): Promise<void> {
    await this.runLifecycle('beforeStart');
    const composed = compose(this.middleware);
    await this.server.listen(this.options.port);
    await this.runLifecycle('afterStart');
  }
}

3.2 中间件系统(洋葱模型)

export function compose(middleware: Middleware[]): ComposedMiddleware {
  return function (context: Context, next?: Next) {
    let index = -1;

    function dispatch(i: number): Promise<void> {
      if (i <= index) {
        throw new Error('next() called multiple times');
      }
      index = i;
      const fn = middleware[i];
      if (!fn) return Promise.resolve();
      
      return Promise.resolve(fn(context, () => dispatch(i + 1)));
    }

    return dispatch(0);
  };
}

使用示例

app.middleware(async (ctx, next) => {
  console.log('Before');
  await next();
  console.log('After');
});

3.3 插件系统

export interface Plugin {
  name: string;
  install(app: Application): void;
  beforeStart?(context: Context): Promise<void>;
  afterStart?(context: Context): Promise<void>;
}

// 数据库插件示例
export const DatabasePlugin: Plugin = {
  name: 'database',
  
  install(app) {
    app.context.db = createDatabase();
  },
  
  async beforeStart(ctx) {
    await ctx.db.connect();
  }
};

四、路由系统设计

4.1 路由匹配

export class Router {
  private routes: Route[] = [];

  get(path: string, handler: RouteHandler): this {
    return this.register('GET', path, handler);
  }

  private register(method: string, path: string, handler: RouteHandler) {
    this.routes.push({
      method,
      path,
      handler,
      regex: pathToRegex(path)
    });
    return this;
  }

  match(method: string, path: string): RouteMatch | null {
    for (const route of this.routes) {
      if (route.method !== method) continue;
      const match = path.match(route.regex);
      if (match) return { route, params: extractParams(match) };
    }
    return null;
  }
}

路径转正则

function pathToRegex(path: string): RegExp {
  // /users/:id -> /users/([^/]+)
  const pattern = path
    .replace(/\//g, '\\/')
    .replace(/:(\w+)/g, '([^/]+)');
  return new RegExp(`^${pattern}$`);
}

4.2 嵌套路由

const apiRouter = new Router();
apiRouter.get('/users', getUsersHandler);

const app = new Application();
app.middleware(
  new Router().use('/api', apiRouter).middleware()
);
// 结果:GET /api/users

五、状态管理

5.1 Store 实现

export class Store<T> {
  private state: T;
  private listeners: Set<Listener<T>> = new Set();

  getState(): T {
    return this.state;
  }

  setState(updater: Updater<T>): void {
    const prevState = this.state;
    const nextState = typeof updater === 'function'
      ? updater(prevState)
      : updater;

    if (prevState === nextState) return;

    this.state = nextState;
    this.listeners.forEach(listener => {
      listener(nextState, prevState);
    });
  }

  subscribe(listener: Listener<T>): Unsubscribe {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }
}

5.2 选择器优化

export function createSelector<T, R>(
  selector: (state: T) => R
): Selector<T, R> {
  let lastState: T;
  let lastResult: R;

  return (state: T): R => {
    if (state === lastState) return lastResult;
    const result = selector(state);
    lastState = state;
    lastResult = result;
    return result;
  };
}

六、构建系统

6.1 Turborepo 配置

{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"],
      "cache": true
    },
    "test": {
      "dependsOn": ["build"],
      "cache": true
    }
  }
}

增量构建: - 只构建变化的包 - 缓存构建结果 - 并行执行任务

6.2 TypeScript 配置

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "composite": true,
    "declaration": true
  },
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/router" }
  ]
}

Project References: - 加快编译速度 - 增量编译 - 更好的类型检查

七、设计模式应用

7.1 工厂模式

export function createApplication(options: ApplicationOptions): Application {
  const app = new Application(options);
  
  // 注册默认插件
  app.use(LoggerPlugin);
  app.use(ErrorHandlerPlugin);
  
  return app;
}

7.2 观察者模式

// Store 的订阅机制
store.subscribe((state) => {
  console.log('State changed:', state);
});

7.3 责任链模式

// 中间件的洋葱模型
app.middleware(middleware1);
app.middleware(middleware2);
app.middleware(middleware3);

7.4 策略模式

// 路由匹配策略
interface MatchStrategy {
  match(path: string): boolean;
}

class ExactMatch implements MatchStrategy {
  match(path: string): boolean {
    return path === this.pattern;
  }
}

class RegexMatch implements MatchStrategy {
  match(path: string): boolean {
    return this.regex.test(path);
  }
}

八、性能优化

8.1 缓存策略

class CacheMiddleware {
  private cache = new Map<string, any>();

  middleware(): Middleware {
    return async (ctx, next) => {
      const key = ctx.request.url;
      
      if (this.cache.has(key)) {
        ctx.json(this.cache.get(key));
        return;
      }

      await next();

      if (ctx.response.status === 200) {
        this.cache.set(key, ctx.response.body);
      }
    };
  }
}

8.2 懒加载

// 路由懒加载
router.get('/admin', async (ctx) => {
  const { AdminController } = await import('./controllers/admin');
  return new AdminController().handle(ctx);
});

8.3 连接池

class DatabasePool {
  private pool: Connection[] = [];
  private maxSize = 10;

  async getConnection(): Promise<Connection> {
    if (this.pool.length > 0) {
      return this.pool.pop()!;
    }
    return await this.createConnection();
  }

  release(conn: Connection): void {
    if (this.pool.length < this.maxSize) {
      this.pool.push(conn);
    } else {
      conn.close();
    }
  }
}

九、测试策略

9.1 单元测试

describe('Router', () => {
  it('should match route', () => {
    const router = new Router();
    router.get('/users/:id', handler);

    const match = router.match('GET', '/users/123');
    expect(match).toBeDefined();
    expect(match.params.id).toBe('123');
  });
});

9.2 集成测试

describe('Application', () => {
  it('should handle request', async () => {
    const app = createApplication();
    app.middleware(async (ctx) => {
      ctx.json({ message: 'Hello' });
    });

    const response = await request(app)
      .get('/')
      .expect(200);

    expect(response.body.message).toBe('Hello');
  });
});

十、最佳实践

10.1 错误处理

app.middleware(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.response.status = err.status || 500;
    ctx.json({
      error: err.message,
      stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
    });
  }
});

10.2 日志记录

app.middleware(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.request.method} ${ctx.request.url} - ${ms}ms`);
});

10.3 安全防护

// CORS
app.middleware(async (ctx, next) => {
  ctx.response.setHeader('Access-Control-Allow-Origin', '*');
  await next();
});

// Rate Limiting
const limiter = new RateLimiter({ max: 100, window: 60000 });
app.middleware(limiter.middleware());

十一、总结

OpenClaw 的架构设计体现了现代 Web 框架的最佳实践:

核心特点: - Monorepo 架构,模块化设计 - 插件化系统,易于扩展 - 中间件洋葱模型,灵活组合 - TypeScript 类型安全 - 高性能运行时

设计模式: - 工厂模式:创建应用实例 - 观察者模式:状态订阅 - 责任链模式:中间件系统 - 策略模式:路由匹配

性能优化: - 缓存策略 - 懒加载 - 连接池 - 增量构建

通过深入理解 OpenClaw 的源码架构,你可以学到: - 如何设计一个可扩展的框架 - 如何实现高性能的运行时 - 如何组织大型项目的代码结构 - 如何应用设计模式解决实际问题

如果这篇文章对你有帮助,欢迎点赞收藏。

React Native 完全入门:从原理到实战


一、React Native 是什么

React Native(简称 RN)是 Facebook 开源的跨平台移动应用开发框架,让你用 JavaScript 和 React 语法编写原生 iOS 和 Android 应用。

核心特点:

- 真原生渲染:不是 WebView,而是调用原生 UI 组件 - 热更新:无需重新打包,线上修复 bug - 跨平台:一套代码,iOS 和 Android 共用 80%+ 逻辑 - React 生态:复用 React 的组件化、状态管理等能力

与其他方案对比:

方案 渲染方式 性能 开发体验
原生开发 原生 最优 需学 Swift/Kotlin
React Native 原生 接近原生 JavaScript + React
Flutter 自绘引擎 接近原生 Dart 语言
Hybrid(Cordova) WebView 较差 Web 技术栈

二、React Native 的工作原理

2.1 整体架构

┌─────────────────────────────────────┐
│      JavaScript 层(业务逻辑)       │
│      (React 组件、状态管理)          │
└──────────────┬──────────────────────┘
               │ Bridge(消息通信)
┌──────────────┴──────────────────────┐
│      Native 层(原生模块)           │
│      (UI 渲染、网络、存储等)         │
└─────────────────────────────────────┘

三层结构:

1. JavaScript 层:运行在 JavaScriptCore(iOS)或 Hermes(Android)引擎中,执行 React 代码 2. Bridge:JS 和 Native 之间的消息通道,传递 JSON 数据 3. Native 层:iOS 用 Objective-C/Swift,Android 用 Java/Kotlin,负责实际渲染和系统调用

2.2 渲染流程

// 1. 你写的 JSX
<View style={{ flex: 1 }}>
  <Text>Hello RN</Text>
</View>

// 2. React 转成虚拟 DOM
{
  type: 'View',
  props: { style: { flex: 1 } },
  children: [
    { type: 'Text', props: {}, children: ['Hello RN'] }
  ]
}

// 3. Bridge 传给 Native
{
  "type": "createView",
  "viewId": 1,
  "viewType": "RCTView",
  "props": { "flex": 1 }
}

// 4. Native 创建真实 UI
UIView *view = [[UIView alloc] init];  // iOS
// 或
View view = new View(context);         // Android

2.3 Bridge 通信

JS 调用 Native:

// JS 端
import { NativeModules } from 'react-native';
const { ToastModule } = NativeModules;

ToastModule.show('Hello', ToastModule.SHORT);

Native 实现(iOS):

// ToastModule.m
#import <React/RCTBridgeModule.h>

@interface ToastModule : NSObject <RCTBridgeModule>
@end

@implementation ToastModule

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(show:(NSString *)message duration:(NSInteger)duration) {
  dispatch_async(dispatch_get_main_queue(), ^{
    // 显示 Toast
  });
}

@end

Native 调用 JS:

// Native 端
[self.bridge enqueueJSCall:@"RCTDeviceEventEmitter"
                    method:@"emit"
                      args:@[@"onNetworkChange", @{@"type": @"wifi"}]
                completion:NULL];
// JS 端
import { NativeEventEmitter, NativeModules } from 'react-native';

const eventEmitter = new NativeEventEmitter(NativeModules.ToastModule);
eventEmitter.addListener('onNetworkChange', (event) => {
  console.log('网络变化:', event.type);
});

三、从零搭建 React Native 项目

3.1 环境准备

安装依赖:

# macOS(开发 iOS 需要)
brew install node watchman
sudo gem install cocoapods

# 安装 React Native CLI
npm install -g react-native-cli

安装 Xcode(iOS)或 Android Studio(Android)。

3.2 创建项目

npx react-native init MyApp
cd MyApp

目录结构:

MyApp/
├── android/          # Android 原生代码
├── ios/              # iOS 原生代码
├── node_modules/
├── App.tsx           # 入口组件
├── index.js          # 注册入口
├── package.json
└── metro.config.js   # 打包配置

3.3 运行项目

# iOS
npx react-native run-ios

# Android(需先启动模拟器或连接真机)
npx react-native run-android

四、核心组件

4.1 View 和 Text

import { View, Text, StyleSheet } from 'react-native';

function App() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Hello React Native</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f5f5f5'
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#333'
  }
});

4.2 Image

<Image
  source={{ uri: 'https://example.com/image.png' }}
  style={{ width: 200, height: 200 }}
  resizeMode="cover"
/>

// 本地图片
<Image source={require('./assets/logo.png')} />

4.3 ScrollView 和 FlatList

// ScrollView:适合少量数据
<ScrollView>
  {data.map(item => <Text key={item.id}>{item.name}</Text>)}
</ScrollView>

// FlatList:适合长列表,支持虚拟滚动
<FlatList
  data={data}
  keyExtractor={item => item.id}
  renderItem={({ item }) => <Text>{item.name}</Text>}
  onEndReached={loadMore}
  onEndReachedThreshold={0.5}
/>

4.4 TouchableOpacity

<TouchableOpacity
  onPress={() => console.log('点击')}
  activeOpacity={0.7}
>
  <Text>点我</Text>
</TouchableOpacity>

五、样式与布局

5.1 Flexbox 布局

RN 默认使用 Flexbox,但有些差异:

// 默认 flexDirection 是 column(Web 是 row)
<View style={{ flexDirection: 'row' }}>
  <View style={{ flex: 1, backgroundColor: 'red' }} />
  <View style={{ flex: 2, backgroundColor: 'blue' }} />
</View>

5.2 尺寸单位

RN 没有 pxrem,只有无单位数字(对应设备独立像素 dp/pt):

<View style={{ width: 100, height: 50 }} />

5.3 响应式布局

import { Dimensions } from 'react-native';

const { width, height } = Dimensions.get('window');

<View style={{ width: width * 0.8 }} />

六、手写简化版 React Native

6.1 核心思路

1. 解析 JSX 生成虚拟 DOM 2. 遍历虚拟 DOM,生成 Native 指令 3. 通过 Bridge 发送给 Native 4. Native 创建真实 UI

6.2 虚拟 DOM 转指令

function renderToNative(vdom, parentId = 0) {
  const viewId = generateId();
  const instructions = [];

  // 创建视图指令
  instructions.push({
    type: 'createView',
    viewId,
    viewType: vdom.type,  // 'View', 'Text' 等
    parentId,
    props: vdom.props
  });

  // 递归处理子节点
  if (vdom.children) {
    vdom.children.forEach(child => {
      if (typeof child === 'string') {
        // 文本节点
        instructions.push({
          type: 'updateText',
          viewId,
          text: child
        });
      } else {
        instructions.push(...renderToNative(child, viewId));
      }
    });
  }

  return instructions;
}

6.3 Bridge 实现

class Bridge {
  constructor() {
    this.queue = [];
  }

  // JS 调用 Native
  callNative(module, method, args) {
    this.queue.push({ module, method, args });
    this.flush();
  }

  // 批量发送
  flush() {
    if (this.queue.length === 0) return;

    const batch = this.queue.splice(0);
    // 实际会调用 Native 的 C++ 接口
    window.__nativeBridge.processBatch(JSON.stringify(batch));
  }

  // Native 调用 JS
  invokeCallback(callbackId, args) {
    const callback = this.callbacks[callbackId];
    if (callback) callback(...args);
  }
}

6.4 Native 端处理(伪代码)

// iOS 端
class NativeBridge {
  func processBatch(_ json: String) {
    let batch = JSON.parse(json)
    
    for instruction in batch {
      switch instruction.type {
      case "createView":
        let view = createView(instruction.viewType)
        view.tag = instruction.viewId
        applyProps(view, instruction.props)
        parentView.addSubview(view)
        
      case "updateText":
        let label = viewRegistry[instruction.viewId] as! UILabel
        label.text = instruction.text
      }
    }
  }
}

6.5 完整示例

// 1. JSX
const App = () => (
  <View style={{ flex: 1 }}>
    <Text>Hello</Text>
  </View>
);

// 2. 转虚拟 DOM
const vdom = {
  type: 'View',
  props: { style: { flex: 1 } },
  children: [
    { type: 'Text', props: {}, children: ['Hello'] }
  ]
};

// 3. 生成指令
const instructions = renderToNative(vdom);
// [
//   { type: 'createView', viewId: 1, viewType: 'View', props: {...} },
//   { type: 'createView', viewId: 2, viewType: 'Text', parentId: 1 },
//   { type: 'updateText', viewId: 2, text: 'Hello' }
// ]

// 4. 发送给 Native
bridge.callNative('UIManager', 'createView', instructions);

七、常用库与生态

7.1 导航

npm install @react-navigation/native @react-navigation/stack
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';

const Stack = createStackNavigator();

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Detail" component={DetailScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

7.2 状态管理

npm install zustand
import create from 'zustand';

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 }))
}));

function Counter() {
  const { count, increment } = useStore();
  return <Text onPress={increment}>{count}</Text>;
}

7.3 网络请求

fetch('https://api.example.com/data')
  .then(res => res.json())
  .then(data => console.log(data));

// 或使用 axios
import axios from 'axios';
const { data } = await axios.get('/api/data');

八、性能优化

8.1 避免不必要的渲染

import { memo } from 'react';

const ListItem = memo(({ item }) => (
  <Text>{item.name}</Text>
));

8.2 使用 FlatList 而非 ScrollView

// ❌ 差
<ScrollView>
  {data.map(item => <Item key={item.id} />)}
</ScrollView>

// ✅ 好
<FlatList
  data={data}
  renderItem={({ item }) => <Item item={item} />}
/>

8.3 图片优化

<Image
  source={{ uri: url }}
  style={{ width: 200, height: 200 }}
  resizeMode="cover"
  // 启用缓存
  cache="force-cache"
/>

九、调试技巧

9.1 开发者菜单

模拟器中按 Cmd + D(iOS)或 Cmd + M(Android)打开菜单,可以:

- Reload:重新加载 - Debug:打开 Chrome DevTools - Show Inspector:查看元素

9.2 日志

console.log('普通日志');
console.warn('警告');
console.error('错误');

9.3 Flipper

Facebook 官方调试工具,支持网络、布局、日志等:

brew install flipper

十、打包发布

10.1 iOS

# 1. 打开 Xcode
open ios/MyApp.xcworkspace

# 2. 选择 Generic iOS Device
# 3. Product -> Archive
# 4. 上传到 App Store Connect

10.2 Android

# 1. 生成签名密钥
keytool -genkey -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000

# 2. 配置 android/gradle.properties
MYAPP_RELEASE_STORE_FILE=my-release-key.keystore
MYAPP_RELEASE_KEY_ALIAS=my-key-alias
MYAPP_RELEASE_STORE_PASSWORD=***
MYAPP_RELEASE_KEY_PASSWORD=***

# 3. 打包
cd android
./gradlew assembleRelease

# 4. APK 在 android/app/build/outputs/apk/release/

总结

React Native 让你用 JavaScript 写原生应用,核心原理:

- JS 层运行 React 代码,生成虚拟 DOM - Bridge 传递 JSON 指令给 Native - Native 层创建真实 UI 组件

关键要点:

- 使用 View、Text、Image 等基础组件 - Flexbox 布局,默认 flexDirection: column - FlatList 处理长列表 - React Navigation 做路由 - 通过 NativeModules 调用原生能力

适合快速开发跨平台应用,性能接近原生。

前端哨兵模式(Sentinel Pattern):优雅实现无限滚动加载


一、什么是哨兵模式

在实现无限滚动加载时,传统做法是监听 scroll 事件,计算滚动位置、元素高度、视口距离等,代码复杂且性能开销大。哨兵模式(Sentinel Pattern)换了个思路:在列表底部放一个「哨兵元素」,当它进入视口时触发加载,利用浏览器原生的 Intersection Observer API 实现,代码简洁、性能更好。

核心思想:用一个不可见的 DOM 元素作为触发器,浏览器帮你监测它是否可见,你只需关注「可见时做什么」。

二、为什么选择哨兵模式

传统 scroll 方案的痛点:

- 需要手动计算 scrollTopscrollHeightclientHeight - 高频触发事件,即使加了节流也有性能损耗 - 代码可读性差,维护成本高

哨兵模式的优势:

- 浏览器原生 API 支持,性能优化由浏览器完成 - 代码量少,逻辑清晰 - 支持多个哨兵(比如顶部加载更多、底部加载更多) - 自动处理元素进入/离开视口的状态

三、基础实现

3.1 HTML 结构

<div class="list-container">
  <div class="list-item">Item 1</div>
  <div class="list-item">Item 2</div>
  <!-- 更多列表项 -->
  
  <!-- 哨兵元素 -->
  <div class="sentinel" id="sentinel"></div>
</div>

<div class="loading">加载中...</div>

3.2 CSS 样式

.sentinel {
  height: 1px;
  /* 不可见但占据空间 */
  visibility: hidden;
}

.loading {
  display: none;
  text-align: center;
  padding: 20px;
}

.loading.active {
  display: block;
}

3.3 JavaScript 核心逻辑

let page = 1;
let isLoading = false;

// 创建观察器
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    // 哨兵进入视口且未在加载中
    if (entry.isIntersecting && !isLoading) {
      loadMore();
    }
  });
}, {
  // 提前 100px 触发
  rootMargin: '100px'
});

// 开始观察哨兵元素
const sentinel = document.getElementById('sentinel');
observer.observe(sentinel);

// 加载更多数据
async function loadMore() {
  isLoading = true;
  document.querySelector('.loading').classList.add('active');
  
  try {
    const data = await fetchData(page);
    renderItems(data);
    page++;
  } catch (error) {
    console.error('加载失败', error);
  } finally {
    isLoading = false;
    document.querySelector('.loading').classList.remove('active');
  }
}

// 模拟数据获取
async function fetchData(page) {
  const response = await fetch(`/api/items?page=${page}`);
  return response.json();
}

// 渲染列表项
function renderItems(items) {
  const container = document.querySelector('.list-container');
  const sentinel = document.getElementById('sentinel');
  
  items.forEach(item => {
    const div = document.createElement('div');
    div.className = 'list-item';
    div.textContent = item.title;
    // 插入到哨兵之前
    container.insertBefore(div, sentinel);
  });
}

四、React 实现

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

function InfiniteList() {
  const [items, setItems] = useState([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const sentinelRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && !loading && hasMore) {
          loadMore();
        }
      },
      { rootMargin: '100px' }
    );

    if (sentinelRef.current) {
      observer.observe(sentinelRef.current);
    }

    return () => observer.disconnect();
  }, [loading, hasMore]);

  const loadMore = async () => {
    setLoading(true);
    try {
      const response = await fetch(`/api/items?page=${page}`);
      const data = await response.json();
      
      if (data.length === 0) {
        setHasMore(false);
      } else {
        setItems(prev => [...prev, ...data]);
        setPage(prev => prev + 1);
      }
    } catch (error) {
      console.error('加载失败', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      {items.map((item, index) => (
        <div key={index} className="list-item">
          {item.title}
        </div>
      ))}
      
      {/* 哨兵元素 */}
      <div ref={sentinelRef} style={{ height: 1, visibility: 'hidden' }} />
      
      {loading && <div className="loading">加载中...</div>}
      {!hasMore && <div className="no-more">没有更多了</div>}
    </div>
  );
}

五、进阶技巧

5.1 双向加载

同时支持向上和向下加载:

// 顶部哨兵
const topSentinel = document.getElementById('top-sentinel');
const bottomSentinel = document.getElementById('bottom-sentinel');

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.target === topSentinel && entry.isIntersecting) {
      loadPrevious(); // 加载上一页
    }
    if (entry.target === bottomSentinel && entry.isIntersecting) {
      loadNext(); // 加载下一页
    }
  });
});

observer.observe(topSentinel);
observer.observe(bottomSentinel);

5.2 虚拟滚动结合

对于超长列表,结合虚拟滚动优化性能:

// 只渲染可见区域 + 缓冲区的元素
const visibleItems = items.slice(startIndex, endIndex);

return (
  <div style={{ height: totalHeight }}>
    <div style={{ transform: `translateY(${offsetY}px)` }}>
      {visibleItems.map(item => (
        <div key={item.id}>{item.title}</div>
      ))}
    </div>
    <div ref={sentinelRef} />
  </div>
);

5.3 错误重试

加载失败时显示重试按钮:

const [error, setError] = useState(null);

const loadMore = async () => {
  setLoading(true);
  setError(null);
  
  try {
    // 加载逻辑
  } catch (err) {
    setError(err.message);
  } finally {
    setLoading(false);
  }
};

// 渲染
{error && (
  <div className="error">
    加载失败:{error}
    <button onClick={loadMore}>重试</button>
  </div>
)}

六、注意事项

1. 防止重复触发:用 isLoading 标志位避免并发请求 2. 提前加载:通过 rootMargin 设置提前触发距离,提升体验 3. 结束判断:后端返回空数组时停止观察,避免无效请求 4. 内存泄漏:组件卸载时记得 observer.disconnect() 5. 兼容性:Intersection Observer 支持 IE11+,旧浏览器需 polyfill

七、与其他方案对比

方案 性能 代码复杂度 兼容性
scroll 事件
Intersection Observer IE11+
第三方库(react-infinite-scroll)

哨兵模式适合现代浏览器环境,追求性能和代码简洁的场景。

总结

哨兵模式用一个不可见元素作为触发器,配合 Intersection Observer API 实现无限滚动,代码简洁、性能优秀。核心要点:

- 在列表底部放置哨兵元素 - 用 IntersectionObserver 监听其可见性 - 可见时触发加载,加防重复逻辑 - 支持双向加载、虚拟滚动等进阶场景

比传统 scroll 方案更优雅,推荐在新项目中使用。

ahooks useRequest 深度解析:一个 Hook 搞定所有请求

二、核心功能详解

1. 自动管理请求状态

const { data, loading, error, run, refresh, cancel } = useRequest(
  fetchUserList,
  {
    manual: false,  // 自动执行
    defaultParams: [{ page: 1 }],  // 默认参数
  }
);

// run: 手动触发
// refresh: 使用上次参数重新请求
// cancel: 取消当前请求

2. 防抖与节流

// 搜索场景:防抖
const { data, loading } = useRequest(searchAPI, {
  debounceWait: 300,  // 300ms 防抖
  manual: true,
});

// 滚动加载:节流
const { run } = useRequest(loadMore, {
  throttleWait: 1000,  // 1s 节流
  manual: true,
});

3. 轮询

// 每 3 秒轮询一次
const { data } = useRequest(getStatus, {
  pollingInterval: 3000,
  pollingWhenHidden: false,  // 页面隐藏时停止轮询
});

// 条件轮询
const { data } = useRequest(getJobStatus, {
  pollingInterval: 2000,
  pollingErrorRetryCount: 3,  // 错误重试次数
  onSuccess: (result) => {
    if (result.status === 'completed') {
      // 完成后停止轮询
      return false;
    }
  }
});

4. 依赖刷新

const [userId, setUserId] = useState('1');

const { data } = useRequest(
  () => fetchUser(userId),
  {
    refreshDeps: [userId],  // userId 变化时自动重新请求
  }
);

5. 缓存机制

// SWR 模式:先返回缓存,后台更新
const { data, loading } = useRequest(fetchUser, {
  cacheKey: 'user-data',
  staleTime: 5000,  // 5s 内认为数据新鲜
  cacheTime: 300000,  // 缓存保留 5 分钟
});

// 清除缓存
import { clearCache } from 'ahooks';
clearCache('user-data');

6. 错误重试

const { data, error, retry } = useRequest(unstableAPI, {
  retryCount: 3,  // 失败后重试 3 次
  retryInterval: 1000,  // 重试间隔 1s
  onError: (error, params) => {
    console.log('请求失败', error);
  }
});

三、进阶场景

并行请求

const user = useRequest(fetchUser);
const posts = useRequest(fetchPosts);
const comments = useRequest(fetchComments);

const loading = user.loading || posts.loading || comments.loading;

串行请求

const { data: user } = useRequest(fetchUser);

const { data: posts } = useRequest(
  () => fetchUserPosts(user.id),
  {
    ready: !!user,  // user 存在时才执行
    refreshDeps: [user],
  }
);

分页加载

function UserList() {
  const { data, loading, loadMore, loadingMore, noMore } = useRequest(
    (d) => fetchList({ page: d?.nextPage || 1 }),
    {
      loadMore: true,
      isNoMore: (d) => !d?.hasMore,
    }
  );
  
  return (
    <>
      {data?.list.map(item => <Item key={item.id} {...item} />)}
      {!noMore && (
        <Button onClick={loadMore} loading={loadingMore}>
          加载更多
        </Button>
      )}
    </>
  );
}

乐观更新

const { run: deleteItem } = useRequest(deleteAPI, {
  manual: true,
  onBefore: (params) => {
    // 立即更新 UI
    setList(list => list.filter(item => item.id !== params[0]));
  },
  onError: (error, params) => {
    // 失败时回滚
    message.error('删除失败');
    refresh();
  }
});

四、与其他方案对比

特性 useRequest React Query SWR
学习成本
功能完整度 很高
包体积 较大
防抖节流 内置 需自己实现 需自己实现
轮询 内置 内置 需配置
TypeScript 良好 优秀 良好

五、最佳实践

  1. 合理使用缓存:列表、详情等读多写少的数据适合缓存
  2. 设置合适的防抖时间:搜索建议 300-500ms
  3. 避免过度轮询:根据业务需求设置合理的轮询间隔
  4. 善用 ready 参数:避免无效请求
  5. 统一错误处理:在全局配置中处理通用错误
// 全局配置
import { configResponsive } from 'ahooks';

configResponsive({
  onError: (error) => {
    if (error.code === 401) {
      // 统一处理未登录
      redirectToLogin();
    }
  }
});

六、源码解析(简化版)

useRequest 的核心实现思路:

function useRequest(service, options) {
  const [state, setState] = useState({
    data: undefined,
    loading: false,
    error: undefined,
  });
  
  const run = useCallback(async (...params) => {
    setState(s => ({ ...s, loading: true }));
    
    try {
      const data = await service(...params);
      setState({ data, loading: false, error: undefined });
    } catch (error) {
      setState(s => ({ ...s, loading: false, error }));
    }
  }, [service]);
  
  useEffect(() => {
    if (!options.manual) {
      run(...(options.defaultParams || []));
    }
  }, []);
  
  return { ...state, run };
}

实际实现还包括:

  • 防抖节流的 debounce/throttle 包装
  • 轮询的 setInterval 管理
  • 缓存的 Map 存储
  • 依赖追踪的 useEffect
  • 请求取消的 AbortController

总结

useRequest 是一个功能强大且易用的请求管理 Hook,它封装了日常开发中 90% 的请求场景。通过合理使用其提供的能力,可以大幅减少样板代码,提升开发效率。

推荐在中小型项目中直接使用 useRequest,大型项目可以考虑 React Query 获得更强的数据管理能力。

如果这篇文章对你有帮助,欢迎点赞收藏!

React Suspense 从入门到实战:让异步加载更优雅

二、代码分割场景

这是 Suspense 最成熟的应用场景,配合 React.lazy 实现组件懒加载。

import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));

function App() {
  return (
    <Suspense fallback={<PageLoader />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

多层 Suspense 边界:

<Suspense fallback={<AppShell />}>
  <Layout>
    <Suspense fallback={<SidebarSkeleton />}>
      <Sidebar />
    </Suspense>
    <Suspense fallback={<ContentSkeleton />}>
      <Content />
    </Suspense>
  </Layout>
</Suspense>

三、数据请求场景

React 18 开始,Suspense 可以配合支持的数据请求库使用。

使用 SWR:

import useSWR from 'swr';

function User({ id }) {
  const { data } = useSWR(`/api/user/${id}`, fetcher, {
    suspense: true  // 开启 Suspense 模式
  });
  
  return <div>{data.name}</div>;
}

<Suspense fallback={<UserSkeleton />}>
  <User id={123} />
</Suspense>

使用 React Query:

import { useQuery } from '@tanstack/react-query';

function Posts() {
  const { data } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    suspense: true
  });
  
  return data.map(post => <Post key={post.id} {...post} />);
}

四、与 Error Boundary 配合

Suspense 只处理"挂起"状态,错误需要 Error Boundary 捕获。

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  render() {
    if (this.state.hasError) {
      return <ErrorFallback />;
    }
    return this.props.children;
  }
}

<ErrorBoundary>
  <Suspense fallback={<Loading />}>
    <DataComponent />
  </Suspense>
</ErrorBoundary>

五、与并发特性配合

配合 useTransition 避免不必要的 loading 状态:

function SearchResults() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();
  
  const handleSearch = (value) => {
    startTransition(() => {
      setQuery(value);  // 低优先级更新
    });
  };
  
  return (
    <>
      <input onChange={e => handleSearch(e.target.value)} />
      {isPending && <InlineSpinner />}
      <Suspense fallback={<ResultsSkeleton />}>
        <Results query={query} />
      </Suspense>
    </>
  );
}

六、最佳实践

  1. 合理划分边界:按路由或功能模块设置 Suspense,避免过细或过粗
  2. 提供有意义的 fallback:使用骨架屏而非简单的 loading 文字
  3. 避免瀑布流:并行发起请求,而非串行等待
  4. 配合预加载:在用户交互前提前触发数据请求
// 预加载示例
const resource = fetchUser(id);  // 提前发起

function Profile() {
  const user = use(resource);  // 直接使用
  return <div>{user.name}</div>;
}

七、注意事项

  • Suspense 在服务端渲染(SSR)中需要特殊处理,React 18 提供了流式 SSR 支持
  • 不是所有数据请求库都支持 Suspense,使用前查看文档
  • fallback 组件应该是轻量的,避免在其中执行副作用
  • 嵌套 Suspense 时,内层边界会优先生效

总结

Suspense 让异步操作的处理更加优雅和声明式。从代码分割到数据请求,它都能提供更好的开发体验和用户体验。配合 React 18 的并发特性,Suspense 将成为构建现代 React 应用的重要工具。

如果这篇文章对你有帮助,欢迎点赞收藏!

ahooks useRequest 深度解析:一个 Hook 搞定所有请求

二、核心功能详解

1. 自动管理请求状态

const { data, loading, error, run, refresh, cancel } = useRequest(
  fetchUserList,
  {
    manual: false,  // 自动执行
    defaultParams: [{ page: 1 }],  // 默认参数
  }
);

// run: 手动触发
// refresh: 使用上次参数重新请求
// cancel: 取消当前请求

2. 防抖与节流

// 搜索场景:防抖
const { data, loading } = useRequest(searchAPI, {
  debounceWait: 300,  // 300ms 防抖
  manual: true,
});

// 滚动加载:节流
const { run } = useRequest(loadMore, {
  throttleWait: 1000,  // 1s 节流
  manual: true,
});

3. 轮询

// 每 3 秒轮询一次
const { data } = useRequest(getStatus, {
  pollingInterval: 3000,
  pollingWhenHidden: false,  // 页面隐藏时停止轮询
});

// 条件轮询
const { data } = useRequest(getJobStatus, {
  pollingInterval: 2000,
  pollingErrorRetryCount: 3,  // 错误重试次数
  onSuccess: (result) => {
    if (result.status === 'completed') {
      // 完成后停止轮询
      return false;
    }
  }
});

4. 依赖刷新

const [userId, setUserId] = useState('1');

const { data } = useRequest(
  () => fetchUser(userId),
  {
    refreshDeps: [userId],  // userId 变化时自动重新请求
  }
);

5. 缓存机制

// SWR 模式:先返回缓存,后台更新
const { data, loading } = useRequest(fetchUser, {
  cacheKey: 'user-data',
  staleTime: 5000,  // 5s 内认为数据新鲜
  cacheTime: 300000,  // 缓存保留 5 分钟
});

// 清除缓存
import { clearCache } from 'ahooks';
clearCache('user-data');

6. 错误重试

const { data, error, retry } = useRequest(unstableAPI, {
  retryCount: 3,  // 失败后重试 3 次
  retryInterval: 1000,  // 重试间隔 1s
  onError: (error, params) => {
    console.log('请求失败', error);
  }
});

三、进阶场景

并行请求

const user = useRequest(fetchUser);
const posts = useRequest(fetchPosts);
const comments = useRequest(fetchComments);

const loading = user.loading || posts.loading || comments.loading;

串行请求

const { data: user } = useRequest(fetchUser);

const { data: posts } = useRequest(
  () => fetchUserPosts(user.id),
  {
    ready: !!user,  // user 存在时才执行
    refreshDeps: [user],
  }
);

分页加载

function UserList() {
  const { data, loading, loadMore, loadingMore, noMore } = useRequest(
    (d) => fetchList({ page: d?.nextPage || 1 }),
    {
      loadMore: true,
      isNoMore: (d) => !d?.hasMore,
    }
  );
  
  return (
    <>
      {data?.list.map(item => <Item key={item.id} {...item} />)}
      {!noMore && (
        <Button onClick={loadMore} loading={loadingMore}>
          加载更多
        </Button>
      )}
    </>
  );
}

乐观更新

const { run: deleteItem } = useRequest(deleteAPI, {
  manual: true,
  onBefore: (params) => {
    // 立即更新 UI
    setList(list => list.filter(item => item.id !== params[0]));
  },
  onError: (error, params) => {
    // 失败时回滚
    message.error('删除失败');
    refresh();
  }
});

四、与其他方案对比

特性 useRequest React Query SWR
学习成本
功能完整度 很高
包体积 较大
防抖节流 内置 需自己实现 需自己实现
轮询 内置 内置 需配置
TypeScript 良好 优秀 良好

五、最佳实践

  1. 合理使用缓存:列表、详情等读多写少的数据适合缓存
  2. 设置合适的防抖时间:搜索建议 300-500ms
  3. 避免过度轮询:根据业务需求设置合理的轮询间隔
  4. 善用 ready 参数:避免无效请求
  5. 统一错误处理:在全局配置中处理通用错误
// 全局配置
import { configResponsive } from 'ahooks';

configResponsive({
  onError: (error) => {
    if (error.code === 401) {
      // 统一处理未登录
      redirectToLogin();
    }
  }
});

六、源码解析(简化版)

useRequest 的核心实现思路:

function useRequest(service, options) {
  const [state, setState] = useState({
    data: undefined,
    loading: false,
    error: undefined,
  });
  
  const run = useCallback(async (...params) => {
    setState(s => ({ ...s, loading: true }));
    
    try {
      const data = await service(...params);
      setState({ data, loading: false, error: undefined });
    } catch (error) {
      setState(s => ({ ...s, loading: false, error }));
    }
  }, [service]);
  
  useEffect(() => {
    if (!options.manual) {
      run(...(options.defaultParams || []));
    }
  }, []);
  
  return { ...state, run };
}

实际实现还包括:

  • 防抖节流的 debounce/throttle 包装
  • 轮询的 setInterval 管理
  • 缓存的 Map 存储
  • 依赖追踪的 useEffect
  • 请求取消的 AbortController

总结

useRequest 是一个功能强大且易用的请求管理 Hook,它封装了日常开发中 90% 的请求场景。通过合理使用其提供的能力,可以大幅减少样板代码,提升开发效率。

推荐在中小型项目中直接使用 useRequest,大型项目可以考虑 React Query 获得更强的数据管理能力。

如果这篇文章对你有帮助,欢迎点赞收藏!

❌