Asyn c вызов в декораторе TypeScript - PullRequest
0 голосов
/ 25 февраля 2020

Извините заранее за длинный квест. Я стараюсь как можно яснее разобраться с проблемой, с которой я сталкиваюсь.

Я создал декораторы использует библиотеку , и я столкнулся со странным поведением при работе с одним из декораторов ( https://github.com/vlio20/utils-decorators/blob/master/src/after/after.ts).

Декоратор называется «после», и он должен выполнять другую функцию после выполнения декорированного метода. Но в том-то и дело, что если функция возвращает обещание, декоратор должен дождаться его разрешения и только потом вызывать после fun c.

Вот соответствующий код:

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

Как вы видите, я предоставляю флаг декоратору, чтобы указать, что декорированный метод является асин c функцией и возвращает Обещание. Я был бы счастлив прочитать этот флаг, имея следующий код:

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

По сути, я хочу всегда ставить await перед выполнением исходного метода, как я понимаю в случае syn c метод, ожидающий ничего не делает.

Проблема в том, что когда я изменяю код, как предложено выше, следующий модульный тест завершается неудачно:

  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
  });

I создали форк библиотеки, в которой этот точный тест не пройден (https://github.com/vlio20/utils-decorators/pull/new/after-issue).

Что не так с моим восприятием?

Ответы [ 2 ]

1 голос
/ 28 февраля 2020

В принципе, я хочу всегда ставить await перед выполнением оригинального метода, так как из моего понимания в случае метода syn c await ничего не делает.

Это неправда. В соответствии со ссылкой AsyncFunction на MDN (которая сама напрямую ссылается на ECMAScript spe c), любая функция, обозначенная как async, будет всегда выполнять тело функции из обычного последовательность вызовов.

Другими словами, вызываемый объект не имеет значения для функции асинхронного ожидания / ожидания, он всегда разрешается асинхронно. Это важно, поскольку в идеале функция должна быть только синхронной или асинхронной и никогда не быть одновременно. Это закреплено в возвращаемом типе асинхронных c функций: они всегда всегда дают обещание, независимо от того, что происходит внутри них, и обещания никогда не могут проверяться синхронно.

Единственный способ выполнить sh - это вообще не использовать 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
  })
}

Как вы, вероятно, можете сказать из тона из этого ответа (и моих ссылок) я не думаю, что это хороший подход для этого. Желательно, чтобы ваши функции были полностью синхронными или асинхронными.

0 голосов
/ 27 февраля 2020

Я надеюсь, что этот маленький код поможет вам:

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

Он покажет b и только потом a. Потому что если есть await, то функция asyn c всегда будет выполняться чуть позже, и вам нужно ждать ее. Вот почему все ваши функции syn c в тесте работали нормально, а последняя asyn c одна - нет.

Если вы добавите 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 в тесте, либо создание реализации декоратора syn c и asyn c. Например (извините, не знаю машинопись):

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
    }
}
...