eslint-plugin-unicorn 的 no-late-current-target-access 规则:禁止在事件派发结束后访问event.currentTarget
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
no-late-current-target-access是 eslint-plugin-unicorn 内置的一条problem类型规则,用于在静态分析阶段捕获对event.currentTarget的"过期访问"——即事件处理器在await、yield或setTimeout()、.then()等延迟回调中读取currentTarget的场景。本指南以 docs/rules/no-late-current-target-access.md 为主体,结合 规则源码、共享检测器 与 测试用例,完整讲解该问题的成因、规则的检测逻辑、修复方式与已知局限,帮助你在浏览器事件处理代码中避免"读到null"的隐性 bug。
问题本质:currentTarget只在同步派发期间有效
根据 Web API 规范,Event.currentTarget标识"当前正在处理事件的元素",但它只在事件被同步派发的过程中被赋值。一旦事件处理器让出执行权——例如await一个 Promise、在生成器里yield,或者把回调交给setTimeout()/Promise.prototype.then()延迟执行——currentTarget就会被重置为null。
此时再读取它,得到的不是预期的 DOM 元素,而是null,随后调用null.click()、null.disabled之类操作会直接抛出 TypeError 或产生静默失效。这是浏览器事件模型中一个反直觉、但非常真实的陷阱。
正确做法是:在同步执行阶段把event.currentTarget保存到局部变量,之后一律使用该变量,因为普通变量不会随事件派发结束而被清空。
规则触发场景与修复示例
规则文档给出了两组典型反例/正例,全部可在项目中直接验证。
场景一:延迟回调中访问
// ❌ 错误:setTimeout 回调运行时,currentTarget 已经是 null element.addEventListener('click', event => { setTimeout(() => { event.currentTarget.click(); }); }); // ✅ 正确:同步阶段先解构保存 element.addEventListener('click', event => { const {currentTarget} = event; setTimeout(() => { currentTarget.click(); }); });场景二:await之后访问
// ❌ 错误:await 之后 currentTarget 已失效 element.addEventListener('click', async event => { event.currentTarget.disabled = true; await somePromise; event.currentTarget.disabled = false; // 这里读取到 null }); // ✅ 正确 element.addEventListener('click', async event => { const {currentTarget} = event; currentTarget.disabled = true; await somePromise; currentTarget.disabled = false; });注意第一段错误代码里,await之前的那次event.currentTarget.disabled = true是合法的——规则只报告"挂起点之后"的访问,测试用例也专门验证了这一点(test/no-late-current-target-access.js)。
规则如何识别"事件参数":命名模式 + 变量解析
规则并非对所有xxx.currentTarget一视同仁,它通过两层过滤来精确定位"事件对象":
- 成员表达式形态:只匹配
MemberExpression,且属性名必须是字面量currentTarget,对象必须是Identifier(规则源码)。因此event["currentTarget"](计算属性访问)、event.target(不同属性)都不在检测范围内。 - 参数名模式:对象名必须匹配共享的正则
eventParameterNamePattern——/^(?:e|event|evt|[a-z][\dA-Za-z]*Event)$/u(rules/shared/late-event-handler.js),即e、event、evt,或以小写字母开头、以Event结尾的驼峰名(如mouseDownEvent)。 - 变量必须是函数参数:通过
findVariable解析作用域,确认该标识符的defs中存在Parameter类型的定义(规则源码)。这排除了三类干扰:- 局部声明的同名变量:
const event = getEvent()不会被误报; - 全局
event(未解析到参数定义):() => { event.currentTarget }不会被误报; - 内层作用域的遮蔽参数:内层回调自己声明
event参数时,按内层参数判断。
- 局部声明的同名变量:
底层实现:共享的"挂起点跟踪器"
规则的核心逻辑并不散落在自身源码里,而是复用了rules/shared/late-event-handler.js导出的createLateEventHandlerTracker——该共享模块同时服务于no-late-current-target-access与姊妹规则no-late-event-control(后者负责preventDefault/stopPropagation等控制方法调用过晚的问题,见 rules/no-late-event-control.js)。
跟踪器的工作机制如下:
1. 遍历时标记"已挂起"的函数
利用 ESLint 的onExit钩子,在退出三类节点时把其所属函数加入suspendedFunctions集合(rules/shared/late-event-handler.js):
AwaitExpression(await表达式)YieldExpression(yield表达式)- 带
await的ForOfStatement(for await...of循环)
由于这些回调在退出节点时执行,后续访问currentTarget时能看到此前已经经过的挂起点——这就是"源码顺序敏感"的基础。
2. 命中条件:三层判断
规则对每个候选访问节点依次判断(规则源码):
- 是否在嵌套函数中:
getEnclosingFunction(node) !== handlerFunction。若是,直接按in-nested-function报错——因为无法证明嵌套函数一定在同步阶段执行; - 外层函数是否已挂起:
lateEventHandlerTracker.isFunctionSuspended(handlerFunction),命中则报after-suspension; - 是否位于含挂起点的循环重复段内:
isInsideSuspendingLoop(node, handlerFunction)。
三种情况都不满足时,说明访问发生在同步安全区间,不报告。
3. 循环场景的专项处理
isInsideSuspendingLoop遍历从访问节点到 handler 函数之间的祖先链,借助isRepeatedLoopPart判断当前子节点是否为循环的"重复执行部分"(rules/shared/late-event-handler.js):
ForStatement:test、update、body都会重复执行;ForInStatement/ForOfStatement:除right(只求值一次)外的部分都会重复执行;DoWhileStatement/WhileStatement:整体重复。
再结合containsSuspensionPoint(rules/utils/contains-suspension-point.js)判断循环体内是否存在await/yield/for await...of。之所以需要这套机制,是因为循环体可能在第一轮同步执行(此时currentTarget仍有效),但第二轮起就发生在await之后了——静态分析必须把循环体内和循环条件中的访问视为"迟早会挂起后执行"。
值得注意的一个实现细节:containsSuspensionPoint在递归时遇到嵌套函数会直接返回false不深入,因为嵌套函数中的挂起点属于另一个函数,不应影响外层判断(rules/utils/contains-suspension-point.js)。
官方文档列出的三条已知局限
规则文档明确声明了检测能力的边界,理解这些局限有助于正确评估它的覆盖面:
- 嵌套函数一律标记:即使嵌套函数内部完全同步(例如
array.map(() => event.currentTarget)),也会被报错。理由是无法静态证明其执行时机,官方建议显式保存到变量以表达"同步引用"的意图。 - 非流敏感分析:只要源码中更早的位置出现过
await或yield,event.currentTarget访问就会被标记,即使两者处于互斥分支、永远不可能同时执行(例如condition ? await foo() : event.currentTarget)。这是为了分析简单与确定性的权衡。 - 无法追踪事件对象传递:如果把整个
event对象作为参数传给另一个函数,规则无法得知该函数内部何时访问currentTarget,因此这类"迟到访问"不会报错:
function handleEvent(event) { console.log('Too late for', event.currentTarget); // 规则不会报告 } element.addEventListener('click', event => { setTimeout(handleEvent, 1, event); });测试用例揭示的边界行为
规则测试覆盖了 30+ 个场景(test/no-late-current-target-access.js),其中几个边界值得关注:
- 同步赋值目标上的读取不报错:
event.onclick = async event => { event.currentTarget.href = await getUrl(...); }—— 赋值目标在右值求值(含await)之前同步求值,因此合法(测试第 26 行);但若此前已有await,则同样报错(测试第 186-191 行)。 - 作为已
await调用的实参不报错:await foo(event.currentTarget)中参数在挂起前同步求值;而log(await ready(), event.currentTarget.value)中后一个实参在前一个await之后求值,会报错(测试第 104 行)。 for await...of的迭代器表达式不报错:for await (const chunk of event.currentTarget.stream())中迭代器在首次挂起前求值,合法(测试第 32 行)。- 生成器 handler:
yield同样会挂起事件派发,function * (event) { yield ...; event.currentTarget... }会被报错(测试第 93-98 行)。 - 解构参数不报错:
async ({currentTarget}) => { await ...; currentTarget.disabled = false; }中currentTarget是解构出的独立变量,不是成员访问,天然安全(测试第 70-75 行)。 - 遮蔽参数:外层 handler 已挂起、但内层回调声明了自己的
event参数时,内层访问按内层参数判定,不报错(测试第 52-59 行)。 - 嵌套异步回调:内层回调自身
await后再访问外层event.currentTarget,会被归为in-nested-function而非after-suspension(测试第 212-219 行)。
配置方式与规则注册
该规则在 rules/index.js 中注册,规则元数据声明recommended: true(规则源码),因此它默认包含在recommended配置中,而在unopinionated配置中处于禁用状态(见 docs/rules/no-late-current-target-access.md)。想单独开启或关闭,可在 ESLint 配置中:
{ "rules": { "unicorn/no-late-current-target-access": "error", "unicorn/no-late-current-target-access": "off" } }该规则不提供任何可配置选项,仅针对js/js语言(languages元数据中声明),属于开箱即用的纯检测规则。
小结:一条规则背后的防御性编程理念
no-late-current-target-access本质上是在用静态分析弥补"浏览器事件生命周期"这一运行时知识的盲区。它把await、yield、for await...of和延迟回调统一抽象为"挂起点",再结合事件参数命名模式与作用域解析,精确打击"同步阶段之后访问currentTarget"这一整类隐性 bug。它有自己的保守边界(非流敏感、不追踪对象传递),但配合"尽早把currentTarget存入变量"的修复范式,足以让事件处理器代码在异步化重构时保持安全。若你的代码同时涉及preventDefault()/stopPropagation()的时序问题,可进一步查阅共享同一跟踪器的姊妹规则 no-late-event-control 及其源码 rules/no-late-event-control.js。
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考