Реализация обещаний в JavaScript

realizacziya obeshhanij v javascript

Больше всего мне нравится программирование ага! момент, когда вы начинаете полностью понимать концепцию. Несмотря на то, что для этого может потребоваться много времени и немалых усилий, оно того стоит.

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

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

Я надеюсь, что эта статья поможет вам разобраться с реализацией обещаний JavaScript.

Мы сосредоточимся на том, как реализовать ядро ​​обещания в соответствии со спецификацией Promises/A+ с помощью нескольких методов Bluebird API. Мы также будем использовать подход TDD из Jest.

TypeScript также пригодится.

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

Теперь, когда мы решили это сделать, клонируйте хранилище и начнем.

Суть обещания

Как известно, обещание – это объект со следующими свойствами:

Затем

Метод, который присоединяет обработчик к нашему обещанию. Он возвращает новое обещание со значением предыдущего, сопоставимого одним из способов обработчика.

Обработчики

Массив обработчиков, подсоединенный к потом. Обработчик – это объект, содержащий два метода на успех и onFailоба из которых передаются как аргументы потом(на успех, onFail).

type HandlerOnSuccess<T, U = any> = (value: T) => U | Thenable<U>;
type HandlerOnFail<U = any> = (reason: any) => U | Thenable<U>;

interface Handler<T, U> {
  onSuccess: HandlerOnSuccess<T, U>;
  onFail: HandlerOnFail<U>;
}

государство

Обещание может быть в одном из трех состояний: решено, отклонено, или на рассмотрении.

Решено означает, что все прошло гладко, и мы получили наше значение, или мы поймали и обработали ошибку.

Отклонено означает, что либо мы отклонили обещание, либо возникла ошибка, и мы его не уловили.

Ожидает означает, что нет решить нет отвергнуть метод еще был вызван, и мы все еще ждем значения.

Термин «обещание выполнено» означает, что обещание либо решено, либо отклонено.

Значение

Значение, которое мы решили или отклонили.

После установки значение невозможно изменить его.

Тестирование

Согласно подходу TDD, мы хотим написать наши тесты до того, как появится фактический код, так что давайте сделаем именно это.

Вот тесты для нашего ядра:

describe('PQ <constructor>', () => {
  test('resolves like a promise', () => {
    return new PQ<number>((resolve) => {
      setTimeout(() => {
        resolve(1);
      }, 30);
    }).then((val) => {
      expect(val).toBe(1);
    });
  });

  test('is always asynchronous', () => {
    const p = new PQ((resolve) => resolve(5));

    expect((p as any).value).not.toBe(5);
  });

  test('resolves with the expected value', () => {
    return new PQ<number>((resolve) => resolve(30)).then((val) => {
      expect(val).toBe(30);
    });
  });

  test('resolves a thenable before calling then', () => {
    return new PQ<number>((resolve) =>
      resolve(new PQ((resolve) => resolve(30))),
    ).then((val) => expect(val).toBe(30));
  });

  test('catches errors (reject)', () => {
    const error = new Error('Hello there');

    return new PQ((resolve, reject) => {
      return reject(error);
    }).catch((err: Error) => {
      expect(err).toBe(error);
    });
  });

  test('catches errors (throw)', () => {
    const error = new Error('General Kenobi!');

    return new PQ(() => {
      throw error;
    }).catch((err) => {
      expect(err).toBe(error);
    });
  });

  test('is not mutable - then returns a new promise', () => {
    const start = new PQ<number>((resolve) => resolve(20));

    return PQ.all([
      start
        .then((val) => {
          expect(val).toBe(20);
          return 30;
        })
        .then((val) => expect(val).toBe(30)),
      start.then((val) => expect(val).toBe(20)),
    ]);
  });
});

Запуск наших тестов

Я настоятельно рекомендую использовать расширение Jest для Visual Studio. Он выполняет наши тесты в фоновом режиме для нас и показывает результат прямо между строками нашего кода в виде зеленых и красных точек для пройденных и неудачных тестов соответственно.

