みなさま、こんにちは!
2013年度新卒入社の吉成祐人(@y_yoshinari)と申します。

昨年の12月に社内フレームワークに関する記事を執筆させていただいたので、9ヶ月ぶりの執筆です。⇒近日公開予定JSフレームワークBeez

現在私はなぞってピグキッチンというサービスのフロントエンドの実装を担当しています。

先日、弊社のインターンシップで『フロントエンドの実装作業の効率化』に関する講義をさせていただいたので、今回はその講義スライドを共有したいと思います。


ではでは。



って感じで終わらせても良いのですが、さすがに既に公開しているスライドをペケっと貼っただけだと味気ないですね。

なので、今回はこのスライドの中で紹介している社内ライブラリbucks.jsに関しても簡単にですが書きたいと思います。

フロー制御ライブラリbucks.js

bucks.jsは弊社がオープンソースで公開しているフロー制御ライブラリです。
Github: https://github.com/CyberAgent/bucks.js

フロー制御ライブラリを用いるとプログラムの処理の流れを制御できるようになります。
それにより、非同期処理が入った際にプログラムを綺麗に書けるようになります。

1つ例を挙げるとすると、しばしばJavaScriptに対する批判の対象となるコールバック地獄が減ったりします。

非同期処理

まず、非同期処理の説明をします。

非同期処理というのは単純に言うと、通常のフローから外れて実行される処理です。
例として、setTimeoutによる遅延処理やXMLHttpRequestによる通信処理があります。

・setTimeout
setTimeoutは第2引数に与えたミリ秒後に第1引数に与えたコールバック関数を実行します。
第1引数のコールバック関数は非同期の処理となり、この関数の実行を待たずにsetTimeout以降の処理は実行されます。

例を挙げます。
console.log('a')
setTimeout(function() {
console.log('b');
}, 1000);
console.log('c');
出力は下記のようになります。
a
c ← setTimeoutのコールバック関数の実行を待たずに出力される
b ← 1000ミリ秒後に出力される

・XMLHttpRequest
XMLHttpRequestの通信も同様です。
var xhr = new XMLHttpRequest();
xhr.addEventListener('readystatechange', function(e) {
if (xhr.readyState === 4) {// 通信処理が終わったとき
if (xhr.status === 200) {// 通信が成功したとき
console.log(xhr.responseText);// 通信結果
}
}
}, false);
xhr.open('GET', 'data.json');
xhr.send(null);
これはreadystatechangeのイベントが走ったタイミング(xhrオブジェクトの状態が変わったタイミング)で、コールバック関数が実行されます。

bucksの説明

それではフロー制御ライブラリのbucksを使い方と合わせて見ていきます。

まず、newを用いてbucksのオブジェクトを生成します。
var bucks = new Bucks();
bucksのaddメソッドを用いて走らせたい処理を順番に追加します。
各々の処理は、nextが叩かれたタイミングで次の処理へと進みます。
bucks.add(function(err, res, next) {
// 行いたい処理1
next();// 次にaddされている処理に進む
});
bucks.add(function(err, res, next) {
// 行いたい処理2
next();// 次にaddされている処理に進む
});
bucks.add(function(err, res, next) {
// 行いたい処理3
next();// 次にaddされている処理に進む
});
ただ、この時点ではbucksに処理が追加されているだけでまだ実行はされていません。

