eslint-plugin-unicorn 的 no-late-current-target-access 规则:禁止在事件派发结束后访问 `event.currentTarget`
2026/9/18 23:47:26 网站建设 项目流程

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的"过期访问"——即事件处理器在awaityieldsetTimeout().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一视同仁,它通过两层过滤来精确定位"事件对象":

  1. 成员表达式形态:只匹配MemberExpression,且属性名必须是字面量currentTarget,对象必须是Identifier(规则源码)。因此event["currentTarget"](计算属性访问)、event.target(不同属性)都不在检测范围内。
  2. 参数名模式:对象名必须匹配共享的正则eventParameterNamePattern——/^(?:e|event|evt|[a-z][\dA-Za-z]*Event)$/u(rules/shared/late-event-handler.js),即eeventevt,或以小写字母开头、以Event结尾的驼峰名(如mouseDownEvent)。
  3. 变量必须是函数参数:通过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):

  • AwaitExpressionawait表达式)
  • YieldExpressionyield表达式)
  • awaitForOfStatementfor 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):

  • ForStatementtestupdatebody都会重复执行;
  • 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)。

官方文档列出的三条已知局限

规则文档明确声明了检测能力的边界,理解这些局限有助于正确评估它的覆盖面:

  1. 嵌套函数一律标记:即使嵌套函数内部完全同步(例如array.map(() => event.currentTarget)),也会被报错。理由是无法静态证明其执行时机,官方建议显式保存到变量以表达"同步引用"的意图。
  2. 非流敏感分析:只要源码中更早的位置出现过awaityieldevent.currentTarget访问就会被标记,即使两者处于互斥分支、永远不可能同时执行(例如condition ? await foo() : event.currentTarget)。这是为了分析简单与确定性的权衡。
  3. 无法追踪事件对象传递:如果把整个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 行)。
  • 生成器 handleryield同样会挂起事件派发,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本质上是在用静态分析弥补"浏览器事件生命周期"这一运行时知识的盲区。它把awaityieldfor 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询