Як написати обіцянку JavaScript

Що таке обіцянка?

Обіцянка JavaScript - це об’єкт, який представляє завершення або невдачу асинхронного завдання та його результуюче значення.¹

Кінець.

Я жартую, звичайно. Отже, що це визначення взагалі означає?

Перш за все, багато речей у JavaScript є об'єктами. Ви можете створити об’єкт кількома різними способами. Найпоширеніший спосіб - це синтаксис об’єктного літералу:

const myCar = { color: 'blue', type: 'sedan', doors: '4', };

Ви також можете створити classта створити його за допомогою newключового слова.

class Car { constructor(color, type, doors) { this.color = color; this.type = type; this.doors = doors } } const myCar = new Car('blue', 'sedan', '4');

console.log(myCar);

Обіцянка - це просто об’єкт, який ми створюємо, як на наступному прикладі. Ми створюємо його за допомогою newключового слова. Замість трьох параметрів, які ми передали для створення нашого автомобіля (колір, тип та двері), ми передаємо функцію, яка приймає два аргументи: resolveі reject.

Зрештою, обіцянки говорять нам щось про завершення асинхронної функції, з якої ми її повернули - якщо вона працювала чи ні. Ми вважаємо, що функція була успішною, сказавши, що обіцянка вирішена , а невдалою, сказавши, що обіцянку відхилено.

const myPromise = new Promise(function(resolve, reject) {});

console.log(myPromise);

const myPromise = new Promise(function(resolve, reject) { resolve(10); });

Бачите, не надто страшно - просто об’єкт, який ми створили. І, якщо ми трохи розширимо його:

Крім того, ми можемо передати все, що хотіли б, для вирішення та відхилення. Наприклад, ми могли б передати об'єкт замість рядка:

