非同期をわかりやすく制御しよう!ES6の「Promise」入門

先生


非同期をわかりやすく制御しよう!ES6の「Promise」入門
(編集部注*2015年10月28日に公開された記事を再編集したものです。)

カメラを新調して写真撮りまくってます。CTOの林です。
ブラウザでの対応が進んできたこともあり、ES6がかなり注目されてきています。今日はその中でも「Promise」について簡単に解説していきたいと思います。

▼目次

Promiseとは

Promiseは、非同期の処理をわかりやすく制御するオブジェクトで、ES6(ES2015/ECMAScript 2015)に追加された機能の1つです。

 
スクリーンショット 2016-06-07 19.12.57
http://caniuse.com/#feat=promises

現在は、多くのブラウザですでに実装されています。

また、多くのPolyfillが作られているため、非対応のIEでもPromiseを使うことができます。

JavaScriptでは多くの非同期処理をします。とくに、Node.jsでは非同期をうまく制御することが開発の肝と言っても過言ではないくらい多くの処理が非同期で行われます。かつてはDefferdやAsyncといったライブラリを使ってこれらを制御していましたが、それをネイティブで対応できるようになったものがPromiseです。

では、さっそくPromiseの使い方を見てみましょう。

Promiseの使い方

var p = new Promise(function(resolve,reject) {
    window.setTimeout(function() {
        resolve(true);
        //reject('error');
    },1000);
});
p.then(function(result) {
    console.log('OK:' + result);
}, function(e) {
    console.log('NG:' + e);
});

Promiseはnewして使います。また第一引数に関数を必ず指定をします。関数内では成功時の処理(resolve)と失敗時の処理(reject)を実行する引数が取得できます。

resolve(渡したい値);    // 成功時
reject(渡したい値);        // 失敗時

newされたPromiseは then(onFulfilled,onRejected),catch(onRejected) という関数を持つオブジェクトを返します。

thenは第一引数にresolveされたときの処理を、第二引数にrejectされたときの処理をそれぞれ受け取ります。先ほどのサンプルコードでは以下の部分がそれに当たるところです。

p.then(function(result) {
    console.log('OK:' + result);
}, function(e) {
    console.log('NG:' + e);
});

catchは失敗時の処理だけを受け取ることができます。上記は、下記のように記述しても同じ結果となります。

p.then(function(result) {
    console.log('OK:' + result);
}).catch(function(e) {
    console.log('NG:' + e);
});

catchはエラーの処理が明確になるだけでなく、後述していく複数の非同期処理のエラーをまとめて制御するためにも使用されるので、基本的にはcatchを使って失敗時の処理を記述することをオススメします。

Promiseを使って同期的に処理する

Promiseを使って複数の非同期処理を順番に制御してみましょう。

function func1(num) {
    return new Promise(function(resolve,reject) {
        window.setTimeout(function() {
            resolve(num);
        },1000);
    });
}
function func2(num) {
    return new Promise(function(resolve,reject) {
        window.setTimeout(function() {
            resolve(num*num);
        },1000);
    });
}

func1(2)
    .then(func2)
    .then(function(result) {
        console.log(result);
    })
    .catch(e) {
        console.error(e);
    });

1444963921693

動作確認

func1の処理の完了を待ち、処理結果をfunc2で受けてデータを返すという形で順番に処理が実行されます。コードとしても順番に処理されているのがわかりやすくなっています。

これを従来のコードで書いた場合には、下記のようになります。

function func1(num,callback,onFailed) {
    window.setTimeout(function() {
        callback(num);
    },1000);
}
function func2(num,callback,onFailed) {
    window.setTimeout(function() {
        callback(num*num);
    },1000);
}

func1(2, function(num) {
    func2(num, function(result) {
        console.log(result);
    }, function(e) {
        console.error(e);
    });
}, function(e) {
    console.error(e);
});

このようにするとfuncがどんどん入れ子になってインデントが深くなっていきます。エラーの処理もしにくくなり読みにくくなっていますね。まだ2つだから良いですが、これが5つ6つとなっていくとメンテナンス性はさらに悪くなっていきます。

func1(2, function(num) {
    func2(num, function(num) {
        func3(num, function(num) {
            func4(num, function(num) {
                func5(num, function(num) {
                    console.log(num);
                });
            }); 
        });
    });
});

F

これをPromiseを用いた場合は、下記のようにすっきりとさせることが可能です。

func1(2)
    .then(func2)
    .then(func3)
    .then(func4)
    .then(func5)
    .then(function(num) {
        console.log(num);
    });

どちらが処理を追いやすいかは一目瞭然ですね。

Promiseを使って並列処理して完了を検知する

続いて、複数の非同期処理を並列で実行し、すべてが完了したら情報を取得する処理をしたいと思います。これが非同期のメリットですものね。

function func1(num) {
    return new Promise(function(resolve,reject) {
        window.setTimeout(function() {
            resolve(num+num);
        },500);
    });
}
function func2(num) {
    return new Promise(function(resolve,reject) {
        window.setTimeout(function() {
            resolve(num*num);
        },2000);
    });
}

Promise.all([func1(3),func2(3)]).then(function(results) {
    console.log(results);
}).catch(function(e) {
    console.log(e);
});

1444964780197

動作確認

Promise.all([Promise,Promise,...])を使って非同期な処理を並列でおこない、すべてがresolveになるタイミングをthenで取得します。

並列で処理しているものが1つでもrejectとなった場合はcatchが実行されます。

function func1(num) {
	return new Promise(function(resolve,reject) {
		window.setTimeout(function() {
			resolve(num+num);
		},500);
	});
}
function func2(num) {
	return new Promise(function(resolve,reject) {
		window.setTimeout(function() {
			reject(num*num);
		},2000);
	});
}

Promise.all([func1(3),func2(3)]).then(function(results) {
	console.log(results);
}).catch(function(e) {
	console.log(e);
});

1444964998108

動作確認

従来は完了数をカウントして全部終わったかどうかを確認するなどしていましたが、とても簡単に並列処理の完了を捕捉することができます。

1個でも処理が返ってきたら実行する

続いて少し用途は狭くなりますが、並列で処理しているものが1つでもresolveになったら実行する処理を作りたいと思います。

function func1(num) {
    return new Promise(function(resolve,reject) {
        window.setTimeout(function() {
            resolve(num+num);
        },500);
    });
}
function func2(num) {
    return new Promise(function(resolve,reject) {
        window.setTimeout(function() {
            reject(num*num);
        },2000);
    });
}

Promise.race([func1(3),func2(3)]).then(function(results) {
  console.log(results);
}).catch(function(e) {
  console.log(e);
});

1444974524299

動作確認

Promise.race([Promise,Promise,...])を使うことで並列で実行した処理でもっとも早く結果を返してきたらthenやcatchが実行されるようになります。もし最初に結果を返してきたものがrejectであれば、catchが実行されます。

使いどころはやや難しいですね。

まとめ

このようにPromiseを使うと非同期の処理をとても制御しやすくなります。とくに、Node.jsでは非同期処理が多いので積極的にPromiseを活用して制御していきましょう。

なお、本稿ではPromiseesというPromiseの挙動を可視化するツールを用いて紹介しました。Promiseの処理が複雑になってしまったときの活用や動作フローの確認などに使えるので、積極的に活用してみてください。

それでは、また。

先生
この記事を書いた人
先生

最高技術責任者

関連記事