bucksのendメソッドを呼ぶ事により、追加された処理が逐次実行されていきます。
bucks.end();// 全ての処理を逐次実行
bucksのaddメソッドは自身を返却するので、全ての処理は続けて書く事が可能です。
それにより、上記の処理は下記のようにまとめて書く事が出来ます。
new Bucks()
.add(function(err, res, next) {
// 行いたい処理1
next();// 次にaddされている処理に進む
})
.add(function(err, res, next) {
// 行いたい処理2
next();// 次にaddされている処理に進む
})
.add(function(err, res, next) {
// 行いたい処理3
next();// 次にaddされている処理に進む
})
.end();// 全ての処理を逐次実行
ちなみにコールバック関数の引数の、errとresとnextはそれぞれ下記になります。
err :前の処理でエラーが発生した場合、そのエラー内容が格納される。
res :前の処理でnextに引数を渡して実行した場合、その内容が格納される。
これにより前の処理の結果を次の処理に渡す事が出来る。
next:次の処理へと処理を進める為のコールバック関数。
また上記の逐次処理だけでなく、parallelというメソッドを用いることで並列処理を実現する事もできます。
new Bucks()
.parallel([// 処理1~3を並列で実行する
function(err, res, next) {
// 行いたい処理1
next();
},
function(err, res, next) {
// 行いたい処理2
next();
},
function(err, res, next) {
// 行いたい処理3
next();
}
])
.add(function(err, res, next) {// 処理1~3のnextが全て叩かれてから処理が走る
// 行いたい処理4
next();
})
.end();
他にもBucksを用いるといろいろと出来るのですが、詳しくはGithubのREADME.mdを見ていただければと思います。
Github: https://github.com/CyberAgent/bucks.js

この後は、非同期処理が入ってきた際に逐次処理と並列処理をbucksでどのように書けるのかを見ていきます。

非同期処理の入った逐次処理

1000ミリ秒後にaと出力して、
その2000ミリ秒後にbと出力して、
その3000ミリ秒後にcと出力して、
その4000ミリ秒後にdと出力して、
その5000ミリ秒後にeと出力するプログラムを書くとします。

その際のコードは下記のようになります。
setTimeout(function() {// 1000ミリ秒遅延させる
console.log('a');
setTimeout(function() {// 2000ミリ秒遅延させる
console.log('b');
setTimeout(function() {// 3000ミリ秒遅延させる
console.log('c');
setTimeout(function() {// 4000ミリ秒遅延させる
console.log('d');
setTimeout(function() {// 5000ミリ秒遅延させる
console.log('e');
}, 5000);
}, 4000);
}, 3000);
}, 2000);
}, 1000);
どんどんコールバック関数が入れ子になっていきますね。
これが俗にいうコールバック地獄です。

これをbucksを用いて書くと下記のように書く事が出来ます。
new bucks()
.add(function(err, res, next) {
setTimeout(function() {// 1000ミリ秒遅延させる
console.log('a');
next();
}, 1000);
})
.add(function(err, res, next) {
setTimeout(function() {// 2000ミリ秒遅延させる
console.log('b');
next();
}, 2000);
})
.add(function(err, res, next) {
setTimeout(function() {// 3000ミリ秒遅延させる
console.log('c');
next();
}, 3000);
})
.add(function(err, res, next) {
setTimeout(function() {// 4000ミリ秒遅延させる
console.log('d');
next();
}, 4000);
})
.add(function(err, res, next) {
setTimeout(function() {// 5000ミリ秒遅延させる
console.log('e');
next();
}, 5000);
})
.end();
コード自体は長くなってしまいましたが、コールバック地獄が解消されて処理の流れを追いやすくなったと思います。

またbucksにはdelayという遅延メソッドも用意されているため、下記のような書き方も可能です。
new bucks()
.delay(1000)// 1000ミリ秒遅延させる
.add(function(err, res, next) {
console.log('a');
next();
})
.delay(2000)// 2000ミリ秒遅延させる
.add(function(err, res, next) {
console.log('b');
next();
})
.delay(3000)// 3000ミリ秒遅延させる
.add(function(err, res, next) {
console.log('c');
next();
})
.delay(4000)// 4000ミリ秒遅延させる
.add(function(err, res, next) {
console.log('d');
next();
})
.delay(5000)// 5000ミリ秒遅延させる
.add(function(err, res, next) {
console.log('e');
next();
})
.end();

