Understanding JavaScript Promises
序章
JavascriptPromisesは理解するのが難しい場合があります。 したがって、私は約束を理解する方法を書き留めたいと思います。
約束を理解する
要するに約束:
「あなたが子供だと想像してみてください。 あなたのお母さんは、来週新しい電話を手に入れることを約束します。」
あなたは来週までその電話を手に入れるかどうかわからない。 あなたのお母さんはあなたに真新しい電話を本当に買うことができます、または彼女はしません。
それが約束です。 約束には3つの状態があります。 彼らです:
- 保留中:あなたはその電話を手に入れるかどうかわからない
- 満たされた:お母さんは幸せです、彼女はあなたに真新しい電話を買います
- 拒否されました:お母さんは不幸です、彼女はあなたに電話を買わない
約束を作成する
これをJavaScriptに変換してみましょう。
// ES5: Part 1
var isMomHappy = false;
// Promise
var willIGetNewPhone = new Promise(
function (resolve, reject) {
if (isMomHappy) {
var phone = {
brand: 'Samsung',
color: 'black'
};
resolve(phone); // fulfilled
} else {
var reason = new Error('mom is not happy');
reject(reason); // reject
}
}
);
コード自体は非常に表現力豊かです。
以下は、promise構文が通常どのように見えるかです。
// promise syntax look like this
new Promise(function (resolve, reject) { ... } );
約束を消費する
約束ができたので、それを消費しましょう。
// ES5: Part 2
var willIGetNewPhone = ... // continue from part 1
// call our promise
var askMom = function () {
willIGetNewPhone
.then(function (fulfilled) {
// yay, you got a new phone
console.log(fulfilled);
// output: { brand: 'Samsung', color: 'black' }
})
.catch(function (error) {
// oops, mom didn't buy it
console.log(error.message);
// output: 'mom is not happy'
});
};
askMom();
例を実行して結果を見てみましょう!
デモ: https://jsbin.com/nifocu/1/edit?js,console
連鎖の約束
約束は連鎖可能です。
あなた、子供、あなたのお母さんがあなたに新しい電話を買ったときにあなたが彼らに新しい電話を見せることをあなたの友人に約束するとしましょう。
それは別の約束です。 書きましょう!
// ES5
// 2nd promise
var showOff = function (phone) {
return new Promise(
function (resolve, reject) {
var message = 'Hey friend, I have a new ' +
phone.color + ' ' + phone.brand + ' phone';
resolve(message);
}
);
};
注:以下のように記述することで、上記のコードを短縮できます。
// shorten it
// 2nd promise
var showOff = function (phone) {
var message = 'Hey friend, I have a new ' +
phone.color + ' ' + phone.brand + ' phone';
return Promise.resolve(message);
};
約束を連鎖させましょう。 子供であるあなたは、willIGetNewPhone
の約束の後でのみshowOff
の約束を開始できます。
// call our promise
var askMom = function () {
willIGetNewPhone
.then(showOff) // chain it here
.then(function (fulfilled) {
console.log(fulfilled);
// output: 'Hey friend, I have a new black Samsung phone.'
})
.catch(function (error) {
// oops, mom don't buy it
console.log(error.message);
// output: 'mom is not happy'
});
};
それはあなたが約束を連鎖させることができる方法です。
約束は非同期です
約束は非同期です。 約束を呼び出す前後にメッセージを記録しましょう。
// call our promise
var askMom = function () {
console.log('before asking Mom'); // log before
willIGetNewPhone
.then(showOff)
.then(function (fulfilled) {
console.log(fulfilled);
})
.catch(function (error) {
console.log(error.message);
});
console.log('after asking mom'); // log after
}
期待される出力のシーケンスは何ですか? あなたは期待するかもしれません:
1. before asking Mom
2. Hey friend, I have a new black Samsung phone.
3. after asking mom
ただし、実際の出力シーケンスは次のとおりです。
1. before asking Mom
2. after asking mom
3. Hey friend, I have a new black Samsung phone.
お母さんの約束(新しい電話)を待っている間、あなたはプレーをやめません。 これを非同期と呼びます。コードはブロックしたり結果を待ったりすることなく実行されます。 約束が進むのを待つ必要があるものはすべて.then
に入れられます。
ES5の完全な例を次に示します。
// ES5: Full example
var isMomHappy = true;
// Promise
var willIGetNewPhone = new Promise(
function (resolve, reject) {
if (isMomHappy) {
var phone = {
brand: 'Samsung',
color: 'black'
};
resolve(phone); // fulfilled
} else {
var reason = new Error('mom is not happy');
reject(reason); // reject
}
}
);
// 2nd promise
var showOff = function (phone) {
var message = 'Hey friend, I have a new ' +
phone.color + ' ' + phone.brand + ' phone';
return Promise.resolve(message);
};
// call our promise
var askMom = function () {
willIGetNewPhone
.then(showOff) // chain it here
.then(function (fulfilled) {
console.log(fulfilled);
// output: 'Hey friend, I have a new black Samsung phone.'
})
.catch(function (error) {
// oops, mom don't buy it
console.log(error.message);
// output: 'mom is not happy'
});
};
askMom();
ES5、ES6 / 2015、ES7/Nextでの約束
ES5-多数派のブラウザ
Bluebird promiseライブラリを含めると、デモコードはES5環境(すべての主要なブラウザー+ NodeJ)で機能します。 これは、ES5がそのままの状態でpromiseをサポートしていないためです。 もう1つの有名なプロミスライブラリは、KrisKowalによるQです。
ES6 / ES2015-最新のブラウザー、NodeJs v6
ES6はpromiseをネイティブにサポートしているため、デモコードはそのままで機能します。 さらに、ES6関数を使用すると、矢印関数を使用してコードをさらに簡略化し、const
およびlet
を使用できます。
ES6コードの完全な例を次に示します。
//_ ES6: Full example_
const isMomHappy = true;
// Promise
const willIGetNewPhone = new Promise(
(resolve, reject) => { // fat arrow
if (isMomHappy) {
const phone = {
brand: 'Samsung',
color: 'black'
};
resolve(phone);
} else {
const reason = new Error('mom is not happy');
reject(reason);
}
}
);
// 2nd promise
const showOff = function (phone) {
const message = 'Hey friend, I have a new ' +
phone.color + ' ' + phone.brand + ' phone';
return Promise.resolve(message);
};
// call our promise
const askMom = function () {
willIGetNewPhone
.then(showOff)
.then(fulfilled => console.log(fulfilled)) // fat arrow
.catch(error => console.log(error.message)); // fat arrow
};
askMom();
すべてのvar
がconst
に置き換えられていることに注意してください。 すべてのfunction(resolve, reject)
は(resolve, reject) =>
に簡略化されています。 これらの変更にはいくつかの利点があります。
ES7-非同期/待機
ES7では、async
およびawait
構文が導入されました。 .then
と.catch
がなくても、非同期構文を理解しやすくなります。
ES7構文で例を書き直します。
// ES7: Full example
const isMomHappy = true;
// Promise
const willIGetNewPhone = new Promise(
(resolve, reject) => {
if (isMomHappy) {
const phone = {
brand: 'Samsung',
color: 'black'
};
resolve(phone);
} else {
const reason = new Error('mom is not happy');
reject(reason);
}
}
);
// 2nd promise
async function showOff(phone) {
return new Promise(
(resolve, reject) => {
var message = 'Hey friend, I have a new ' +
phone.color + ' ' + phone.brand + ' phone';
resolve(message);
}
);
};
// call our promise in ES7 async await style
async function askMom() {
try {
console.log('before asking Mom');
let phone = await willIGetNewPhone;
let message = await showOff(phone);
console.log(message);
console.log('after asking mom');
}
catch (error) {
console.log(error.message);
}
}
// async await it here too
(async () => {
await askMom();
})();
約束とそれらをいつ使用するか
なぜ約束が必要なのですか? 約束の前に世界はどのように見えましたか? これらの質問に答える前に、基本に戻りましょう。
通常機能VS非同期機能
これらの2つの例を見てみましょう。 どちらの例も、2つの数値の加算を実行します。1つは通常の関数を使用して加算し、もう1つはリモートで加算します。
2つの数値を加算する通常の関数
// add two numbers normally
function add (num1, num2) {
return num1 + num2;
}
const result = add(1, 2); // you get result = 3 immediately
2つの数値を加算する非同期関数
// add two numbers remotely
// get the result by calling an API
const result = getAddResultFromServer('http://www.example.com?num1=1&num2=2');
// you get result = "undefined"
通常の関数で数値を加算すると、すぐに結果が得られます。 ただし、結果を取得するためにリモートコールを発行する場合は、待機する必要があり、すぐに結果を取得することはできません。
サーバーがダウンしている、応答が遅いなどの理由で、結果が得られるかどうかはわかりません。 結果を待っている間、プロセス全体がブロックされることは望ましくありません。
APIの呼び出し、ファイルのダウンロード、ファイルの読み取りは、実行する通常の非同期操作の一部です。
非同期呼び出しにpromiseを使用する必要はありません。 promiseの前は、コールバックを使用していました。 コールバックは、戻り結果を取得したときに呼び出す関数です。 前の例を変更して、コールバックを受け入れましょう。
// add two numbers remotely
// get the result by calling an API
function addAsync (num1, num2, callback) {
// use the famous jQuery getJSON callback API
return $.getJSON('http://www.example.com', {
num1: num1,
num2: num2
}, callback);
}
addAsync(1, 2, success => {
// callback
const result = success; // you get result = 3 here
});
後続の非同期アクション
番号を1つずつ追加するのではなく、3回追加します。 通常の機能では、これを行います:-
// add two numbers normally
let resultA, resultB, resultC;
function add (num1, num2) {
return num1 + num2;
}
resultA = add(1, 2); // you get resultA = 3 immediately
resultB = add(resultA, 3); // you get resultB = 6 immediately
resultC = add(resultB, 4); // you get resultC = 10 immediately
console.log('total' + resultC);
console.log(resultA, resultB, resultC);
これは、コールバックでどのように見えるかです。
// add two numbers remotely
// get the result by calling an API
let resultA, resultB, resultC;
function addAsync (num1, num2, callback) {
// use the famous jQuery getJSON callback API
// https://api.jquery.com/jQuery.getJSON/
return $.getJSON('http://www.example.com', {
num1: num1,
num2: num2
}, callback);
}
addAsync(1, 2, success => {
// callback 1
resultA = success; // you get result = 3 here
addAsync(resultA, 3, success => {
// callback 2
resultB = success; // you get result = 6 here
addAsync(resultB, 4, success => {
// callback 3
resultC = success; // you get result = 10 here
console.log('total' + resultC);
console.log(resultA, resultB, resultC);
});
});
});
デモ: https://jsbin.com/barimo/edit?html,js,console
この構文は、コールバックが深くネストされているため、使い勝手が悪くなります。
深くネストされたコールバックの回避
Promiseは、深くネストされたコールバックを回避するのに役立ちます。 同じ例のpromiseバージョンを見てみましょう。
// add two numbers remotely using observable
let resultA, resultB, resultC;
function addAsync(num1, num2) {
// use ES6 fetch API, which return a promise
// What is .json()? https://developer.mozilla.org/en-US/docs/Web/API/Body/json
return fetch(`http://www.example.com?num1=${num1}&num2=${num2}`)
.then(x => x.json());
}
addAsync(1, 2)
.then(success => {
resultA = success;
return resultA;
})
.then(success => addAsync(success, 3))
.then(success => {
resultB = success;
return resultB;
})
.then(success => addAsync(success, 4))
.then(success => {
resultC = success;
return resultC;
})
.then(success => {
console.log('total: ' + success)
console.log(resultA, resultB, resultC)
});
promiseを使用して、.then
でコールバックをフラット化します。 ある意味では、コールバックのネストがないため、見た目はすっきりしています。 ES7 async
構文を使用すると、この例をさらに拡張できます。
オブザーバブル
約束に落ち着く前に、Observables
と呼ばれる非同期データの処理に役立つ何かがあります。
Observablesで書かれた同じデモを見てみましょう。 この例では、オブザーバブルにRxJSを使用します。
let Observable = Rx.Observable;
let resultA, resultB, resultC;
function addAsync(num1, num2) {
// use ES6 fetch API, which return a promise
const promise = fetch(`http://www.example.com?num1=${num1}&num2=${num2}`)
.then(x => x.json());
return Observable.fromPromise(promise);
}
addAsync(1,2)
.do(x => resultA = x)
.flatMap(x => addAsync(x, 3))
.do(x => resultB = x)
.flatMap(x => addAsync(x, 4))
.do(x => resultC = x)
.subscribe(x => {
console.log('total: ' + x)
console.log(resultA, resultB, resultC)
});
Observablesはもっと面白いことをすることができます。 たとえば、delay
は、3 seconds
によって1行のコードで関数を追加するか、再試行して、特定の回数だけ呼び出しを再試行できるようにします。
...
addAsync(1,2)
.delay(3000) // delay 3 seconds
.do(x => resultA = x)
...
私のRxJの投稿の1つここを読むことができます。
結論
コールバックとプロミスをよく理解することが重要です。 それらを理解し、使用してください。 まだObservablesについて心配する必要はありません。 状況に応じて、3つすべてを開発に含めることができます。