ethers.js监听智能合约事件:Contract.on与response.wait()哪个更好?
ethers.js 交易状态同步:
wait() 与 on() 的选型与实践 两种方案不存在绝对的优劣,二者解决的核心场景完全不同,生产环境通常不会二选一,而是搭配使用。
两种方案的核心特性对比
TransactionResponse.wait() 方案
这是针对单条交易生命周期追踪的方法,逻辑是持有交易hash轮询(或等待节点推送)该交易的上链状态,直到交易达到指定的区块确认数后返回交易回执。
- 优势:
- 和用户操作强绑定:能精准追踪当前用户在当前会话中发起的交易,不需要额外做事件匹配,适合给操作的用户做即时反馈
- 能捕获交易全状态:交易成功上链、交易被节点拒收、交易执行revert等状态都能通过wait的resolve/reject捕获,不会漏过失败场景
- 实现简单:不需要额外维护长连接、监听器生命周期,逻辑线性好理解
- 劣势:
- 覆盖范围极窄:只能追踪你主动发起、且持有交易对象的交易,其他用户发起的交易、用户刷新页面/断连重连前发起的交易都无法追踪
- 日志解析成本高:wait返回的回执里的事件是原始log格式,需要手动用合约接口解码才能拿到事件参数
- 逻辑冗余:如果应用多个模块需要同步同一状态变更,每个发起交易的位置都要写状态更新逻辑,维护成本高
Contract.on() 事件订阅方案
这是针对全局合约事件监听的方法,逻辑是和RPC节点建立长连接,订阅对应合约的指定事件,只要链上任意地址触发了该事件,节点就会把事件推送给前端。
- 优势:
- 覆盖范围全:不管是哪个用户发起的交易、甚至是合约底层逻辑自动触发的事件,都能监听到,适合做全局状态同步
- 开箱即用的解析能力:ethers会自动把事件参数解码成JS对象,不需要手动处理原始log
- 全局统一维护:只需要在应用入口初始化一次监听,所有模块都能同步拿到状态变更,不需要重复写逻辑
- 劣势:
- 稳定性差:强依赖RPC节点的长连接支持,大部分公共RPC的订阅连接不稳定,断连期间的事件会直接丢失,需要自己实现重连、补拉历史区块事件的兜底逻辑
- 无法直接关联用户操作:监听到的事件无法直接判断是不是当前用户刚发起的交易触发的,需要自己拿交易hash、触发地址和本地待确认交易做匹配,否则容易出现“用户A发交易,用户B的页面弹交易成功提示”的错乱
- 容易出现内存泄漏:在React这类组件化框架里,如果组件卸载时没有手动调用
removeListener清理监听器,会出现回调重复触发的问题 - 无法捕获失败场景:只有交易成功执行上链才会抛出事件,交易在内存池被丢弃、执行revert的场景完全收不到通知,没法给用户做失败反馈
行业通用实践
标准分工方案
不要二选一,按场景拆分职责搭配使用:
- 用户操作反馈链路用
wait():
用户发起交易后,立刻展示「交易确认中」的状态,调用wait()等待交易上链:如果抛出错误就提示交易失败,如果返回成功就给当前操作的用户弹即时成功提示,不需要等待全局事件同步。 - 全局状态同步链路用
Contract.on():
在应用初始化层(比如React的全局状态层、入口组件)统一注册事件监听,收到事件后更新全局状态,所有业务组件从全局状态取数渲染,保证所有来源的状态变更都能同步到全应用。同时加兜底逻辑:每次页面加载、订阅重连时,用contract.queryFilter拉取最近10-20个区块的对应事件,补上断连期间漏掉的事件,避免状态不一致。
React开发额外注意事项
- 所有通过
on注册的监听器,必须在useEffect的清理函数中调用contract.removeListener移除,避免组件重渲染导致监听器重复注册 wait()逻辑只处理当前操作的用户反馈,不要在wait回调里直接更新全局状态,避免和事件监听的更新逻辑重复触发导致页面闪烁- 如果用的公共RPC不支持长连接订阅,可以直接放弃
on,改用定时轮询queryFilter拉取最新区块事件,延迟在几秒级别,对学习场景的简单dApp完全够用。
生产级应用的优化方向:如果后续要做正式上线的dApp,通常不会直接在前端连RPC做事件订阅,会使用专门的链上数据索引服务同步合约事件,稳定性更高,也不需要自己处理重连、补块的逻辑。学习阶段先掌握原生的两种方法即可。
内容的提问来源于stack exchange,提问作者Masa
相关产品推荐
相关产品推荐