・おまけ
setTimeoutは下記のようにやれば、コールバック地獄にならずには済むのですが、XMLHttpRequestの通信だともう通信してみるまでコールバック関数が呼ばれるまでの時間が分からないので、コールバック地獄は避けられないと思います。
setTimeout(function() {
console.log('a');
next();
}, 1000);
setTimeout(function() {
console.log('b');
next();
}, 1000 + 2000);
setTimeout(function() {
console.log('c');
next();
}, 1000 + 2000 + 3000);
setTimeout(function() {
console.log('d');
next();
}, 1000 + 2000 + 3000 + 4000);
setTimeout(function() {
console.log('e');
next();
}, 1000 + 2000 + 3000 + 4000 + 5000);

非同期処理の入った並列処理

互いに依存し合っていない2つの通信(fooとbar)を行う状況があり、その通信の結果を用いて画面を生成したい状況があるとします。
var resFoo = null,
resBar = null,
responseCount = 0,
xhrFoo = null,
xhrBar = null;

// foo.jsonへのGET通信
xhrFoo = new XMLHttpRequest();
xhrFoo.addEventListener('readystatechange', function(e) {
if (xhrFoo.readyState === 4 && xhrFoo.status === 200) {
responseCount++;
resFoo = JSON.parse(xhrFoo.responseText);
onEndRequest();
}
}, false);
xhrFoo.open('GET', 'foo.json');
xhrFoo.send(null);

// bar.jsonへのGET通信
xhrBar = new XMLHttpRequest();
xhrBar.addEventListener('readystatechange', function(e) {
if (xhrBar.readyState === 4 && xhrBar.status === 200) {
responseCount++;
resBar = JSON.parse(xhrBar.responseText);
onEndRequest();
}
}, false);
xhrBar.open('GET', 'bar.json');
xhrBar.send(null);

function onEndRequest() {
// 2つの通信がまだ返ってきていなかったら何もしない
if (respponseCount < 2) {
return;
}
createView();
};

function createView() {
// resFoo と resBar を用いて画面を生成
};
これをbucksを用いて書くと下記のように書く事が出来ます。
new bucks()
.parallel([
function(err, res, next) {
var xhr = new XMLHttpRequest(),
resFoo = null;
xhr.addEventListener('readystatechange', function(e) {
if (xhr.readyState === 4 && xhr.status === 200) {
resFoo = JSON.parse(xhr.responseText);
next(resFoo);// 通信の結果を渡してnextを叩く
}
}, false);
xhr.open('GET', 'foo.json');
xhr.send(null);
},
function(err, res, next) {
var xhr = new XMLHttpRequest(),
resBar = null;
xhr.addEventListener('readystatechange', function(e) {
if (xhr.readyState === 4 && xhr.status === 200) {
resBar = JSON.parse(xhr.responseText);
next(resBar);// 通信の結果を渡してnextを叩く
}
}, false);
xhr.open('GET', 'bar.json');
xhr.send(null);
}
])
.add(function(err, res, next) {
// resにはparallelの中のnextに与えた引数が配列で格納されている
var resFoo = res[0],
resBar = res[1];
createView(resFoo, resBar);
next();
})
.end();

function createView(resFoo, resBar) {
// resFoo と resBar を用いて画面を生成
};
bucksを用いる事で、全ての通信が終わっているかどうかの管理を自分でする必要が無くなり、コードの流れも追いやすくなったと思います。
さらに、これはコードの書き方を工夫すればそもそも減らせはするのですが、グローバル変数も減らす事が出来ました。

また、parallelの引数の配列にメソッドを追加すれば、通信と同時に通信と関係ない処理を走らせておく事も出来ます。

まとめ

すごくザックリとした説明になってしまいましたが、使いやすいライブラリなので是非とも1度使ってみてください!
Github: https://github.com/CyberAgent/bucks.js

記事の内容がbucks.jsの内容ばかりっぽくなってしまいましたが、あくまでこの記事のメインの内容は『フロント作業の効率化』のつもりです!
なので、スライドの方も読んでいただけるとありがたいです!

拙い記事ではございますが、最後まで目を通していただきありがとうございました。