Для просмотра результатов откройте консоль «Выход» и выберите вкладку «Jest».

0*dr7riPl5ZRkUF8lo

Мы также можем запустить наши тесты, выполнив следующую команду:

npm run test

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

Давайте изменим это.

Реализация ядра Promise

конструктор

class PQ<T> {
  private state: States = States.PENDING;
  private handlers: Handler<T, any>[] = [];
  private value: T | any;
  public static errors = errors;

  public constructor(callback: (resolve: Resolve<T>, reject: Reject) => void) {
    try {
      callback(this.resolve, this.reject);
    } catch (e) {
      this.reject(e);
    }
  }
}

Наш конструктор принимает a обратный звонок как параметр.

Мы называем этот обратный звонок с это.решить и это. как аргументы.

Заметьте, что обычно мы бы связались это.решить и это. к этоно здесь вместо этого мы использовали метод стрелки класса.

setResult

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

class PQ<T> {

  // ...
  
  private setResult = (value: T | any, state: States) => {
    const set = () => {
      if (this.state !== States.PENDING) {
        return null;
      }

      if (isThenable(value)) {
        return (value as Thenable<T>).then(this.resolve, this.reject);
      }

      this.value = value;
      this.state = state;

      return this.executeHandlers();
    };

    setTimeout(set, 0);
  };
}

Сначала проверяем, нет ли штата на рассмотрении — если это так, значит, обещание уже выполнено, и мы не можем назначить ему новое значение.

Затем нам нужно проверить, есть ли значение a тогда можно. Проще говоря, а тогда можно является объектом с потом как метод.

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

Однажды тогда можно согласуется, он вызовет один из наших методов и придаст нам ожидаемую непредсказуемую ценность.

Итак, теперь мы должны проверить, есть ли объект a тогда можно.

describe('isThenable', () => {
  test('detects objects with a then method', () => {
    expect(isThenable({ then: () => null })).toBe(true);
    expect(isThenable(null)).toBe(false);
    expect(isThenable({})).toBe(false);
  });
});
const isFunction = (func: any) => typeof func === 'function';

const isObject = (supposedObject: any) =>
  typeof supposedObject === 'object' &&
  supposedObject !== null &&
  !Array.isArray(supposedObject);

const isThenable = (obj: any) => isObject(obj) && isFunction(obj.then);

Важно отдавать себе отчет, что наше обещание никогда не будет синхронным, даже если код внутри обратный звонок есть

Мы собираемся отложить исполнение до следующей итерации цикла событий с помощью setTimeout.

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

executeHandlers

class PQ<T> {

  // ...
  
  private executeHandlers = () => {
    if (this.state === States.PENDING) {
      return null;
    }

    this.handlers.forEach((handler) => {
      if (this.state === States.REJECTED) {
        return handler.onFail(this.value);
      }

      return handler.onSuccess(this.value);
    });

    this.handlers = [];
  };
}

Опять же убедитесь, что государство не является на рассмотрении.

Состояние обещания определяет, какую функцию мы собираемся использовать.

Если это так решеномы должны выполнить на успехиначе — onFail.

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

И это то, что мы должны обсудить: способ подключения нашего обработчика.

attachHandler

class PQ<T> {

  // ...
  
  private attachHandler = (handler: Handler<T, any>) => {
    this.handlers = [...this.handlers, handler];

    this.executeHandlers();
  };
}

Это действительно так просто, как кажется. Мы просто добавляем обработчик к нашему массиву обработчиков и выполняем его. Это оно.

Теперь, чтобы объединить все это нам нужно реализовать потом метод.

потом

class PQ<T> {

  // ...
  
  public then<U>(
    onSuccess?: HandlerOnSuccess<T, U>,
    onFail?: HandlerOnFail<U>,
  ) {
    return new PQ<U | T>((resolve, reject) => {
      return this.attachHandler({
        onSuccess: (result) => {
          if (!onSuccess) {
            return resolve(result);
          }

          try {
            return resolve(onSuccess(result));
          } catch (e) {
            return reject(e);
          }
        },
        onFail: (reason) => {
          if (!onFail) {
            return reject(reason);
          }

          try {
            return resolve(onFail(reason));
          } catch (e) {
            return reject(e);
          }
        },
      });
    });
  }
}

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

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

