От автора: я думаю, что лучший способ объяснить JavaScript promise — это примеры. Какой лучший, самодостаточный и самый короткий способ написать пример? Тест! Для тех, кто никогда не видел оболочку теста Жасмин, it(‘…’, (done) => {…}) — это тест, а done — это функция, которая должна выполняться при завершении асинхронного теста.
Для тестов применяются следующие правила:
Каждый тест начинается с утверждения чего-то на английском языке. Вы должны определить, как тестовый код подтверждает, что утверждение теста верно.
Некоторые тесты содержат ожидания. Если тест успешен, ожидания верны.
Другие тесты основаны на обратном вызове done(). Если done () не вызывается, тест не пройден.
Все тесты вы можете найти здесь на JSFiddle, поэтому не бойтесь экспериментировать с ними. Особенно, если у вас есть какие-то сомнения в отношении любого из тестов, измените тестовый код и посмотрите, что произойдет.
Тесты
Начнем с основ Promise:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
it('Исполнитель запускает promise СИНХРОННО', () => { let executorRun = false; new Promise(function executor() { executorRun = true; }); expect(executorRun).toBe(true); }); it('вы можете обработать promise', (done) => { new Promise((resolve) => setTimeout(resolve, 1)) .then(done); }); it('... или вы можете отклонить promise', (done) => { new Promise((resolve, reject) => setTimeout(reject, 1)) .then(undefined, done); }); it('Ошибка внутри исполнителя, отклоняем promise', (done) => { new Promise(function executor() { throw 'Error'; }).catch(done); }); |
Похоже, что когда вы вызываете resolve(), выполняется обратный вызов первого then(…). Если вы вызываете reject() или генерируется ошибка, catch(), или выполняется обратный вызов второй then(…).
Кроме того, исполнитель promise запускается синхронно. Это означает, что promise — это способ обработки асинхронного кода, а не выполнения задач в асинхронных потоках. Если вы хотите выполнить код JavaScript вне основного потока, используйте Web Workers.
Давайте рассмотрим подробнее, какие существуют функции then(…) и catch(), и что означает «цепочки promise»:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
// Цепочки promise it('вы можете задавать цепочки promise, потому что .then(...) возвращает promise' , (done) => { fetch('//jsonplaceholder.typicode.com/posts/1') .then(response => response.json()) .then(json => expect(json.userId).toBe(1)) .then(done); }); it('вы можете использовать обратный вызов ошибки .then(success, fail), чтобы ' + 'обработать отклоненные promises', (done) => { Promise.reject() .then(function success() { throw 'I must not be executed'; }, function fail() { done(); }); }); it('... или вы можете использовать .catch() чтобы обработать отклоненный promises' , (done) => { Promise.reject() .then(function success() { throw 'I must not be executed'; }) .catch(done); }); it('также .catch() возвращает promise, делая возможной цепочку promise' , (done) => { Promise.reject() .catch(() => undefined) .then(done); }); it('вы должны вернуть отклоненный promise, если хотите' + 'выполнить следующий вызов ошибки', (done) => { function someApiCall() { return Promise.reject('Error'); } someApiCall() .catch((err) => { console.error(err); // Без следующей строки.catch не дает вызова возвращения Promise.reject(err); }) .catch(done); }); it('... или вы можете выдать ошибку, если хотите' + 'выполнить следующий обратный вызов ошибки', (done) => { function someApiCall() { return Promise.reject('Error'); } someApiCall() .catch((err) => { console.error(err); throw err; // Без этой строки .catch не вызывается }) .catch(done); }); it(' обратные вызовы значений, возвращаемых внутри .then()/.catch()' + 'обеспечивают следующий обратный вызов', (done) => { Promise.resolve(1) .then(value => value + 1) .then(value => expect(value).toBe(2)); Promise.reject(1) .catch(value => value + 1) .then(value => expect(value).toBe(2)); setTimeout(() => { done(); }, 1); }); |
Хорошо, но что такое Promise.resolve() и Promise.reject()? Давай выясним!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
it('вы можете использовать Promise.resolve(), чтобы обернуть значения или promises' , (done) => { function iMayReturnAPromise() { return Math.random() >= 0.5 ? Promise.resolve() : 5; } Promise.resolve(iMayReturnAPromise()).then(done); }); it('вы можете использовать Promise.resolve(), чтобы выполнить что-то сразу после' , (done) => { let arr = []; Promise.resolve().then(() => arr.push(2)); arr.push(1); setTimeout(() => { expect(arr).toEqual([1, 2]); done(); }, 1); }); /** @see //jakearchibald.com/2015/tasks-microtasks-queues-and-schedules **/ it('Promise.resolve() обычно выполняется перед setTimeout(.., 0)' , (done) => { let arr = []; setTimeout(() => arr.push('timeOut'), 0); Promise.resolve().then(() => { arr.push('resolve'); }); setTimeout(() => { expect(arr).toEqual(['resolve', 'timeOut']); done(); }, 1); }); it('вы можете создать отклоненные promises', (done) => { Promise.reject('reason').catch(done); }); it('обратите внимание на "Uncaught (in promise) ..."', () => { Promise.reject('The error'); // Выводит в консоли Uncaught (in promise) The error }); |
Цепочки promise или создание новых
Хотя new Promise(…) является способом создания promise, вам следует избегать его использования. Как правило функции / библиотеки возвращают promise, поэтому вам следует продолжать promise, а не создавать новые:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
it("Не используйте new Promise(...), отдавая предпочтение цепочкам", (done) => { const url = '//jsonplaceholder.typicode.com/posts/1'; function badlyDesignedCustomFetch() { return new Promise((resolve, reject) => { fetch(url).then((response) => { if (response.ok) { resolve(response); } else { reject('Fetch failed'); } }); }); } function wellDesignedCustomFetch() { return fetch(url).then((response) => { if (!response.ok) { return Promise.reject('Fetch failed'); } return (response); }); } Promise.all([ badlyDesignedCustomFetch(), wellDesignedCustomFetch() ]).then(done); }); |
Но когда следует использовать new Promise(…)? Когда вы хотите перейти от обратного вызова интерфейса к promise. Например:
1 2 3 4 5 6 |
function imgOnLoad(img) { return new Promise((resolve, reject) => { img.onload = resolve; img.onerror = reject; }); } |
Параллельное выполнение
Цепочка promises — это хорошо, но что насчет выполнения асинхронных операций параллельно? Ниже приведено все, что вам нужно знать:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
// Параллельное выполнение promises it('вы можете использовать Promise.all([...]), чтобы выполнить promises параллельно' , (done) => { const url = '//jsonplaceholder.typicode.com/posts'; const p1 = fetch(`${url}/1`); const p2 = fetch(`${url}/2`); Promise.all([p1, p2]) .then(([res1, res2]) => { return Promise.all([res1.json(), res2.json()]) }) .then(([post1, post2]) => { expect(post1.id).toBe(1); expect(post2.id).toBe(2); }) .then(done); }); it('Promise.all([...]) не проходит, если любой promises дает ошибку' , (done) => { const p1 = Promise.resolve(1); const p2 = Promise.reject('Error'); Promise.all([p1, p2]) .then(() => { fail('I will not be executed') }) .catch(done); }); it("если вы не хотите, чтобы Promise.all() давал ошибку, оберните promises " + "в promise, который не будет давать ошибку", (done) => { function iMayFail(val) { return Math.random() >= 0.5 ? Promise.resolve(val) : Promise.reject(val); } function promiseOr(p, value) { return p.then(res => res, () => value); } const p1 = iMayFail(10); const p2 = iMayFail(9); Promise.all([promiseOr(p1, null), promiseOr(p2, null)]) .then(([val1, val2]) => { expect(val1 === 10 || val1 === null).toBe(true); expect(val2 === 9 || val2 === null).toBe(true); }) .catch(() => { fail('I will not be executed') }) .then(done); }); it('Promise.race([...]) будет обрабатываться, как только ' + 'один из promises будет обработан или отклонен', (done) => { const timeout = new Promise((resolve, reject) => setTimeout(reject, 100)); const data = fetch('//jsonplaceholder.typicode.com/posts/1'); Promise.race([data, timeout]) .then(() => console.log('Fetch OK')) .catch(() => console.log('Fetch timeout')) .then(done); }); |
Синтаксис
Синтаксис promise немного сложнее по сравнению со стандартным синтаксисом синхронного кода. Это правда, что, создавая цепочки promises, мы делаем код более читаемым, но это можно улучшить. Новый синтаксис await / async делает использование promise столь же простым, как и написание синхронного кода.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// Новый синтаксис await / async it('вы можете использовать новый синтаксис await/async', async () => { function timeout(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } const start = Date.now(); const delay = 200; await timeout(delay + 2); // Просто допуск в ms expect(Date.now() - start).toBeGreaterThanOrEqual(delay); }); it('асинхронная функция возвращает promise', (done) => { async function iAmAsync() { return 1; } iAmAsync() .then((val) => expect(val).toBe(1)) .then(done); }); it('await просто ожидает обработки promise', async (done) => { await Promise.resolve(); done(); }); it('await выдает ошибку, если promise дает ошибку', async(done) => { try { await Promise.reject(); fail('I will not be executed'); } catch (err) { done(); } }); |
Синхронные функции
И напоследок: при разработке функций вы должны решить, является ли она синхронной или нет. Не возвращайте promise только потому, что «вы никогда не знаете». Когда это возможно, используйте «обычные» синхронные функции. Все тесты вы можете найти здесь, на JSFiddle.
Это все! Надеюсь, вам понравилась эта статья.
Автор: Andrea Koutifaris
Источник: //medium.freecodecamp.org/
Редакция: Команда webformyself.