Наши проекты:
Журнал · Discuz!ML · Wiki · DRKB · Помощь проекту |
||
ПРАВИЛА | FAQ | Помощь | Поиск | Участники | Календарь | Избранное | RSS |
[18.227.114.125] |
|
Сообщ.
#1
,
|
|
|
Написал свой класс Promise
есть трабла никак не могу понять как реализовать возврат промиса из колбэка 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); } } тестирую 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!!!'); }); }); }); два последних теста не проходят, пишет Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error: Где я накосячил? |
Сообщ.
#2
,
|
|
|
Нашёл баг
Я совершил ошибку, о которой сам часто предупреждал! Это смена контекста функции, в моём случае это имело место в handleState. Есть несколько методов решения, я привёл самый любимый : ... 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); }); } }; ... |
Сообщ.
#3
,
|
|
|
Добавил асинхронность в промис через setImmediate:
... 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); }); } }); }; ... тестируем все ОК ... test('test 4: asynchronous behaviour', () => { const p = new Promise(resolve => resolve(5)); expect(p.value).not.toBe(5); }); ... а вот следущие два теста на обработку ошибки не проходят... где то я накосячил ... 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); }); }); ... |
Сообщ.
#4
,
|
|
|
интересно то, что если вызвать этот код не из тестовой среды Jest, то он отрабатывает как надо
// test var p = new Promise((resolve, reject) => { reject('Error!!!'); }) .catch(console.error); // Error!!! |
Сообщ.
#5
,
|
|
|
Нашел баг
Как оказалось я ошибся в логике функции rejected. В нем вызывался всегда reject, что было ошибкой поскольку по спецификации Promise/A+ отказ должен быть обработан только один раз, что в случае с reject приводило к повторному вызову колбэка. Выход? Все просто! Надо вместо reject вызывать resolve в случае наличия колбэка и reject в его отсутствии ... 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 }); }); } ... Теперь все пучком! Промис полностью функционален и протестирован! Итак вот конечный код промиса: 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 строчек кода Позже выложу диалог Масты и Новичка. |
Сообщ.
#6
,
|
|
|
Еще одна реализация промиса, где логика реализована через члены класса, а не через замыкания:
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 ПС. Во второй реализации метод handleState можно покрошить на более мелкую логику, но я не стал это делать, чтобы сохранить структуру кода по аналогии с предыдущим примером. |
Сообщ.
#7
,
|
|
|
- Маста есть вопросы по коду
Жги Новичок! - Я просмотрел ваш последний код из поста №6 и никак не могу понять для чего в нем юзается массив callbacks, а не просто объект callbacks? Как это? - Ну вот смотрите я немного изменил ваш код (пометил комментами): 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 и все работает! Новичок ты уже не новичок! Все верно, но если честно я спецификацию по промисам не всю изучил Могу сказать тока, то что тогда в твоем случае след код будет работать не совсем так как возможно ты будешь ожидать: // test var p = new Promise((resolve, reject) => { resolve(42); }); p.then(console.log); p.catch(console.error) т.е. тут я добавляю несколько колбэков в один и тот же промис, переписывая, как ты понимаешь, предыдущие обработчики и как следствие срабатывать будет тока последний колбэк. Хотя я не вижу смысла так писать код, ибо промисы организуют обработку колбэков через цепочки промисов. Тут их нет. Может кто другой знает когда требуется такая форма обработки промиса? - Ок Маста я понял вас - Идем дальше... - Маста, а зачем вы вынесли след кусок кода метода _handleState из setImmediate? ... if (result && typeof result.then === 'function') { //<-- сюда return result.then(this._resolve.bind(this), this._reject.bind(this)); } setImmediate(() => { .... //<-- был тут }); ... |
Сообщ.
#8
,
|
|
|
Новичок, в resolve мы может передавать не только простые значения, но и например другой промис. В этом случае нам необходимо дождаться возврата результата этого промиса. Поэтому нам надо отделить проверку (имеет ли значение, передаваемое в resolve, метод then) от обработки состояния. Под отделением я подразумеваю исполнение кода в разных фазах цикла событий (event loop).
Вот пример когда наша проверка внутри setImmediate неправильно отработает: var p = new Promise(resolve => resolve(new Promise((resolve) => resolve(42))), ) p.then(console.log); // нет вывода, а должно быть 42 Чтобы было более понятно вынесем эту проверку в отдельный метод _isThenable ... _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 и все работало Дело в том, в предыдущем варианте код юзал замыкания, и вызов этой проверки и обработка состояния промиса выполнялось в разных фазах цикла обработки событий. - ОК я потом еще погоняю код и возможно доганю - Маста, а зачем вы окружили вызов колбэка в _handleState в try_catch? ... _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); } }); } }); } ... Новичок просто когда написал очередной тест, он не прошел: ... 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. Сделай сам. - ОК сделаю! - Маста еще вопрос, зачем вы устанавливаете состояние всегда в FULFILLED перед вызовом колбэка? ... _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); } } ... }); } }); } ... |
Сообщ.
#9
,
|
|
|
Новичок я просто сделал рефакторинг кода метода then из поста №5, где юзается замыкание. Вот смотри как это было вначале:
... _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); } } // ^^^ }); } }); } ... как видишь тут есть кусок кода, который дублируется дважды, это первая часть условия, я вынес ее за пределы эти двух условий и вот что получил: _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. |