Здесь нужно помнить, что ни один из обработчиков не перешел в потом необходимо. Однако важно, чтобы мы не пытались выполнить что-нибудь, что может быть неопределенный.

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

выловить

Выловить на самом деле является лишь абстракцией над потом метод.

class PQ<T> {

  // ...
  
  public catch<U>(onFail: HandlerOnFail<U>) {
    return this.then<U>(identity, onFail);
  }
}

Это оно.

Наконец

Наконец также является лишь абстракцией над исполнением потом(наконецCb, наконецCb), потому что его не очень волнует результат обещания.

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

describe('PQ.prototype.finally', () => {
  test('it is called regardless of the promise state', () => {
    let counter = 0;
    return PQ.resolve(15)
      .finally(() => {
        counter += 1;
      })
      .then(() => {
        return PQ.reject(15);
      })
      .then(() => {
        // wont be called
        counter = 1000;
      })
      .finally(() => {
        counter += 1;
      })
      .catch((reason) => {
        expect(reason).toBe(15);
        expect(counter).toBe(2);
      });
  });
});
class PQ<T> {

  // ...
  

  public finally<U>(cb: Finally<U>) {
    return new PQ<U>((resolve, reject) => {
      let val: U | any;
      let isRejected: boolean;

      return this.then(
        (value) => {
          isRejected = false;
          val = value;
          return cb();
        },
        (reason) => {
          isRejected = true;
          val = reason;
          return cb();
        },
      ).then(() => {
        if (isRejected) {
          return reject(val);
        }

        return resolve(val);
      });
    });
  }
}

toString

describe('PQ.prototype.toString', () => {
  test('returns [object PQ]', () => {
    expect(new PQ<undefined>((resolve) => resolve()).toString()).toBe(
      '[object PQ]',
    );
  });
});
class PQ<T> {

  // ...
  
  public toString() {
    return `[object PQ]`;
  }
}

Он просто вернет строчку [object PQ].

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

Дополнительные методы

Обещайте.решите

Как это должно работать?

describe('PQ.resolve', () => {
  test('resolves a value', () => {
    return PQ.resolve(5).then((value) => {
      expect(value).toBe(5);
    });
  });
});
class PQ<T> {

  // ...
  
  public static resolve<U = any>(value?: U | Thenable<U>) {
    return new PQ<U>((resolve) => {
      return resolve(value);
    });
  }
}

Обещать.

Как это должно работать?

describe('PQ.reject', () => {
  test('rejects a value', () => {
    return PQ.reject(5).catch((value) => {
      expect(value).toBe(5);
    });
  });
});
class PQ<T> {

  // ...
  
  public static reject<U>(reason?: any) {
    return new PQ<U>((resolve, reject) => {
      return reject(reason);
    });
  }
}

Обещай.

Как это должно работать?

describe('PQ.all', () => {
  test('resolves a collection of promises', () => {
    return PQ.all([PQ.resolve(1), PQ.resolve(2), 3]).then((collection) => {
      expect(collection).toEqual([1, 2, 3]);
    });
  });

  test('rejects if one item rejects', () => {
    return PQ.all([PQ.resolve(1), PQ.reject(2)]).catch((reason) => {
      expect(reason).toBe(2);
    });
  });
});
class PQ<T> {

  // ...
  
  public static all<U = any>(collection: (U | Thenable<U>)[]) {
    return new PQ<U[]>((resolve, reject) => {
      if (!Array.isArray(collection)) {
        return reject(new TypeError('An array must be provided.'));
      }

      let counter = collection.length;
      const resolvedCollection: U[] = [];

      const tryResolve = (value: U, index: number) => {
        counter -= 1;
        resolvedCollection[index] = value;

        if (counter !== 0) {
          return null;
        }

        return resolve(resolvedCollection);
      };

      return collection.forEach((item, index) => {
        return PQ.resolve(item)
          .then((value) => {
            return tryResolve(value, index);
          })
          .catch(reject);
      });
    });
  }
}