return new Promise((resolve, reject) => { if(somethingSuccesfulHappened) { const successObject = { msg: 'Success', data,//...some data we got back } resolve(successObject); } else { const errorObject = { msg: 'An error occured', error, //...some error we got back } reject(errorObject); } });

Або, як ми бачили раніше, нам не потрібно нічого передавати:

return new Promise((resolve, reject) => { if(somethingSuccesfulHappend) { resolve() } else { reject(); } });

А як щодо "асинхронної" частини визначення?

JavaScript є однопоточним. Це означає, що одночасно може працювати лише одна річ. Якщо ви можете собі уявити дорогу, ви можете уявити JavaScript як односмугову магістраль. Певний код (асинхронний код) може пересуватися до плеча, щоб дозволити іншому коду передавати його. Коли цей асинхронний код зроблений, він повертається на проїжджу частину.

Як допоміжна примітка, ми можемо повернути обіцянку з будь-якої функції. Це не повинно бути асинхронно. З огляду на це, обіцянки зазвичай повертаються в тих випадках, коли функція, з якої вони повертаються, є асинхронною. Наприклад, API, який має методи збереження даних на сервері, буде чудовим кандидатом для повернення обіцянки!

Винос:

Обіцянки дають нам спосіб дочекатися завершення нашого асинхронного коду, захопити з нього деякі значення та передати ці значення в інші частини нашої програми.

У мене тут є стаття, яка заглиблюється глибше в ці концепції: Thrown For Loop: Розуміння циклів та таймаутів у JavaScript.

Як ми використовуємо обіцянку?

Використання обіцянки також називається споживанням обіцянки. У нашому прикладі вище наша функція повертає об’єкт обіцянки. Це дозволяє нам використовувати ланцюжок методів з нашою функцією.

Ось приклад ланцюжка методів, впевнений, ви бачили:

const a = 'Some awesome string'; const b = a.toUpperCase().replace('ST', '').toLowerCase(); console.log(b); // some awesome ring

А тепер згадайте нашу обіцянку (прикидатися):

const somethingWasSuccesful = true; function someAsynFunction() { return new Promise((resolve, reject){ if (somethingWasSuccesful) { resolve(); } else { reject() } }); }

І, споживаючи нашу обіцянку, використовуючи ланцюжок методів:

someAsyncFunction .then(runAFunctionIfItResolved(withTheResolvedValue)) .catch(orARunAfunctionIfItRejected(withTheRejectedValue));

(Більш) реальний приклад.

Уявіть, у вас є функція, яка отримує користувачів із бази даних. Я написав приклад функції на Codepen, яка імітує API, який ви можете використовувати. Він надає два варіанти доступу до результатів. По-перше, ви можете забезпечити функцію зворотного дзвінка, де ви можете отримати доступ до користувача або будь-яку помилку. Або два, функція повертає обіцянку як спосіб доступу до користувача або помилку.

Традиційно ми отримували б доступ до результатів асинхронного коду за допомогою зворотних викликів.

rr someDatabaseThing(maybeAnID, function(err, result)) { //...Once we get back the thing from the database... if(err) { doSomethingWithTheError(error) } else { doSomethingWithResults(results); } }

Використання зворотних викликів дозволено, поки вони не стануть надто вкладеними. Іншими словами, вам доведеться запускати більше асинхронного коду з кожним новим результатом. Цей шаблон зворотних викликів у зворотних викликах може призвести до чогось, відомого як "пекло зворотного дзвінка".

Обіцянки пропонують нам більш елегантний та читабельний спосіб побачити хід нашої програми.

doSomething() .then(doSomethingElse) // and if you wouldn't mind .catch(anyErrorsPlease);

Writing our own promise: Goldilocks, the Three Bears, and a Supercomputer

Imagine you found a bowl of soup. You’d like to know the temperature of that soup before you eat it. You're out of thermometers, but luckily, you have access to a supercomputer that tells you the temperature of the bowl of soup. Unfortunately, this supercomputer can take up to 10 seconds to get the results.

Here are a couple of things to notice.

  1. We initiate a global variable called result.
  2. We simulate the duration of the network delay with Math.random() and setTimeout().
  3. We simulate a temperature with Math.random().
  4. We keep the delay and temperature values confined within a range by adding some extra “math”. The range for temp is 1 to 300; the range for delay is 1000ms to 10000ms (1s to 10 seconds).
  5. We log the delay and temperature so we have an idea of how long this function will take and the results we expect to see when it’s done.

Run the function and log the results.

getTemperature(); console.log(results); // undefined

The temperature is undefined. What happened?

The function will take a certain amount of time to run. The variable is not set until the delay is over. So while we run the function, setTimeout is asynchronous. The part of the code in setTimeout moves out of the main thread into a waiting area.

I have an article here that dives deeper into this process: Thrown For a Loop: Understanding Loops and Timeouts in JavaScript.

Since the part of our function that sets the variable result moves into a holding area until it is done, our parser is free to move onto the next line. In our case, it’s our console.log(). At this point, result is still undefined since our setTimeout is not over.

So what else could we try? We could run getTemperature() and then wait 11 seconds (since our max delay is ten seconds) and then console.log the results.

getTemperature(); setTimeout(() => { console.log(result); }, 11000); // Too Hot | Delay: 3323 | Temperature: 209 deg

This works, but the problem with this technique is, although in our example we know the maximum network delay, in a real-life example it might occasionally take longer than ten seconds. And, even if we could guarantee a maximum delay of ten seconds, if the result is ready sooner, we are wasting time.

Promises to the Rescue

We are going to refactor our getTemperature() function to return a promise. And instead of setting the result, we will reject the promise unless the result is “Just Right,” in which case we will resolve the promise. In either case, we will pass in some values to both resolve and reject.

We can now use the results of our promise we are returning (also know as consuming the promise).

getTemperature() .then(result => console.log(result)) .catch(error => console.log(error)); // Reject: Too Cold | Delay: 7880 | Temperature: 43 deg

.then will get called when our promise resolves and will return whatever information we pass into resolve.

.catch will get called when our promise rejects and will return whatever information we pass into reject.

Most likely, you’ll consume promises more than you will create them. In either case, they help make our code more elegant, readable, and efficient.

Summary

  1. Promises are objects that contain information about the completion of some asynchronous code and any resulting values we want to pass in.
  2. To return a promise we use return new Promise((resolve, reject)=> {})
  3. To consume a promise we use .then to get the information from a promise that has resolved, and .catch to get the information from a promise that has rejected.
  4. You’ll probably use (consume) promises more than you’ll write.

References

1.) //developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise