Сохраняйте цепочку объектов, используя асинхронные методы

Допустим, у меня есть класс Test около 10-20 методов, каждый из которых является цепным.

В другом методе мне нужно выполнить асинхронную работу.

let test = new Test();
console.log(test.something()); // Test
console.log(test.asynch()); // undefined since the async code isn't done yet
console.log(test.asynch().something()); // ERROR > My goal is to make this 

Так как любой другой метод является цепным, я чувствую, что было бы странно для пользователя, если этот единственный метод не является.

Есть ли способ для меня сохранить цепную тему моего класса?


Я уже думал о передаче следующего метода в функцию обратного вызова внутри параметра этого метода, но на самом деле это не цепочка.

test.asynch(() => something())

То же самое с Promises Это не совсем цепочка.

test.asynch().then(() => something())

Результат, который я хочу, это

test.asynch().something()

Вот фрагмент, который демонстрирует мою проблему:

class Test {
  /**
   * Executes some async code
   * @returns {Test} The current {@link Test}
   */
  asynch() {
    if (true) { //Condition isn't important
      setTimeout(() => { //Some async stuff
        return this;
      }, 500);
    } else {
      // ...
      return this;
    }
  }

  /**
   * Executes some code
   * @returns {Test} The current {@link Test}
   */
  something() {
    // ...
    return this
  }
}

let test = new Test();
console.log(test.something()); // Test
console.log(test.asynch()); // undefined
console.log(test.asynch().something()); // ERROR > My goal is to make this work.

3 ответа

Решение

Я полагаю, что это действительно хорошая идея сделать что-то подобное. Но использование прокси- сервера позволит создать такое поведение, если исходный объект удовлетворяет определенным условиям. И я очень рекомендую не делать этого таким образом.

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

Один Прокси используется, чтобы обернуть оригинальный класс Test так что есть возможность исправить каждый экземпляр, чтобы сделать их цепочечными.

Второй будет исправлять каждый вызов функции и создает очередь для этих вызовов функций, чтобы они вызывались по порядку.

    class Test {
      /**
       * Executes some async code
       * @returns {Test} The current {@link Test}
       */
      asynch() {
        console.log('asynch')
        return new Promise((resolve, reject) => setTimeout(resolve, 1000))
      }

      /**
       * Executes some code
       * @returns {Test} The current {@link Test}
       */
      something() {
        console.log('something')

        return this
      }
    }


    var TestChainable = new Proxy(Test, {
      construct(target, args) {
        return new Proxy(new target(...args), {

          // a promise used for chaining
          pendingPromise: Promise.resolve(),

          get(target, key, receiver) {
            //  intercept each get on the object
            if (key === 'then' || key === 'catch') {
              // if then/catch is requested, return the chaining promise
              return (...args2) => {
                return this.pendingPromise[key](...args2)
              }
            } else if (target[key] instanceof Function) {
              // otherwise chain with the "chainingPromise" 
              // and call the original function as soon
              // as the previous call finished 
              return (...args2) => {
                this.pendingPromise = this.pendingPromise.then(() => {
                  target[key](...args2)
                })

                console.log('calling ', key)

                // return the proxy so that chaining can continue
                return receiver
              }
            } else {
              // if it is not a function then just return it
              return target[key]
            }
          }
        })
      }
    });

    var t = new TestChainable
    t.asynch()
      .something()
      .asynch()
      .asynch()
      .then(() => {
        console.log('all calles are finished')
      })

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

Различные способы цепочки функций:

Обещаю с потом

bob.bar()
    .then(() => bob.baz())
    .then(() => bob.anotherBaz())
    .then(() => bob.somethingElse());

И вы также можете использовать композиции, чтобы получить другой стиль функционального, многократно используемого синтаксиса для цепочки асинхронных и синхронизирующих функций.

const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));
const transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2);
transformData(data);

Или используя async / await

for (const f of [func1, func2]) {
  await f();
}

Как обсуждалось в комментариях к OP, это может быть достигнуто с помощью прокси.

Я признаю, что t.niese дал аналогичный ответ несколько часов назад. Мой подход несколько отличается, но он по-прежнему по существу перехватывает вызовы методов, возвращает получателя и внутренне накапливает затем свойства.

class ProxyBase {

    constructor () {

        // Initialize a base thennable.
        this.promiseChain = Promise.resolve();

    }

    /**
     * Creates a new instance and returns an object proxying it.
     * 
     * @return {Proxy<ProxyBase>}
     */
    static create () {

        return new Proxy(new this(), {

            // Trap all property access.
            get: (target, propertyName, receiver) => {

                const value = target[propertyName];

                // If the requested property is a method and not a reserved method...
                if (typeof value === 'function' && !['then'].includes(propertyName)) {

                    // Return a new function wrapping the method call.
                    return function (...args) {

                        target.promiseChain = target.promiseChain.then(() => value.apply(target, args));

                        // Return the proxy for chaining.
                        return receiver;

                    }

                } else if (propertyName === 'then') {
                    return (...args) => target.promiseChain.then(...args);
                }

                // If the requested property is not a method, simply attempt to return its value.
                return value;

            }

        });

    }

}

// Sample implementation class. Nonsense lies ahead.
class Test extends ProxyBase {

    constructor () {
        super();
        this.chainValue = 0;
    }

    foo () {
        return new Promise(resolve => {
            setTimeout(() => {
                this.chainValue += 3;
                resolve();
            }, 500);
        });
    }

    bar () {
        this.chainValue += 5;
        return true;
    }

    baz () {
        return new Promise(resolve => {
            setTimeout(() => {
                this.chainValue += 7;
                resolve();
            }, 100);
        });
    }

}

const test = Test.create();

test.foo().bar().baz().then(() => console.log(test.chainValue)); // 15

Другие вопросы по тегам