Я считаю, что реализация достаточно проста.

Начиная с collection.length, мы отсчитываем с каждым попробуйтеResolve пока мы не дойдем до 0, что означает, что каждый элемент коллекции решен. Затем мы решаем новую коллекцию.

Обещайте.любые

Как это должно работать?

describe('PQ.any', () => {
  test('resolves the first value', () => {
    return PQ.any<number>([
      PQ.resolve(1),
      new PQ((resolve) => setTimeout(resolve, 15)),
    ]).then((val) => expect(val).toBe(1));
  });

  test('rejects if the first value rejects', () => {
    return PQ.any([
      new PQ((resolve) => setTimeout(resolve, 15)),
      PQ.reject(1),
    ]).catch((reason) => {
      expect(reason).toBe(1);
    });
  });
});
class PQ<T> {

  // ...

  public static any<U = any>(collection: (U | Thenable<U>)[]) {
    return new PQ<U>((resolve, reject) => {
      return collection.forEach((item) => {
        return PQ.resolve(item)
          .then(resolve)
          .catch(reject);
      });
    });
  }
}

Мы просто ждем решения первого значения и возвращаем его в обещание.

Обещание.

Как это должно работать?

describe('PQ.props', () => {
  test('resolves object correctly', () => {
    return PQ.props<{ test: number; test2: number }>({
      test: PQ.resolve(1),
      test2: PQ.resolve(2),
    }).then((obj) => {
      return expect(obj).toEqual({ test: 1, test2: 2 });
    });
  });

  test('rejects non objects', () => {
    return PQ.props([]).catch((reason) => {
      expect(reason).toBeInstanceOf(TypeError);
    });
  });
});
class PQ<T> {

  // ...
  
  public static props<U = any>(obj: object) {
    return new PQ<U>((resolve, reject) => {
      if (!isObject(obj)) {
        return reject(new TypeError('An object must be provided.'));
      }

      const resolvedObject = {};

      const keys = Object.keys(obj);
      const resolvedValues = PQ.all<string>(keys.map((key) => obj[key]));

      return resolvedValues
        .then((collection) => {
          return collection.map((value, index) => {
            resolvedObject[keys[index]] = value;
          });
        })
        .then(() => resolve(resolvedObject as U))
        .catch(reject);
    });
  }
}

Мы выбираем ключи переданного объекта, решая каждое значение. Затем мы назначаем значение новому объекту и решаем с ним обещание.

Promise.prototype.spread

Как это должно работать?

describe('PQ.protoype.spread', () => {
  test('spreads arguments', () => {
    return PQ.all<number>([1, 2, 3]).spread((...args) => {
      expect(args).toEqual([1, 2, 3]);
      return 5;
    });
  });

  test('accepts normal value (non collection)', () => {
    return PQ.resolve(1).spread((one) => {
      expect(one).toBe(1);
    });
  });
});
class PQ<T> {

  // ...
  
  public spread<U>(handler: (...args: any[]) => U) {
    return this.then<U>((collection) => {
      if (Array.isArray(collection)) {
        return handler(...collection);
      }

      return handler(collection);
    });
  }
}

Обещание.

Как это должно работать?

describe('PQ.delay', () => {
  test('waits for the given amount of miliseconds before resolving', () => {
    return new PQ<string>((resolve) => {
      setTimeout(() => {
        resolve('timeout');
      }, 50);

      return PQ.delay(40).then(() => resolve('delay'));
    }).then((val) => {
      expect(val).toBe('delay');
    });
  });

  test('waits for the given amount of miliseconds before resolving 2', () => {
    return new PQ<string>((resolve) => {
      setTimeout(() => {
        resolve('timeout');
      }, 50);

      return PQ.delay(60).then(() => resolve('delay'));
    }).then((val) => {
      expect(val).toBe('timeout');
    });
  });
});
class PQ<T> {

  // ...
  
  public static delay(timeInMs: number) {
    return new PQ((resolve) => {
      return setTimeout(resolve, timeInMs);
    });
  }
}

С помощью setTimeout, мы просто задерживаем исполнение решить функции на заданное количество миллисекунд.

Promise.prototype.timeout

Как это должно работать?

describe('PQ.prototype.timeout', () => {
  test('rejects after given timeout', () => {
    return new PQ<number>((resolve) => {
      setTimeout(resolve, 50);
    })
      .timeout(40)
      .catch((reason) => {
        expect(reason).toBeInstanceOf(PQ.errors.TimeoutError);
      });
  });

  test('resolves before given timeout', () => {
    return new PQ<number>((resolve) => {
      setTimeout(() => resolve(500), 500);
    })
      .timeout(600)
      .then((value) => {
        expect(value).toBe(500);
      });
  });
});
class PQ<T> {

  // ...
  
  public timeout(timeInMs: number) {
    return new PQ<T>((resolve, reject) => {
      const timeoutCb = () => {
        return reject(new PQ.errors.TimeoutError());
      };

      setTimeout(timeoutCb, timeInMs);

      return this.then(resolve);
    });
  }
}

Это немного сложнее.

Если setTimeout выполняется быстрее потом в нашем обещании, он отклонит обещание с нашей особой ошибкой.

Обещать.

Как это должно работать?

describe('PQ.promisify', () => {
  test('works', () => {
    const getName = (firstName, lastName, callback) => {
      return callback(null, `${firstName} ${lastName}`);
    };

    const fn = PQ.promisify<string>(getName);
    const firstName="Maciej";
    const lastName="Cieslar";

    return fn(firstName, lastName).then((value) => {
      return expect(value).toBe(`${firstName} ${lastName}`);
    });
  });
});
class PQ<T> {

  // ...
  
  public static promisify<U = any>(
    fn: (...args: any[]) => void,
    context = null,
  ) {
    return (...args: any[]) => {
      return new PQ<U>((resolve, reject) => {
        return fn.apply(context, [
          ...args,
          (err: any, result: U) => {
            if (err) {
              return reject(err);
            }

            return resolve(result);
          },
        ]);
      });
    };
  }
}

Мы применяем к функции все переданные аргументы, плюс – как последний – придаем ошибку сначала обратный звонок.

Promise.promisifyAll

Как это должно работать?

describe('PQ.promisifyAll', () => {
  test('promisifies a object', () => {
    const person = {
      name: 'Maciej Cieslar',
      getName(callback) {
        return callback(null, this.name);
      },
    };

    const promisifiedPerson = PQ.promisifyAll<{
      getNameAsync: () => PQ<string>;
    }>(person);

    return promisifiedPerson.getNameAsync().then((name) => {
      expect(name).toBe('Maciej Cieslar');
    });
  });
});
class PQ<T> {

  // ...
  
  public static promisifyAll<U>(obj: any): U {
    return Object.keys(obj).reduce((result, key) => {
      let prop = obj[key];

      if (isFunction(prop)) {
        prop = PQ.promisify(prop, obj);
      }

      result[`${key}Async`] = prop;

      return result;
    }, {}) as U;
  }
}

Перебираем ключи объекта и обещать его методы и добавьте к каждому имени метода слово Асинхронный.

Подведению

Здесь были представлены некоторые из всех методов Bluebird API, поэтому я настоятельно призываю вас изучить, поиграть с ними и попытаться реализовать остальные.

Сначала это может показаться тяжелым, но не отчаивайтесь — это было бы бесполезно, если бы это было легко.

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

Если у вас есть вопросы или комментарии, не стесняйтесь оставлять их в разделе комментариев ниже или отправлять мне сообщение.

Просмотрите мои социальные сети!

Присоединяйтесь к моей рассылке!

Первоначально опубликовано на www.mcieslar.com 4 августа 2018 года.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *