You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何检测函数调用退出调用栈且未执行fnEnd?

基于FinalizationRegistry实现无样板代码的函数日志收尾检测

需求说明

  • 前端Logger类库需支持:应用调用logger.fnStart时返回ScopedLogger实例,用于函数执行期日志记录
  • 自动检测函数返回时未调用fnEnd的场景,触发fnEndNotCalled并传入函数详情
  • 禁止使用函数包装、强制添加try...finally等额外样板代码方案

现有实现的问题

你当前的代码存在几个关键问题导致功能失效:

  1. 注册/注销不匹配:注册FinalizationRegistry时用WeakRef实例作为目标,但unregister传入的是ScopedLogger实例,两者不对应,导致无法成功注销,GC时依然会触发错误回调
  2. 冗余引用影响GC:ScopedLogger实例自身持有weakRef和registry的强引用,可能延迟对象被回收的时机
  3. GC触发逻辑缺失:测试代码仅等待,未主动触发GC(浏览器/Node.js默认不会主动触发)

修正后的实现方案

以下是修复后的完整代码,解决了上述问题并保留核心逻辑:

class ScopedLogger {
  constructor(functionName, registry) {
    this.functionName = functionName;
    this.isFnEndCalled = false;
    // 生成独立注销令牌,确保注册/注销精准匹配
    this.unregisterToken = {};
    // 直接以当前实例为目标注册到FinalizationRegistry
    registry.register(this, functionName, this.unregisterToken);
    
    console.log(`fnStart called for ${functionName}`);
  }

  fnEnd() {
    if (!this.isFnEndCalled) {
      this.isFnEndCalled = true;
      console.log(`fnEnd called for ${this.functionName}`);
      // 使用令牌注销,确保移除对应注册项
      this.registry.unregister(this.unregisterToken);
    }
  }
}

class Logger {
  constructor() {
    // FinalizationRegistry回调即为fnEndNotCalled的核心逻辑
    this.registry = new FinalizationRegistry((functionName) => {
      console.error(`fnEndNotCalled: Function "${functionName}" exited without calling fnEnd`);
    });
  }

  static getInstance() {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  fnStart(functionName) {
    const scopedLogger = new ScopedLogger(functionName, this.registry);
    // 绑定registry到实例,避免构造函数参数冗余
    scopedLogger.registry = this.registry;
    return scopedLogger;
  }
}

// 单例Logger实例
const logger = Logger.getInstance();

// 测试函数
function myFunction() {
  const scopedLogger = logger.fnStart("myFunction");
  console.log("Doing something...");
  // 注释掉此行,GC时会触发fnEndNotCalled
  // scopedLogger.fnEnd();
}

myFunction();

// 开发环境手动触发GC(仅Chrome DevTools开启垃圾回收按钮、或Node.js加--expose-gc参数时生效)
setTimeout(() => {
  console.log("Attempting to trigger GC...");
  typeof gc === 'function' ? gc() : console.log("Manual GC not available in this environment");
}, 1000);

关键改进点

  • 精准注册/注销:使用独立的unregisterToken作为注销标识,彻底解决注册目标不匹配的问题
  • 简化引用结构:移除不必要的WeakRef,直接用ScopedLogger实例作为监控目标,减少引用层级
  • 明确回调逻辑:将FinalizationRegistry的回调直接作为fnEndNotCalled的实现,可按需扩展为自定义钩子

注意事项

  • GC的不确定性:生产环境中GC触发时机不可预测,该方案更适合开发环境调试或非实时的合规性检查
  • 兼容性:FinalizationRegistry是ES2021特性,现代浏览器及Node.js 14.6+均已支持
  • 内存泄漏风险:避免在ScopedLogger实例中保存外部对象的强引用,否则会延迟GC,导致检测失效

内容的提问来源于stack exchange,提问作者DANIEL SSEJJEMBA

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 05:22:12