eslint-plugin-unicorn 规则深度解析prefer-observer-apis 快照测试全解读【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn导读prefer-observer-apis是 eslint-plugin-unicorn 提供的推荐级规则recommended配置默认启用用于在代码中同时出现resize/scroll事件监听与同步布局读取时给出提示引导开发者改用ResizeObserver/IntersectionObserver。本文以仓库中的快照测试报告 test/snapshots/prefer-observer-apis.js.md 为骨架结合规则实现 rules/prefer-observer-apis.js 与测试用例 test/prefer-observer-apis.js逐项解读该规则识别的 60 个反模式用例、报错定位行为与消息文本并说明其类型感知能力与边界限制帮助你快速判断该规则在你的代码库中会命中哪些模式、为什么命中。规则背景为什么 layout reads 要配合 observer API在浏览器中resize与scroll事件触发频率极高若在监听回调里同步读取布局几何信息如element.offsetWidth、element.getBoundingClientRect()会强制浏览器同步执行布局计算forced synchronous layout / layout thrashing导致明显的滚动与缩放卡顿。Web 平台为此提供了两个观察者 APIResizeObserver在元素尺寸变化时回调通过entry.contentRect拿到新尺寸无需在resize事件里反复读取offsetWidth等属性IntersectionObserver在元素与视口交叉状态变化时回调通过entry.isIntersecting判断可见性无需在scroll事件里计算getBoundingClientRect()与视口高度的关系。该规则的 meta 定义 表明它属于suggestion类型、recommended: true、schema: []无任何配置项并声明了两条消息const messages { resize: Prefer ResizeObserver over a resize listener with layout reads., scroll: Prefer IntersectionObserver over a scroll listener with layout reads., };快照文件中每次报错的Message:行都与上述文本逐字对应例如 invalid(1) 输出Prefer ResizeObserver over a resize listener with layout reads.invalid(9) 输出Prefer IntersectionObserver over a scroll listener with layout reads.。触发条件事件名 × 监听器 × 布局读取的三重组合从规则主逻辑rules/prefer-observer-apis.js#L456-L478可以看到一条代码要触发报告必须同时满足三个条件是addEventListener调用通过isAddEventListenerCall判定window/self/globalThis上的方法调用或全局裸调用addEventListener(...)参数数量为 2 到 3 个事件名静态可解析为resize或scrollgetEventName先尝试静态字符串值字符串字面量、模板字面量、as const、satisfies等失败时回退到 TypeScript 类型信息监听函数体内存在布局读取containsLayoutRead递归遍历回调体 AST命中布局方法调用 / 布局属性读取 / 布局解构读取三者之一即命中且不会跨入嵌套函数node ! root isFunction(node)时停止下钻。快照中的 invalid(1)–invalid(8) 覆盖了resize事件的各种写法箭头函数、普通函数、{passive: true}第三参数、模板字面量事件名、/复合写入全部命中resize消息invalid(9)–invalid(16) 覆盖scroll事件getBoundingClientRect、可选链?.、计算属性访问[getBoundingClientRect]、模板字符串键、带参数调用、getClientRects全部命中scroll消息。监听器写法对命中与否的影响getListenerFunctionrules/prefer-observer-apis.js#L399-L429决定哪些监听器引用可被解析内联箭头函数 / 内联函数表达式invalid(1)、invalid(2)局部函数声明FunctionNameinvalid(36) 中function handler() { return element.offsetHeight; }作为监听器传入被命中且即便之后有handler () {}重新赋值invalid(37)仍然命中文档 Limitations 明确函数声明重新赋值不追踪局部const变量初始化器为函数invalid(38)、invalid(39)、invalid(60)不可解析的引用不报window.addEventListener(resize, handler)中handler无定义、object.handler成员引用、import导入的 handler、let/var声明的变量在 valid 用例中均不触发见 test/prefer-observer-apis.js#L43-L48。布局读取的完整识别清单规则维护了若干白名单集合rules/prefer-observer-apis.js#L29-L55类别成员快照代表用例布局方法getBoundingClientRect、getClientRectsinvalid(9)–invalid(16)元素布局属性clientHeight、clientLeft、clientTop、clientWidth、offsetHeight、offsetLeft、offsetParent、offsetTop、offsetWidth、scrollHeight、scrollWidthinvalid(17)–invalid(18)计算属性访问视口属性innerHeight、innerWidth作用于window/globalThis/self全局对象invalid(27)–invalid(30)VisualViewport 属性height、widthinvalid(31)–invalid(32)全局裸标识符直接引用innerWidth/innerHeight且不是写入目标invalid(8)、invalid(28)解构读取const {offsetWidth} element;、({offsetWidth} element);、const {width} visualViewport;等invalid(19)–invalid(26)其中解构场景是快照的重点扩展invalid(19)–invalid(21) 使用计算属性解构const {[clientWidth]: width} element;invalid(22)–invalid(25) 覆盖变量声明解构与赋值表达式解构invalid(26) 覆盖visualViewport的width解构——这些都能被isLayoutDestructuringNoderules/prefer-observer-apis.js#L337-L354识别。写入与删除不算读取isWriteOnlyLeftHandSiderules/prefer-observer-apis.js#L231-L235排除了赋值目标场景因此以下 valid 用例不报错window.addEventListener(resize, () { element.offsetWidth 1; }); window.addEventListener(resize, () { window.innerWidth 1; }); window.addEventListener(resize, () { delete element.offsetWidth; });对应测试见 test/prefer-observer-apis.js#L25-L27。同理element.scrollTop/scrollLeft属于滚动位置读取而非几何布局读取快照与测试明确将其归为合法updateScrollPosition(element.scrollTop)不报错因为文档指出纯滚动位置监听器被忽略——observer API 并不总是更优替代。类型感知TypeScript 项目中的精确判定规则在 TypeScript 文件测试中通过typeAware辅助函数启用typescriptEslintParser与projectService见 test/prefer-observer-apis.js#L7-L14中具备类型感知能力核心是判断对象表达式是否确认为 DOM 库类型isKnownDefaultLibraryNode/isKnownDefaultLibraryTyperules/prefer-observer-apis.js#L101-L173借助parserServices.getTypeAtLocation与 TypeScriptTypeChecker确认类型是默认库program.isSourceFileDefaultLibrary中的Window/VisualViewport/Element/HTMLElement等isKnownNonDomNode/isKnownNonDomTyperules/prefer-observer-apis.js#L62-L99反向排除非 DOM 的自定义类型避免误报。类型感知带来的典型判定均来自快照命中确认为 DOM 类型element: Element/element: HTMLElement参数invalid(40)–invalid(41)继承HTMLElement的类/接口invalid(42)–invalid(43)强制断言回 DOMsize as unknown as HTMLElement的属性读取与解构读取invalid(44)–invalid(45)window_: Window参数上的方法调用与解构invalid(46)–invalid(47)联合类型Window | undefined与泛型T extends Windowinvalid(48)–invalid(49)VisualViewport类型参数上的属性读取与解构invalid(50)–invalid(51)类型化为字面量的事件名resize as const、const eventName resize、eventName: scroll、(resize satisfies string)invalid(52)–invalid(55)监听器被断言为EventListener(() element.offsetWidth) as EventListenerinvalid(59)–invalid(60)。不命中确认为非 DOM 类型对应 valid 用例test/prefer-observer-apis.js#L63-L168自定义type Size { offsetWidth: number }的属性访问与解构自定义WindowLike/ViewportLike结构类型普通局部变量参数innerWidth: number自定义Emitter类 /extends EventTarget的 emitter 上的addEventListener联合事件名resize | scroll无法证明具体值与as resize的宽断言。类型工具函数位于 rules/utils/types.js其中isDefaultLibrarySymbol正是通过声明文件是否来自默认库来判断类型来源。报错定位与消息行为快照实证快照报告展示了每条用例的精确报错位置高亮^始终指向addEventListener调用中的事件名参数而非监听器或布局读取位置。例如 invalid(1) 1 | window.addEventListener(resize, () element.offsetWidth) | ^^^^^^^^ Prefer ResizeObserver over a resize listener with layout reads.这对应规则返回值{node: eventNameNode, messageId: eventName}rules/prefer-observer-apis.js#L473-L476。在跨行场景invalid(36)中高亮仍然定位到第 5 行的resize参数上同时快照上下文会附带整个监听器函数体便于理解触发原因。消息语义可总结为事件名消息文本建议替代 APIresizePrefer ResizeObserver over a resize listener with layout reads.ResizeObserverscrollPrefer IntersectionObserver over a scroll listener with layout reads.IntersectionObserver正确改写示例与边界限制规则文档 docs/rules/prefer-observer-apis.md 给出了标准改写示例// ❌ resize 布局读取 window.addEventListener(resize, () { element.classList.toggle(is-small, element.offsetWidth 500); }); // ✅ ResizeObserver new ResizeObserver(entries { for (const entry of entries) { entry.target.classList.toggle(is-small, entry.contentRect.width 500); } }).observe(element);// ❌ scroll 布局读取 window.addEventListener(scroll, () { element.classList.toggle(is-visible, element.getBoundingClientRect().top window.innerHeight); }); // ✅ IntersectionObserver new IntersectionObserver(entries { for (const entry of entries) { entry.target.classList.toggle(is-visible, entry.isIntersecting); } }).observe(element);// ✅ 纯滚动位置读取不报错 window.addEventListener(scroll, () { updateScrollPosition(window.scrollY); });规则的 Limitations见 docs/rules/prefer-observer-apis.md需要在使用时注意只检查内联监听函数、局部函数声明与局部const函数监听器不追踪函数声明重新赋值invalid(37) 仍命中正是这一行为动态事件名默认忽略除非 TypeScript 类型信息能证明其值恰好是resize或scroll如as const、字面量类型参数、satisfies对更宽泛动态名称的直接类型断言如as resize之外的类型被忽略不透明的监听器引用导入的 handler、成员方法、let/var变量被忽略以避免误报由于规则依赖 TypeScript parser 的类型服务跨项目使用时需要确保 ESLint 配置中启用了类型感知的 parser测试环境通过parserOptions: {projectService: {allowDefaultProject: [*.ts]}}实现。如何在自己的项目中启用该规则已包含在recommended配置中recommended: true使用标准配置即可默认开启由于schema: []它不接受任何选项。若需单独启用或关闭可在 ESLint 配置中对unicorn/prefer-observer-apis设置error/warn/off。需要说明的是快照测试文件 test/snapshots/prefer-observer-apis.js.md 本身是 AVA 测试框架自动生成的产物文件头注明由 AVA 生成其内容60 个 invalid 用例的输入与报错输出由 test/prefer-observer-apis.js 中的用例驱动是观察该规则行为边界的直接材料——当你怀疑某个写法是否会被该规则命中时快照中的 60 个反例与测试中的 40 余个正向用例就是最可靠的判定依据。【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考