こんにちは。台湾在住、ShareWisのソフトウェアエンジニアのサイです。
この記事ではJavaScriptのPromiseオブジェクトについて紹介したいと思います。
なんでJavaScriptにPromiseが必要になったか?
まずもってPromiseはなぜ必要なのでしょうか?
その理由はJavaScriptに以下の2つの特徴があるためです。
- シングルスレッド
- 同期
上記の2つの特徴から、JavaScriptは基本的に1つのことを1つずつしか実行できません。そのため、何かが実行されているのを待たなければいけない状況が発生し、UXが悪くなってしまうケースがあります。
この問題を解決するために、ECMAScript 2015(いわゆるES6)ではJavaScriptにPromiseオブジェクトが導入されました。
JavaScriptのPromiseオブジェクトって何?
Promiseは非同期のコードの実行を行うオブジェクトです。
Promise.then()
は2つの引数を持ちます。resolve
と reject
です。一方は処理が成功したとき、もう一方は失敗したときのためのものです。
resolve(…) は非同期的に成功したときに呼び出され、reject(…) は非同期的に失敗したときに呼び出されます。
これらはオプショナルなので、成功したときだけ、失敗したときだけ、のどちらか一方だけを記述するのもOKです。
let doSomething = new Promise(function(resolve, reject) {
// ファイルアップロードなど時間がかかる処理
resolve(); // when successful
reject(); // when error
});
doSomething.then(
function(value) { /* code if successful */ },
function(error) { /* code if some error */ }
);
Promiseオブジェクトのプロパティ
Promiseオブジェクトは3つのステートを持っています。
- Pending: 最初の状態。以下の2つのどちらでもない状態
- Fulfilled: 処理が完了して成功した状態
- Rejected: 処理が失敗した状態
デフォルトのステートは pending です。resolve() が呼び出されると、fulfilledにステートが変わります。また、reject()が呼び出されるとrejectedになります。
Promiseの結果のみがステートに影響を与えることに注意してください。ステートを自身で変更することはできません。ステートが一度 pending から fulfilled か rejeted に変わったら、それ以上変更することはできません。
Promise の簡単な例
Promiseを使ったコードのちょっとした例を掲載しておきます。
8分茹でて、カチカチのゆで卵を作るお料理プログラムです。
let cookPromise = (food, minutes) => {
return new Promise((resolve, reject) => {
if (minutes > 7 && 9 > minutes) {
resolve(`${food}が完成!`)
} else {
reject("オー!ノー!")
}
})
}
const cookingTime = parseInt(Math.random() * 10);
cookPromise('ゆで卵', cookingTime)
.then((res) => {
console.log(res); // ゆで卵が完成!
})
.catch((err) => {
console.error(err); // オー!ノー!
})
.finally(() => {
console.log('調理終了!');
});
cookPromise
が実行されたとき、resolveからレスポンスを受けて、then()
メソッドを使い、rejectからレスポンスを受けて、catch()
メソッドを使っています。そして、fulfiiled か rejected に関わらず、finally()
を実行しています。
Promise のチェーン
promise.then(), promise.catch(), promise.finally() は新しい Promise オブジェクトを返すこともできます。
これにより、次のアクションごとにメソッドを変更できるので、コールバック地獄(ネストされまくった「運命のピラミッド」 (pyramid of doom) と呼ばれるもの)を回避することができます。
myPromise
.then(handleResolvedA)
.then(handleResolvedB)
.then(handleResolvedC)
.catch(handleRejectedAny);
Promise.all()
ゆで卵の例を取り上げましたが、ときには大勢のお客様に一気にゆで卵を調理したいときもあると思います。Promise.all()
メソッドは引数として反復する Promise を取り、結果のアレイを返します。
Promise.all([cookPromise('オムレツ', 8), cookPromise('目玉焼き', 8)])
.then(res => console.log(res));
// ["オムレツが完成!", "目玉焼きが完成!"]
Promise.race()
Promise.race()
は Promise のアレイを受け取り、最初に確定した Promise の結果を返します。fulfills か rejects かに関わらず、最初に定まった結果に応じて実行されます。
Promise.race([cookPromise('オムレツ', 8), cookPromise('目玉焼き', 8)])
.then(res => console.log(res));
// 何でもいいから最初にでき上がったのを食べる
次のステップ
Promiseの基本と使い方を紹介しました。
JavaScriptの非同期処理について少しでも感触をつかんでいただけると幸いです。
さらに踏み込んで学びたい方は以下から、シンタックスシュガー (糖衣構文)として: Async
と Await
について学んでみましょう!
(サイ)
English version: Understanding JavaScript Promises
Hi, this is Tsai, a ShareWis software engineer from Taiwam.
I want to share the basic knowledge of JavaScript Promise object and how to use it.
Why do we need Promises?
Why do we need Promises? That is because JavaScript has 2 features:
- Single Thread
- Synchronous
It means that JavaScript only executes one thing at a time and one by one. So the user experience would be bad if we make our users wait for something running for a long time. ECMAScript 2015, also known as ES6, introduced the JavaScript Promise object.
What is JavaScript Promise Object?
A promise is an object to deal with asynchronous code. Promise.then() takes two arguments which are resolve
and reject
, a callback for success and another for failure. We call resolve(…) when what we were doing asynchronously was successful, and reject(…) when it failed. They are optional, so you can add a callback for success or failure only.
let doSomething = new Promise(function(resolve, reject) {
// Something like uploading files which may take some time.
resolve(); // when successful
reject(); // when error
});
doSomething.then(
function(value) { /* code if successful */ },
function(error) { /* code if some error */ }
);
Promise Object Properties
A JavaScript Promise object has 3 states:
- Pending: initial state, neither fulfilled nor rejected.
- Fulfilled: the operation was completed successfully.
- Rejected: the operation failed.
The default state of Promise is pending, once resolve() is called, the state will change to fulfilled, and when reject() is called, the state will be changed to rejected. Please note that only the result of Promise affects the state, and you can not modify the state by yourself. Once the state is changed from pending to fulfilled or rejected, it will not change anymore.
Basic Example
We want to have a program for cooking. Let’s say making perfect hard-boiled eggs is 8 minutes.
let cookPromise = (food, minutes) => {
return new Promise((resolve, reject) => {
if (minutes > 7 && 9 > minutes) {
resolve(`${food} is perfect!`)
} else {
reject("Oh no!!!")
}
})
}
const cookingTime = parseInt(Math.random() * 10);
cookPromise('Boiled egg', cookingTime)
.then((res) => {
console.log(res); // Boiled egg is perfect!
})
.catch((err) => {
console.error(err); // Oh no!!!
})
.finally(() => {
console.log('Experiment completed');
});
When the function cookPromise
is executed, we use the method then()
to receive the response from resolve, catch()
to receive the response from reject. And we have finally()
, either fulfilled or rejected, the specified callback function is executed.
Chained Promises
The methods promise.then(), promise.catch(), and promise.finally() return a new Promise object. We can prevent the callback hell (pyramid of doom) by chaining the methods to take associate further actions.
myPromise
.then(handleResolvedA)
.then(handleResolvedB)
.then(handleResolvedC)
.catch(handleRejectedAny);
Promise.all()
We sometimes want to cook many dishes at the same time. The Promise.all()
method takes an iterable of promises as an argument and returns an array of the results.
Promise.all([cookPromise('Omelette', 8), cookPromise('Fried egg', 8)])
.then(res => console.log(res));
// ["Omelette is perfect!", "Fried egg is perfect!"]
Promise.race()
The Promise.race()
also accepts an array of promises, but returns the first promise that is settled. So it will return the first result no matter it fulfills or rejects.
Promise.race([cookPromise('Omelette', 8), cookPromise('Fried egg', 8)])
.then(res => console.log(res));
// I will eat whatever got finished first.
Next steps
Today I give you a brief introduction to Promise and its usage. You are no longer unfamiliar with asynchronous processes and will be good at the entire flow control. The advanced topic is to use syntactic sugar: Async
& Await
.
(Tsai)