На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
Модераторы: ElcnU, ANDLL, fatalist
  
> Иммитация промиса
    Написал свой класс Promise :D
    есть трабла никак не могу понять как реализовать возврат промиса из колбэка :huh:
    ExpandedWrap disabled
      const States = {
          PENDING: 'pending',
          FULFILLED: 'fulfilled',
          REJECTED: 'rejected'
      };
       
      export class Promise {
          constructor(executor) {
              this.state = States.PENDING;
              this.value = undefined;
              this.callbacks = [];
       
              const handleState = (newState, result) => {
                  if (this.state === States.PENDING) {
                      const then = result && result.then;
                      if (typeof then === 'function') {
                          return then(resolve, reject);
                      }
                      this.state = newState;
                      this.value = result;
                      this.callbacks.forEach(cb => {
                          cb[this.state](result);
                      });
                  }
              };
              const resolve = result => handleState(States.FULFILLED, result);
              const reject = reason => handleState(States.REJECTED, reason);
       
              try {
                  executor(resolve, reject);
              } catch (error) {
                  reject(error);
              }
          }
       
          then(onfulfilled, onrejected) {
              return new Promise((resolve, reject) => {
                  const fulfilled = value => resolve(onfulfilled ? onfulfilled(value) : value);
                  const rejected = reason => reject(onrejected ? onrejected(reason) : reason);
       
                  if (this.state === States.FULFILLED) {
                      return fulfilled(this.value);
                  }
                  if (this.state === States.REJECTED) {
                      return rejected(this.value);
                  }
                  this.callbacks.push({ fulfilled, rejected });
              });
          }
       
          catch(onrejected) {
              return this.then(null, onrejected);
          }
      }

    тестирую
    ExpandedWrap disabled
      import { Promise } from './my-promise';
       
      describe('Test Promise class', () => {
          test('test 1', () => {
              return new Promise(resolve => {
                  setTimeout(() => {
                      resolve('Hello');
                  }, 0);
              })
              .then(val => {
                  return val + ' World!!!';
              })
              .then(val => {
                  expect(val).toBe('Hello World!!!');
              });
          });
       
          test('test 2', () => {
              return new Promise(resolve => {
                  setTimeout(() => {
                      resolve('Hello');
                  }, 0);
              })
              .then(val => {
                  return new Promise((resolve, reject) =>{
                      resolve(val + ' World!!!');
                  });
              })
              .then(val => {
                  expect(val).toBe('Hello World!!!');
              });
          });
       
          test('test 3', () => {
              return new Promise(resolve => {
                  setTimeout(() => {
                      resolve('Hello');
                  }, 0);
              })
              .then(val => {
                  return new Promise((resolve, reject) =>{
                      setTimeout(() => {  
                          resolve(val + ' World!!!');
                      }, 0);
                  });
              })
              .then(val => {
                  expect(val).toBe('Hello World!!!');
              });
          });
      });

    два последних теста не проходят, пишет
    ExpandedWrap disabled
      Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error:

    Где я накосячил? :wacko:
      Нашёл баг :D
      Я совершил ошибку, о которой сам часто предупреждал! Это смена контекста функции, в моём случае это имело место в handleState. Есть несколько методов решения, я привёл самый любимый :D:
      ExpandedWrap disabled
        ...
                const handleState = (newState, result) => {
                    if (this.state === States.PENDING) {
                        const then = result && result.then; //<-- тут
                        const then = result && result.then.bind(result); //<-- биндим, хотя можно просто переместить проверку в if и не биндить :D
         
                        if (typeof then === 'function') {
                            return then(resolve, reject);
                        }
                        this.state = newState;
                        this.value = result;
                        this.callbacks.forEach(cb => {
                            cb[this.state](result);
                        });
                    }
                };
        ...
        Добавил асинхронность в промис через setImmediate:
        ExpandedWrap disabled
          ...
                  const handleState = (newState, result) => {
                      setImmediate(() => {
                          if (this.state === States.PENDING) {
                              if (result && typeof result.then === 'function') {
                                  return result.then(resolve, reject);
                              }
                              this.state = newState;
                              this.value = result;
                              this.callbacks.forEach(cb => {
                                  cb[this.state](result);
                              });
                          }
                      });
                  };
          ...

        тестируем все ОК
        ExpandedWrap disabled
          ...
              test('test 4: asynchronous behaviour', () => {
                  const p = new Promise(resolve => resolve(5));
                  expect(p.value).not.toBe(5);
              });
          ...

        а вот следущие два теста на обработку ошибки не проходят...
        где то я накосячил :huh:
        ExpandedWrap disabled
          ...
              test('test 7: catches errors (reject)', () => {
                  const error = new Error('Error!!!');
                  return new Promise((resolve, reject) => {
                      reject(error);
                  })
                  .catch(err => {
                      expect(err).toBe(error);
                  });
              });
           
              test('test 8: catches errors (throw)', () => {
                  const error = new Error('Error!!!');
                  return new Promise(() => {
                      throw error;
                  })
                  .catch(err => {
                      expect(err).toBe(error);
                  });
              });
          ...
        Сообщение отредактировано: Cfon -
          интересно то, что если вызвать этот код не из тестовой среды Jest, то он отрабатывает как надо :wall:
          ExpandedWrap disabled
            // test
            var p = new Promise((resolve, reject) => {
                reject('Error!!!');
            })
            .catch(console.error);
            // Error!!!
            Нашел баг :D
            Как оказалось я ошибся в логике функции rejected. В нем вызывался всегда reject, что было ошибкой поскольку по спецификации Promise/A+ отказ должен быть обработан только один раз, что в случае с reject приводило к повторному вызову колбэка.
            Выход? Все просто! Надо вместо reject вызывать resolve в случае наличия колбэка и reject в его отсутствии :D
            ExpandedWrap disabled
              ...
                  then(onfulfilled, onrejected) {
                      return new Promise((resolve, reject) => {
                          const fulfilled = value => {
                              resolve(onfulfilled ? onfulfilled(value) : value);
                          };
               
                          const rejected = reason => {
                              // reject(onrejected ? onrejected(reason) : reason); //<-- no
               
                              if (typeof onrejected === 'function') {
                                  resolve(onrejected(reason)); //<-- yes
                              } else {
                                  reject(reason);
                              }
                          };
               
                          if (this.state === States.FULFILLED) {
                              return fulfilled(this.value);
                          }
                          if (this.state === States.REJECTED) {
                              return rejected(this.value);
                          }
                          this.callbacks.push({ fulfilled, rejected });
                      });
                  }
              ...

            Теперь все пучком! Промис полностью функционален и протестирован! :victory:
            Итак вот конечный код промиса:
            ExpandedWrap disabled
              const States = {
                  PENDING: 'pending',
                  FULFILLED: 'fulfilled',
                  REJECTED: 'rejected'
              };
               
              export class Promise {
                  constructor(executor) {
                      this.state = States.PENDING;
                      this.value = undefined;
                      this.callbacks = [];
               
                      const handleState = (newState, result) => {
                          setImmediate(() => {
                              if (this.state === States.PENDING) {
                                  if (result && typeof result.then === 'function') {
                                      return result.then(resolve, reject);
                                  }
                                  
                                  this.state = newState;
                                  this.value = result;
                                  this.callbacks.forEach(cb => {
                                      cb[this.state](result);
                                  });
                              }
                          });
                      };
                      const resolve = result => handleState(States.FULFILLED, result);
                      const reject = reason => handleState(States.REJECTED, reason);
               
                      try {
                          executor(resolve, reject);
                      } catch (error) {
                          reject(error);
                      }
                  }
               
                  then(onfulfilled, onrejected) {
                      return new Promise((resolve, reject) => {
                          const fulfilled = value => {
                              resolve(onfulfilled ? onfulfilled(value) : value);
                          };
               
                          const rejected = reason => {
                              if (typeof onrejected === 'function') {
                                  resolve(onrejected(reason));
                              } else {
                                  reject(reason);
                              }
                          };
               
                          if (this.state === States.FULFILLED) {
                              return fulfilled(this.value);
                          }
                          if (this.state === States.REJECTED) {
                              return rejected(this.value);
                          }
                          this.callbacks.push({ fulfilled, rejected });
                      });
                  }
               
                  catch(onrejected) {
                      return this.then(null, onrejected);
                  }
              }

            получилось 65 строчек кода :D

            Позже выложу диалог Масты и Новичка.
            Сообщение отредактировано: Cfon -
              Еще одна реализация промиса, где логика реализована через члены класса, а не через замыкания:
              ExpandedWrap disabled
                class Promise {
                    constructor(executor) {
                        this.state = States.PENDING;
                        this.value = undefined;
                        this.callbacks = [];
                 
                        try {
                            executor(this._resolve.bind(this), this._reject.bind(this));
                        } catch (error) {
                            this._reject(error);
                        }
                    }
                 
                    _handleState(newState, result) {
                        if (result && typeof result.then === 'function') {
                            return result.then(this._resolve.bind(this), this._reject.bind(this));
                        }
                 
                        setImmediate(() => {
                            if (this.state === States.PENDING) {
                                this.state = newState;
                                this.value = result;
                                this.callbacks.forEach(cb => {    
                                    var callback = cb[this.state];
                                    var next = cb.next;
                 
                                    if (typeof callback === 'function') {
                                        this.state = States.FULFILLED;
                                        try {
                                            this.value = callback(result);
                                        } catch(error) {
                                            next._reject(error);
                                        }
                                    }
                 
                                    if (this.state === States.FULFILLED) {
                                        return next._resolve(this.value);
                                    }
                 
                                    if (this.state === States.REJECTED) {
                                        return next._reject(this.value);
                                    }
                                });
                            }
                        });
                    }
                    
                    _resolve(result) {
                        this._handleState(States.FULFILLED, result);
                    }
                    
                    _reject(reason) {
                        this._handleState(States.REJECTED, reason);
                    }
                 
                    then(onfulfilled, onrejected) {
                        var next = new Promise(() => {});
                        this.callbacks.push({
                            fulfilled: onfulfilled,
                            rejected: onrejected,
                            next
                        });
                        return next;
                    }
                 
                    catch(onrejected) {
                        return this.then(null, onrejected);
                    }
                }

              Оба эти кода затрагивают практически все стороны JS.
              Обе реализации рекомендуются к изучению для достижения степени Мастера JS :D

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

                Жги Новичок! :D

                - Я просмотрел ваш последний код из поста №6 и никак не могу понять для чего в нем юзается массив callbacks, а не просто объект callbacks? :huh:

                Как это? :blink:

                - Ну вот смотрите я немного изменил ваш код (пометил комментами):
                ExpandedWrap disabled
                  class Promise {
                      constructor(executor) {
                          this.state = States.PENDING;
                          this.value = undefined;
                          this.callbacks = undefined; //<-- раз
                          try {
                              executor(this._resolve.bind(this), this._reject.bind(this));
                          } catch (error) {
                              this._reject(error);
                          }
                      }
                   
                      _handleState(newState, result) {
                          if (result && typeof result.then === 'function') {
                              return result.then(this._resolve.bind(this), this._reject.bind(this));
                          }
                   
                          setImmediate(() => {
                              if (this.state === States.PENDING) {
                                  this.state = newState;
                                  this.value = result;
                                  
                                  if (this.callbacks) {  //<-- два
                                      var
                                          state = this.state,
                                          value = this.value,
                                          callback = this.callbacks[state],
                                          next = this.callbacks.next;  
                                  
                                      if (typeof callback === 'function') {
                                          state = States.FULFILLED;
                                          value = callback(value);  
                                      }
                                      
                                      if (state === States.FULFILLED) {
                                          next._resolve(value);
                                      } else if (state === States.REJECTED) {
                                          next._reject(value);
                                      }
                                  }
                              }
                          });
                      }
                      
                      _resolve(result) {
                          this._handleState(States.FULFILLED, result);
                      }
                      
                      _reject(reason) {
                          this._handleState(States.REJECTED, reason);
                      }
                   
                      then(onfulfilled, onrejected) {
                          var next = new Promise(() => {});
                          this.callbacks = {          //<-- три
                              fulfilled: onfulfilled,
                              rejected: onrejected,
                              next
                          };
                          return next;
                      }
                   
                      catch(onrejected) {
                          return this.then(null, onrejected);
                      }
                  }
                   
                  // test
                  var promise = new Promise((resolve, reject) => {
                      resolve(42);
                  });
                   
                  promise
                      .then(console.log);
                      .catch(console.error)
                   
                  // 42

                и все работает! :D

                Новичок ты уже не новичок! :D
                Все верно, но если честно я спецификацию по промисам не всю изучил :jokingly:
                Могу сказать тока, то что тогда в твоем случае след код будет работать не совсем так как возможно ты будешь ожидать:
                ExpandedWrap disabled
                  // test
                  var p = new Promise((resolve, reject) => {
                      resolve(42);
                  });
                  p.then(console.log);
                  p.catch(console.error)

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

                - Ок Маста я понял вас :)
                - Идем дальше...
                - Маста, а зачем вы вынесли след кусок кода метода _handleState из setImmediate?
                ExpandedWrap disabled
                  ...
                          if (result && typeof result.then === 'function') { //<-- сюда
                              return result.then(this._resolve.bind(this), this._reject.bind(this));
                          }
                          
                          setImmediate(() => {
                            .... //<-- был тут
                          });
                  ...
                Сообщение отредактировано: Cfon -
                  Новичок, в resolve мы может передавать не только простые значения, но и например другой промис. В этом случае нам необходимо дождаться возврата результата этого промиса. Поэтому нам надо отделить проверку (имеет ли значение, передаваемое в resolve, метод then) от обработки состояния. Под отделением я подразумеваю исполнение кода в разных фазах цикла событий (event loop).
                  Вот пример когда наша проверка внутри setImmediate неправильно отработает:
                  ExpandedWrap disabled
                    var p = new Promise(resolve =>
                        resolve(new Promise((resolve) => resolve(42))),
                    )
                    p.then(console.log);
                     
                    // нет вывода, а должно быть 42

                  Чтобы было более понятно вынесем эту проверку в отдельный метод _isThenable
                  ExpandedWrap disabled
                    ...
                        _resolve(result) {
                            if (!this._isThenable(result)) {
                                this._handleState(States.FULFILLED, result);
                            }
                        }
                     
                        _reject(reason) {
                            if (!this._isThenable(reason)) {
                                this._handleState(States.REJECTED, reason);
                            }
                        }
                     
                        _isThenable(value) {
                            if (value && typeof value.then === 'function') {
                                value.then(this._resolve.bind(this), this._reject.bind(this));
                                return true;
                            }
                            return false;
                        }
                     
                        _handleState(newState, result) {
                            setImmediate(() => {
                                if (this.state === States.PENDING) {
                                    ...
                                }
                            });
                        }
                    ...

                  Теперь все работает как надо.

                  - Маста, но в предыдущем варианте кода из поста №5 эта проверка была внутри setImmediate и все работало :huh:

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

                  - ОК я потом еще погоняю код и возможно доганю :D
                  - Маста, а зачем вы окружили вызов колбэка в _handleState в try_catch?
                  ExpandedWrap disabled
                    ...
                        _handleState(newState, result) {
                            setImmediate(() => {
                                if (this.state === States.PENDING) {
                                    this.state = newState;
                                    this.value = result;
                                    this.callbacks.forEach(cb => {
                                        var state = this.state;
                                        var value = this.value;
                                        var callback = cb[state];
                                        var next = cb.next;
                     
                                        if (typeof callback === 'function') {
                                            state = States.FULFILLED;
                                            try {                //<-- вот оно
                                                value = callback(value);
                                            } catch(error) {
                                                next._reject(error);
                                            }
                                        }
                     
                                        if (state === States.FULFILLED) {
                                            next._resolve(value);
                                        } else if (state === States.REJECTED) {
                                            next._reject(value);
                                        }
                                    });
                                }
                            });
                        }
                    ...

                  Новичок просто когда написал очередной тест, он не прошел:
                  ExpandedWrap disabled
                    ...
                        test('test 9: catches errors (throw in then)', () => {
                            const error = Error('Error!!!');
                            return new Promise((resolve) => {
                                resolve(42);
                            })
                            .then(() => {
                                throw error;
                            })
                            .catch(err => {
                                expect(err).toBe(error);
                            });
                        });
                    ...

                  Тут возникает эксепшен внутри колбэка, ну и как следствие если не обработать его то выполнение программы на нем прервется без возможности обработки. Поэтому оборачиваем колбэк в try_catch и все пучком. Кстати код вызова колбэка из поста №5 тоже надо обернуть в try_catch. Сделай сам.

                  - ОК сделаю! :D
                  - Маста еще вопрос, зачем вы устанавливаете состояние всегда в FULFILLED перед вызовом колбэка? :huh:
                  ExpandedWrap disabled
                    ...
                        _handleState(newState, result) {
                            setImmediate(() => {
                                if (this.state === States.PENDING) {
                                    this.state = newState;
                                    this.value = result;
                                    this.callbacks.forEach(cb => {
                                        var state = this.state;
                                        var value = this.value;
                                        var callback = cb[state];
                                        var next = cb.next;
                     
                                        if (typeof callback === 'function') {
                                            state = States.FULFILLED; //<-- вот тут
                                            try {                
                                                value = callback(value);
                                            } catch(error) {
                                                next._reject(error);
                                            }
                                        }
                     
                                        ...
                                    });
                                }
                            });
                        }
                    ...
                  Сообщение отредактировано: Cfon -
                    Новичок я просто сделал рефакторинг кода метода then из поста №5, где юзается замыкание. Вот смотри как это было вначале:
                    ExpandedWrap disabled
                      ...
                          _handleState(newState, result) {
                              setImmediate(() => {
                                  if (this.state === States.PENDING) {
                                      this.state = newState;
                                      this.value = result;
                                      this.callbacks.forEach(cb => {
                                          var state = this.state;
                                          var value = this.value;
                                          var callback = cb[state];
                                          var next = cb.next;
                       
                                          // vvv
                                          if (state === States.FULFILLED) {
                                              if (typeof callback === 'function') { //<-- 1
                                                  next._resolve(callback(this.value));  
                                              } else {
                                                  next._resolve(this.value);
                                              }
                                          } else if (state === States.REJECTED) {
                                              if (typeof callback === 'function') { //<-- 2
                                                  next._resolve(callback(this.value));  
                                              } else {
                                                  next._reject(this.value);
                                              }
                                          }
                                          // ^^^
                                      });
                                  }
                              });
                          }
                      ...

                    как видишь тут есть кусок кода, который дублируется дважды, это первая часть условия, я вынес ее за пределы эти двух условий и вот что получил:
                    ExpandedWrap disabled
                         _handleState(newState, result) {
                              setImmediate(() => {
                                  if (this.state === States.PENDING) {
                                      this.state = newState;
                                      this.value = result;
                                      this.callbacks.forEach(cb => {
                                          var state = this.state;
                                          var value = this.value;
                                          var callback = cb[state];
                                          var next = cb.next;
                       
                                          // vvv
                                          if (typeof callback === 'function') {
                                              state = States.FULFILLED;
                                              value = callback(value);
                                          }
                       
                                          if (state === States.FULFILLED) {
                                              next._resolve(value);
                                          } else if (state === States.REJECTED) {
                                              next._reject(value);
                                          }
                                          // ^^^
                                      });
                                  }
                              });
                          }
                      ...

                    но поскольку первое условие зависит от второго нам необходимо явно установить state = States.FULFILLED.
                    0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
                    0 пользователей:


                    Рейтинг@Mail.ru
                    [ Script execution time: 0,0608 ]   [ 15 queries used ]   [ Generated: 29.03.24, 12:17 GMT ]