eslint-plugin-unicorn 规则深度解析:require-passive-events 强制高频事件使用被动监听器
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
导读
require-passive-events是 eslint-plugin-unicorn 中一条"开箱即用"的可自动修复规则(suggestion 类型),它要求开发者对touchstart、wheel等高频滚动类事件注册事件监听器时显式传入{passive: true}选项,从而让浏览器在滚动时无需等待 JS 判断是否调用preventDefault(),显著提升页面滚动的响应速度。读完本文,你将掌握该规则的完整判定逻辑、自动修复的多种场景与边界处理、以及它对"误报"的精细规避策略,并了解如何从源码与测试层面验证其行为。
该规则在 docs/rules/require-passive-events.md 中定义,完整实现位于 rules/require-passive-events.js,测试用例见 test/require-passive-events.js,快照结果在 test/snapshots/require-passive-events.js.snap。
规则要解决的核心问题
浏览器在处理滚动、触摸这类高频事件时,无法预知事件监听器是否会调用preventDefault()。为了让页面滚动不被阻塞,浏览器只能先执行完 JS 监听器再决定是否滚动,这会造成明显的滚动延迟和卡顿。
被动事件监听器(passive listener)正是为解决此问题而生:当监听器以{passive: true}注册时,浏览器可以假设它不会阻止默认行为,从而立即开始滚动,不必等待 JS 执行完毕。这正是"被动"的含义——监听器被动接受事件,但不干预浏览器的默认行为。
因此,本规则的核心理念是:对于高频事件,只要你的监听器确实不会调用preventDefault(),就应该显式声明{passive: true},把"我不会阻止滚动"的承诺提前告诉浏览器,换取更流畅的用户体验。
规则覆盖的事件白名单
哪些事件属于"高频事件"?规则在 rules/require-passive-events.js 中维护了一个硬编码白名单:
const passiveEventNames = new Set([ 'touchstart', 'touchmove', 'touchenter', 'touchend', 'touchleave', 'wheel', 'mousewheel', ]);即以下 7 个事件会被检查:
| 事件名 | 典型场景 |
|---|---|
touchstart | 触屏按下,如移动端手势起点 |
touchmove | 触屏拖动,如轮播图、滚动容器内部手势 |
touchenter | 触点进入元素 |
touchend | 触点离开屏幕 |
touchleave | 触点离开元素 |
wheel | 鼠标滚轮 / 触控板滚动 |
mousewheel | 旧版浏览器滚轮事件 |
注意一个边界:scroll事件并不在白名单内。原因从浏览器机制上可以理解——scroll事件本身就发生在滚动之后,preventDefault()对滚动没有意义,所以passive选项对它是无效的;而wheel/touch*事件发生在滚动之前,浏览器需要先执行监听器判断是否拦截,这时passive才有价值。测试用例也印证了这一点:window.addEventListener("scroll", () => {})被视为合法代码(见 test/require-passive-events.js)。
同时,只有字符串字面量事件名才会被识别。window.addEventListener(eventName, () => {})这种动态事件名在测试中被列为有效代码(test/require-passive-events.js),因为规则无法在静态分析阶段确定事件名,只能跳过以避免误报。
规则的基本判定与三个合法示例
规则在每次CallExpression上触发(rules/require-passive-events.js),先用isMethodCall工具做精确匹配:
context.on('CallExpression', callExpression => { if (!isMethodCall(callExpression, { method: 'addEventListener', minimumArguments: 2, maximumArguments: 3, optionalCall: false, optionalMember: false, })) { return; } // ... });isMethodCall是插件中最常用的 AST 检查工具(实现见 rules/ast/is-method-call.js),它要求:调用必须是xxx.addEventListener(...)形式的成员方法调用,参数数量为 2~3 个,且不允许可选链(window?.addEventListener("wheel", () => {})、window.addEventListener?.("wheel", () => {})在测试中均为有效代码,见 test/require-passive-events.js——因为可选调用时监听器可能不会真正注册,加上passive与否无从保证)。
随后规则校验三件事:事件名是白名单内的字符串、监听器是内联函数(箭头函数或函数表达式)、监听器参数(事件对象)的使用是"安全"的(详见后文"安全分析")。全部通过后,再分析第三个参数(options)来决定是否报告问题。
文档给出的三类合法写法
① 高频事件 + 显式 passive 选项(推荐):
// ✅ window.addEventListener('wheel', () => {}, {passive: true});② 高频事件 + 监听器内确实调用了 preventDefault()(豁免):
// ✅ window.addEventListener('wheel', event => { event.preventDefault(); });这种写法是允许的,因为监听器明确要阻止默认行为,此时若强制passive: true反而会破坏功能。规则通过isEventParameterSafe分析(rules/require-passive-events.js)识别出event.preventDefault()调用并豁免。
③ 非高频事件(不需要 passive):
// ✅ window.addEventListener('click', () => {});click不属于白名单事件,点击事件不会阻塞滚动,无需被动监听。
违规场景与完整修复策略
当判定违规时,规则报告消息:Use '{passive: true}' for this high-frequency event listener.,并返回一个带fix函数的修复对象。修复逻辑根据 options 参数的形态分为四类,这是本规则最精巧的部分。
场景一:完全没有第三个参数 → 追加完整选项对象
// ❌ window.addEventListener('wheel', () => {}); // ✅ 修复后 window.addEventListener('wheel', () => {}, {passive: true});对应fixMissingOptions(rules/require-passive-events.js)。实现细节值得注意:修复文本插入在监听器外层的括号之后(使用getParenthesizedRange获取括号范围),这样window.addEventListener("wheel", (() => {}))会被修复为window.addEventListener("wheel", (() => {}), {passive: true})而不是把选项错误地塞进括号内部变成序列表达式。测试中的'window.addEventListener("wheel", (() => {}))'、'window.addEventListener("wheel", ((function () {})))'两个用例正是验证这一点(test/require-passive-events.js)。
场景二:第三个参数是布尔字面量 → 原地展开为对象
// ❌ window.addEventListener('wheel', () => {}, true); // ✅ 修复后 window.addEventListener('wheel', () => {}, {capture: true, passive: true});// ❌ window.addEventListener('wheel', () => {}, false); // ✅ 修复后 window.addEventListener('wheel', () => {}, {passive: true});对应fixBooleanOptions(rules/require-passive-events.js):老式 API 中true表示捕获阶段(capture),所以修复时保留语义——true展开为{capture: true, passive: true},false则直接替换为{passive: true}。测试覆盖了true、false以及带括号的(true)(test/require-passive-events.js)。
场景三:对象选项但没有 passive 属性 → 智能插入
// ❌ window.addEventListener('wheel', () => {}, {once: true}); // ✅ 修复后(单行) window.addEventListener('wheel', () => {}, {once: true, passive: true});对应fixObjectOptionsWithoutPassive(rules/require-passive-events.js),它进一步细分了三种排版情况:
- 空对象
{}:直接整体替换为{passive: true}; - 单行对象:在最后一个属性后插入
, passive: true; - 多行对象:读取最后一个属性的缩进(
getIndentString),在,之后按相同缩进另起一行插入passive: true,,保持代码风格一致。
多行场景在测试中有明确用例(test/require-passive-events.js)。
这里还有一个安全阀:如果对象最后一个属性与右花括号之间存在注释(hasCommentsBeforeClosingBrace,见 rules/require-passive-events.js),规则会报告问题但不提供自动修复(fix为undefined),避免修复时打乱注释的归属。对应测试'once: true // Keep this comment with once.'(test/require-passive-events.js)。
场景四:对象选项里 passive 显式为 false → 翻转为 true
// ❌ window.addEventListener('wheel', () => {}, {passive: false}); // ✅ 修复后 window.addEventListener('wheel', () => {}, {passive: true});对应fixPassiveFalse(rules/require-passive-events.js),仅把false字面量替换为true。{"passive": false}(字符串键)、{passive: (false)}(带括号)等形式同样会被识别,见测试 test/require-passive-events.js。
不自动修复的"灰色地带"
getOptionsProblem(rules/require-passive-events.js)定义了三种不修复也不报告的情况,避免对动态代码产生破坏性修改:
- options 是标识符或表达式,如
options、{...options}(无法静态推断内容); - options 对象中包含展开元素(SpreadElement)或计算属性,如
{...options}、{[passive]: true}; - 对象里已有
passive属性,但值是动态表达式,如{passive: Boolean(value)}。
这些情形下规则保持沉默,测试一一覆盖(test/require-passive-events.js)。
精细的安全分析:如何避免误报
文档的 Limitations 部分明确写道:只有内联监听器函数会被检查;命名监听器、动态选项、带展开的选项以及"不透明"的事件参数使用都会被忽略,以避免误报。源码把这一承诺落实为多层安全检查。
内联函数限定
isFunction(rules/require-passive-events.js)只接受ArrowFunctionExpression和FunctionExpression,因此window.addEventListener("wheel", handler)、window.addEventListener("wheel", object.handleEvent)等命名/方法监听器不会被检查(对应测试 test/require-passive-events.js)——命名函数可能被多处复用,无法确定其是否调用preventDefault()。
事件参数使用分析(isEventParameterSafe)
即使监听器是内联函数,规则还要分析其事件参数的使用方式(rules/require-passive-events.js),只有当事件参数的使用"可证明安全"时才允许报告:
- 无参数监听器(
() => {})——不触碰事件对象,必然安全; - 参数为解构模式(如
({target}) => ...)——无法静态追踪,直接视为不安全而跳过(测试 test/require-passive-events.js); - 事件参数被传给其他函数、被
return、被赋给其他变量——引用逃逸,跳过; - 事件参数上发生赋值/更新(如
event.returnValue = false、event[method]())——跳过; - 直接调用
event.preventDefault()——监听器确实要阻止默认行为,跳过(这是正确的豁免); - 只读属性访问(如
event.target、event.currentTarget.dataset.value)——安全,允许报告。
"只读"的判断基于isReadOnlyMemberExpression与 rules/utils/is-left-hand-side.js:如果成员表达式位于赋值左侧、更新表达式、解构模式或delete操作中,就属于"可写",视为不安全。所以event.preventDefault被单独拎出来做豁免判断(isDirectPreventDefaultReference,rules/require-passive-events.js),event["preventDefault"]()这种计算属性写法也逃不出检测(测试 test/require-passive-events.js)。
arguments 对象的追踪
对普通函数(非箭头)监听器,规则还会检查是否使用了arguments(rules/require-passive-events.js),因为function (event) { arguments[0].preventDefault(); }可以通过arguments[0]绕过事件参数引用检查。规则利用 ESLint 的 scope 分析遍历arguments的引用;若arguments被嵌套函数捕获(闭包)同样会判定不安全。测试中function () { arguments[0].preventDefault(); }和嵌套闭包版本均被列为有效代码(test/require-passive-events.js)。
在项目中的启用方式
该规则在文档头部标注为:✅recommended和☑️unopinionated配置中均启用,并且支持--fix自动修复(规则元数据中fixable: 'code'、recommended: 'unopinionated',见 rules/require-passive-events.js)。
在 ESLint 中,最简单的启用方式是在你的配置里加上插件名与规则名:
// eslint.config.js(flat config 示例) import eslintPluginUnicorn from 'eslint-plugin-unicorn'; export default [ { plugins: {unicorn: eslintPluginUnicorn}, rules: { 'unicorn/require-passive-events': 'error', }, }, ];也可以直接继承插件的recommended或unopinionated预设配置(相关配置结构见 configs/flat-config-base.js),此时该规则会自动生效。运行npx eslint --fix .即可让规则自动为满足条件的高频事件监听器补上{passive: true}。
小结
require-passive-events的价值在于:它把一个性能优化建议变成了一条可静态检查、可自动修复的工程规范,同时用严密的 AST 分析和作用域分析把误报率压到最低。从实现上看,它兼顾了四类修复形态(追加对象、布尔展开、对象插入、false 翻转)、三种放弃修复的场景(动态选项、展开/计算属性、动态 passive 值),以及一整套事件参数"安全使用"证明——这些设计共同保证了规则的实用性与可靠性。对团队而言,启用该规则后,滚动手势类事件从代码层面就被强制声明为被动监听,页面滚动响应性有了制度化的保障。
想要深入验证本文描述的行为,可以直接阅读规则源码 rules/require-passive-events.js、测试用例 test/require-passive-events.js 以及快照文件 test/snapshots/require-passive-events.js.snap,对照测试输入逐一理解每条分支的判定结果。
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考