why-did-you-render 深度解析: monkey patch 如何追踪可避免的重渲染【免费下载链接】why-did-you-renderwhy-did-you-render by Welldone Software monkey patches React to notify you about potentially avoidable re-renders. (Works with React Native as well.)项目地址: https://gitcode.com/gh_mirrors/wh/why-did-you-renderwhy-did-you-render 用 monkey patch 包住 React 的createElement、hooks 与 JSX 入口把「本可避免的重渲染」变成可定位的 diff 报告。全文拆解它的追踪判定、四种组件补丁、diff 算法、owner 归因与 React 19 接管路径读后可接入项目并看懂每条日志。它回答的是「为什么又渲染」不是「渲染有多慢」React 的重渲染成本在组件粒度上父组件一次重渲染会让所有未优化的子组件跟着重渲染。React.memo只能告诉你「这次跳过了」却回答不了反面问题——当重渲染真的发生时是 props 的引用变了而值没变还是某个 hook 的结果变了甚至是父组件自己多渲染了一遍手动在组件里console.log对比 props只能覆盖一层、还要自己写深比较。why-did-you-render下称 WDYR取代的就是这类手工排查。它的设计边界很明确这是一个观测工具补丁只读快照、计算 diff、调用notifier不拦截也不改变渲染结果异常时回退到原始createElement保证不吞错tests/index.test.js 里有「dont swallow errors」用例。它是开发期工具README 明确禁止用于生产环境理由是显著拖慢 React 且 monkey patch 可能引入意外行为。它只报告可避免的重渲染默认静默掉那些 props/state 值真的变了的重渲染把噪音留给可选的logOnDifferentValues。理解了这三条边界再看它的实现就是一条主线用最小侵入的包装在每次渲染时拿到「上次与这次」的输入做一次确定性的 diff把差异归因到具体字段。状态放在哪里一张全局 store一枚组件静态标记全部运行状态集中在模块单例 wdyrStore.js字段类型用途React/optionsobject被 patch 的 React 实例经 normalizeOptions.js 补全默认值后的用户配置origCreateElement/origCreateFactory/origCloneElementfunction原始 API 引用用于异常回退与还原componentsMapWeakMap原始组件 → 补丁后组件的缓存同一组件只包一次ownerDataMapWeakMapowner 的 fiber →{props, state, hooksInfo, ...}快照是 owner 归因的数据源hooksInfoForCurrentRenderWeakMap当前这次渲染中 owner 的 hooks 结果存档后即清除ownerBeforeElementCreationfiber创建子元素前一刻记录的 owner子组件补丁读取它作为nextOwner三个 map 全部用WeakMap组件与 fiber 被 GC 时 store 不持有强引用patch 不产生额外生命周期管理成本。全局配置之外每个组件还可以挂一枚静态标记types.d.ts 中定义为boolean或WhyDidYouRenderOptionsEnhancedMenu.whyDidYouRender true; // 或 false EnhancedMenu.whyDidYouRender { logOnDifferentValues: true, // 值真的变了也报告 customName: Menu, // 覆盖日志里的显示名 };全局选项的默认值由normalizeOptions固定src/normalizeOptions.js关键几项选项默认值作用include/excludenull按displayName匹配的正则列表exclude优先级最高trackAllPureComponentsfalse批量追踪React.PureComponent与React.memo组件trackHookstrue是否包装并追踪原生 hookstrackExtraHooks[][[模块, hook名], ...]追踪自定义 hook如 react-redux 的useSelectorlogOwnerReasonstrue额外报告「owner 为什么重渲染」logOnDifferentValuesfalse默认只报告「值相等」的重渲染true则报告所有重渲染hotReloadBufferMs500HMR 完成后忽略通知的缓冲毫秒数notifierdefaultNotifierupdateInfo的最终消费者可整体替换titleColor/diffNameColor/diffPathColor/textBackgroundColor#058/blue/red/white控制台配色hotReloadBufferMs: 500是实打实的实现参数而非文案defaultNotifier.js 里createDefaultNotifier向 webpack 的module.hot.addStatusHandler注册回调当 HMR 状态回到idle时置inHotReload truesetTimeout(hotReloadBufferMs)后再放开——热更新触发的整树重渲染因此不会刷屏。一个组件凭什么进入追踪入口是getWDYRTypesrc/whyDidYouRender.js先过一道「支持性」闸门getIsSupportedComponentType——memo递归解开.type、forwardRef解开.render、函数组件直接通过不满足的组件直接返回nullcreateElement原样透传零开销。支持性通过后进入shouldTracksrc/shouldTrack.js判定顺序本身就是优先级表优先级判断结果1exclude任一正则命中displayName不追踪压过include和手动标记2Component.whyDidYouRender false不追踪3仅 hook 变化路径whyDidYouRender.trackHooks false不追踪 hook 变化4Component.whyDidYouRender为真值追踪5trackAllPureComponents且组件是PureComponent或memo追踪6include任一正则命中追踪兜底以上都不满足不追踪这里的「为什么」值得注意exclude放最前面是因为它是团队级开关比如把第三方 HOC 全部排除必须能压过一切个体配置whyDidYouRender false次之让trackAllPureComponents开后仍能按组件豁免。displayName由 getDisplayName.js 按displayName → name → memo 内层 type → forwardRef 的 render → 字符串本身 → Unknown的链式回退取得正则匹配、日志标题、include/exclude都用它所以自定义的customName也会进入这套匹配。通过判定的组件经createPatchedComponent包装后写进componentsMapWeakMap同一组件后续每次createElement都复用同一个补丁副本包装成本只付一次。四种组件四套补丁同一个快照套路createPatchedComponent按类型分派到 patches/ 下四个补丁器组件形态补丁器快照存放处关键细节类组件patchClassComponent.js实例this._WDYRrenderNumber、prevProps/prevState/prevOwner子类化原组件StrictMode 下跳过奇数序渲染处理render被bind的边角函数 / 字符串标签patchFunctionalOrStrComponent.js包装函数内两个useRefprevProps、prevOwner字符串标签如div先转成包装函数再包isPure标志决定「空 diff」报告规则React.memopatchMemoComponent.js递归内层解开 memo → 按内层类型递归补丁 → 用原始compare函数重新React.memo包回React.forwardRefpatchForwardRefComponent.js递归内层内层若是 memo 则先以原compare重包再进forwardRef注意 memo 与 forwardRef 补丁都刻意保留原compare追踪不应改变组件的渲染决策跳过/重渲染的行为必须与未包装时一致。函数补丁的核心循环节选自 patchFunctionalOrStrComponent.jsfunction WDYRFunctionalComponent(nextProps, refMaybe, ...args) { const prevProps prevPropsRef.current; prevPropsRef.current nextProps; const prevOwner prevOwnerRef.current; const nextOwner wdyrStore.ownerBeforeElementCreation; prevOwnerRef.current nextOwner; if (prevProps) { const updateInfo getUpdateInfo({ prevProps, nextProps, prevOwner, nextOwner, /* ... */ }); const notifiedByHooks (!updateInfo.reason.propsDifferences || (isPure updateInfo.reason.propsDifferences.length 0)); if (!notifiedByHooks) wdyrStore.options.notifier(updateInfo); } return FunctionalComponent(nextProps, refMaybe, ...args); }这里的门控规则对应findObjectsDifferences的两种返回props引用未变时返回false父组件重渲染但子 props 是同一对象子跟着重渲染属正常不报告props引用变了时返回差异数组其中空数组表示「对象换了、内部值全是同一引用」。函数组件在引用变化时一律报告含空数组唯独纯组件memo的空数组静默——那种情形只能由自定义compare允许重渲染造成属于设计内行为。类补丁没有这层门控除首次外每次渲染都调用notifier是否打印完全交给通知层。StrictMode 的细节在类补丁里它沿 fiber 链检查REACT_STRICT_MODE位标记0b1000拷贝自 React 内部常量见 consts.js 与 utils.js命中后跳过renderNumber为奇数的渲染——StrictMode 下渲染成对出现报告偶数序即可。所有补丁都会把原始组件函数挂到包装组件的ComponentForHooksTracking上。这是 hook 追踪的关键hooks 在包装组件里执行getCurrentOwner()拿到的是包装组件的 fiber但日志必须显示原始组件名、按原始组件判定是否追踪ComponentForHooksTracking就是这层指针。React 19 之后钩子不能再只挂在 createElement 上React 16 的 automatic JSX 转换下开发模式的 JSX 编译产物是jsxDEV(...)调用来自react/jsx-dev-runtime并不经过React.createElement。只 patchcreateElement的工具在 React 19 的开发模式下会彻底失明。WDYR 的对策是让 Babel 把【免费下载链接】why-did-you-renderwhy-did-you-render by Welldone Software monkey patches React to notify you about potentially avoidable re-renders. (Works with React Native as well.)项目地址: https://gitcode.com/gh_mirrors/wh/why-did-you-render创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
企业数字化 ERP 产品动态
相关推荐
OpCore-Simplify:十分钟生成完整 OpenCore EFI 的终极指南 OpCore-Simplify:十分钟生成完整 OpenCore EFI 的终极指南 【免费下载链接】OpCore-Simplify A tool designed to simplify the creation of OpenCore EFI 项目地址: https://gitcode.com/GitHub_Trending/op/OpCore-Simplify
打开一台待装系统的笔记本&… · 2026/11/3 2:23:23
为什么你的跨数据集迁移学习没生效?Fashion-MNIST 迁移 CIFAR-10 的三个避坑决策 为什么你的跨数据集迁移学习没生效?Fashion-MNIST 迁移 CIFAR-10 的三个避坑决策 【免费下载链接】fashion-mnist A MNIST-like fashion product database. Benchmark :point_down: 项目地址: https://gitcode.com/gh_mirrors/fa/fashion-mnist
同一个模型&… · 2026/11/3 2:23:23
* LangGraph 基础入门:定位、核心概念与第一个可运行图 一、LangGraph总览
LangGraph 运行时底层基于自研的 Pregel 运行时,其核心思想借鉴了 Google Pregel 计算模型,用于组织和执行复杂的图计算流程。
1.0. LangGraph和LangChain的版本迭代及定位
LangChain 1.x 的定位发生了明显变化:旧式 Ch… · 2026/11/3 2:49:27
squish6.3自动化代码用例编写 一、 用例函数之 init() 和 cleanup()
附加模式下 一定要注意用例结束后的恢复工作,否则会造成后续用例失败;
squish中默认提供了init和cleanup函数:
init()方法失败,main()方法不执行,cleanup()方法会执行;… · 2026/11/3 2:49:27
【元宝搜索开发者征文大赛】少点幻觉,多点搜索:我做了个政策过期检测器 目录
前言
1 起因:AI 拿着一份过期三年的补贴政策,差点让他报错材料
2 为什么治「政策幻觉」,非得用元宝搜索不可
3 先把钥匙拿到手:元宝搜索 API Key 开通全流程
3.1 开通服务,把版本选对
3.2 先用 curl 打通一… · 2026/11/3 2:49:27
大模型小白必看:收藏这份AI Agent底层逻辑解析,轻松上手构建智能应用 本文深入解析AI Agent的核心运作机制,揭示其本质为“调用工具的循环系统”。文章详细拆解了Agent的四大核心组件:Loop循环机制、Tools工具调用能力、Memory记忆模块以及Harness调度管控模块,并阐述了它们如何协同工作以实现智能任务执行。通过… · 2026/11/3 2:49:27
从0开始学架构-结束语:坚持,成就你的技术梦想 “从0开始学架构”专栏历经4个月的时间,现在到了跟你说再见的时候了。一路走来,非常感谢你的坚持。看到专栏的内容能够帮助你理解架构设计这个看起来高大上的技术,我非常欣慰,也算完成了自己3年前的一个心愿:帮助更多同学更快更好地掌握架构设计的技术。 专栏的结束意味着… · 2026/11/3 2:48:27
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18