【问题标题】:Async call in a TypeScript decoratorTypeScript 装饰器中的异步调用
【发布时间】:2026-01-31 10:00:01
【问题描述】:

对于漫长的任务,我们深表歉意。我试图尽可能清楚地解决我面临的问题。

我创建了一个 decorators utils library,在处理其中一个装饰器 (https://github.com/vlio20/utils-decorators/blob/master/src/after/after.ts) 时遇到了一个奇怪的行为。

装饰器被命名为“after”,它应该在执行装饰方法后执行不同的功能。但事情是这样的,如果函数返回一个 Promise,装饰器应该等待它被解析,然后才调用 after func。

相关代码如下:

        if (resolvedConfig.wait) {
          const response = await originalMethod.apply(this, args);
          afterFunc({
            args,
            response
          });
        } else {
          const response = originalMethod.apply(this, args);
          afterFunc({
            args,
            response
          });
        }

正如你所看到的,我为装饰器提供了一个标志,以表明被装饰的方法是一个异步函数并且它返回一个 Promise。我很乐意通过以下代码阅读此标志:

        const response = await originalMethod.apply(this, args);
          afterFunc({
            args,
            response
          });

基本上,我希望始终将await 放在执行原始方法之前,因为据我了解,在同步方法的情况下,await 不会做任何事情。

问题在于,当我按照上面的建议更改代码时,以下单元测试失败:

  it('should verify after method invocation when method is provided', () => {
    let counter = 0;

    const afterFunc = jest.fn(() => {
      expect(counter).toBe(1);
    });

    class T {

      @after<T, void>({
        func: afterFunc
      })
      foo(x: number): void {
        return this.goo(x);
      }

      goo(x: number): void {
        expect(counter++).toBe(0);

        return;
      }
    }

    const t = new T();
    const spyGoo = jest.spyOn(T.prototype, 'goo');

    t.foo(1);
    expect(spyGoo).toBeCalledTimes(1);
    expect(spyGoo).toBeCalledWith(1);
    expect(afterFunc.mock.calls.length).toBe(1); // this line fails
  });

我已经创建了这个精确测试失败的库的一个分支 (https://github.com/vlio20/utils-decorators/pull/new/after-issue)。

我的看法有什么问题?

【问题讨论】:

    标签: javascript typescript unit-testing jestjs decorator


    【解决方案1】:

    基本上,我希望始终将 await 放在原始方法的执行之前,因为根据我的理解,在同步方法的情况下,await 不会做任何事情。

    这不是真的。 According to the AsyncFunction reference on MDN(它本身直接引用 ECMAScript 规范),任何标记为 async 的函数将总是在常规调用序列之外执行函数体。

    换句话说,被调用者与 async/await 函数无关,它会总是异步解析。这一点很重要,因为理想情况下函数应该只有ever be synchronous or asynchronous,而不是两者兼而有之。这体现在异步函数的返回类型中:它们将总是产生一个 Promise,无论它们内部发生什么,并且永远不能同步检查 Promise。

    实现这一点的唯一方法是完全避免使用await/async,并直接检查函数的返回类型:

    const after = ({ func }) => (f) => (..args) => {
      const value = f(...args)
      if ('then' in value === false) {
        func()
        return value 
      }
    
      return value.then(value => {
        func()
        return value
      })
    }
    

    正如您可能从这个答案的语气(以及我的参考资料)中看出的那样,我认为这不是一个好方法。建议让您的函数完全同步或异步。

    【讨论】:

    • 谢谢丹。那么我们如何解释下面的prnt.sc/r8pd5t 产生a 然后产生b?
    • 您使用的是await a(),然后是b()。在支持*async的浏览器中,*控制台被包裹在async function()中,所以执行顺序会如你所愿
    【解决方案2】:

    希望这个小代码可以帮到你:

    a = async () => console.log(await 'a')
    a()
    console.log('b')
    

    它会显示 b,然后才显示 a。因为如果有await,那么异步函数总是会稍后执行,你需要等待它。这就是为什么您在测试中的所有同步函数都运行良好,而最后一个异步函数却没有。

    如果您将await 添加到t.foo(1) 测试应该通过。

    我的意见是最好单独实现afterFunc,幸运的是可以从function.name确定异步

    这是更接近装饰器问题的示例:

    let didDecoratorFinish = false
    
    const decorator = (fn) => {
      return async (...args) => {
        await fn()
        didDecoratorFinish = true
      }
    }
    
    const test = () => {
      let fnWasCalled = false
      const fn = decorator(() => fnWasCalled = true)
      fn()
      console.log(fnWasCalled) // true
      console.log(didDecoratorFinish) // guess what =)
    }
    test()
    

    同样,解决方案是或者在测试中使用await,或者实现同步和异步装饰器。例如(对不起,不知道打字稿):

    const afterFn = function(fn, afterFn) {
      // you can use is-async-function npm package for example
      if (isFunctionAsync(fn))
        return (...args) =>
          new Promise(async (resolve, reject) => {
            try {
              const result = await fn.apply(this, args)
              await afterFn() // I don't know if you want to wait for afterFn
              resolve(result)
            } catch (err) {
              reject(err)
            }
          })
      else
        return (...args) => {
          const result = fn.apply(this, args)
          afterFn() // it can be async, I don't know if you want to wait for it
          return result
        }
    }
    

    【讨论】:

    • 你在屏幕截图上等待,而不是在测试中
    • 等待在装饰器中