はじめに
JavaScriptを同期的に処理させたい!
これはちょっと複雑なスクリプトを組むと出てくる要望であり、来たるコールバック地獄への入り口でもあります。
しかし幸いな事にJavaScriptにはこれを解決させる手段としてPromiseが用意されています。
この時代に生まれてよかった。心からそう思ったところで、いざJavaScriptのPromiseを利用しようと調べると、JavaScriptのネイティブで実装する方法とjQueryを利用する方法(Deferred)の2種類があることに気が付くでしょう。
ネイティブでできるならネイティブでやればいいじゃない!
普通はそう考えるでしょう。私はそう考えました。
そしてすべての実装が終わってから気づくのです。動作対象の端末にiOS9が含まれていたことを。iOS9はES6に対応していないことを。ネイティブのPromiseはES6での実装だということを!!
これは、ネイティブのPromiseで書かれたソースを泣く泣くjQueryのDeferredに置き換えた私が、この先同じ過ちを繰り返した場合の為に残す備忘録です。
Promiseを使わないJavaScript
まずはサンプルとしてPromiseを利用しないJavaScriptでQiitaについてのHello Worldメッセージを表示しようと思ます。
下記のサンプルコードで期待される結果は、コードの上から順に実行されて「Qiitaは、プログラミングに関する知識を記録・共有するためのサービスです。」と、コンソールに表示されることです。
function helloWorld() {
console.log('Qiitaは、');
// 重たい処理
createMessage();
// ちょっと重たい処理
setTimeout(function() {
console.log('記録・共有するための');
}, 100);
console.log('サービスです。');
}
function createMessage() {
setTimeout(function() {
console.log('プログラミングに関する知識を');
}, 300)
}
しかし、実際にはこうなります。
「Qiitaは、サービスです。記録・共有するためのプログラミングに関する知識を」
当然ですね。
これはあまり良い事例ではないかもしれませんが、一連の流れのなかで思い通りの順番に関数を実行させたいんだ!という思いは伝わったかと思います。
Promiseを使ったJavaScript
それではPromiseを利用してサンプルコードを意図した通りに実行してみます。
とりあえずthenメソッドで繋げる
これはまったく意味のないサンプルですが、まずはシンプルな状態から考えるということで。
個々の処理をthenメソッドで引数を渡して連結し、最後に出力してみます。
ネイティブ(Promise)
thenメソッドで連結してみます。
Promise.resolve()
.then(function() {
return Promise.resolve('Qiitaは、');
})
.then(function(result) {
return Promise.resolve(result + 'プログラミングに関する知識を');
})
.then(function(result) {
return Promise.resolve(result + '記録・共有するための');
})
.then(function(result) {
return Promise.resolve(result + 'サービスです。');
})
.then(function(result) {
console.log(result);
});
jQuery(Deferred)
ネイティブとほとんど同じです。
$(function() {
var defer = new $.Deferred().resolve();
defer.promise()
.then(function() {
return 'Qiitaは、';
})
.then(function(result) {
return result + 'プログラミングに関する知識を';
})
.then(function(result) {
return result + '記録・共有するための';
})
.then(function(result) {
return result + 'サービスです。';
})
.done(function(result) {
console.log(result);
});
});
関数を同期処理してみる
createMessage関数のようにしてメソッド化する事でコードをすっきりさせてみます。
外部メソッド内で処理に失敗した場合に全体の処理も停止させるようにrejectとcatchも使ってみます。
ネイティブ(Promise)
Promise.resolve()
.then(function() {
return Promise.resolve('Qiitaは、');
})
.then(function(result) {
// 重たい処理
return createMessage(result);
})
.then(function(result) {
// ちょっと重たい処理
return new Promise((resolve, reject) => {
setTimeout(function() {
if (result) {
var message = result + '記録・共有するための';
resolve(message);
} else {
reject();
}
}, 100)
});
})
.then(function(result) {
var message = result + 'サービスです。';
console.log(message);
})
.catch(function() {
console.log('error');
});
createMessage = function(val) {
return new Promise((resolve, reject) => {
setTimeout(function() {
if (val) {
resolve(val + 'プログラミングに関する知識を');
} else {
reject();
}
}, 300);
});
}
jQuery(Deferred)
これもまたネイティブとほとんど同じです。
$(function() {
var defer = new $.Deferred().resolve();
defer.promise()
.then(function() {
return 'Qiitaは、';
})
.then(function(result) {
// 重たい処理
return createMessage(result);
})
.then(function(result) {
// ちょっと重たい処理
return new $.Deferred(function(defer) {
setTimeout(function() {
if (result) {
var message = result + '記録・共有するための';
defer.resolve(message);
} else {
defer.reject();
}
}, 100)
}).promise();
})
.done(function(result) {
var message = result + 'サービスです。';
console.log(message);
})
.catch(function() {
console.log('error');
});
function createMessage(val) {
var defer = new $.Deferred();
setTimeout(function() {
if (val) {
defer.resolve(val + 'プログラミングに関する知識を');
} else {
defer.reject();
}
}, 300);
return defer.promise();
}
});
並列に処理してみる
例えば、AとBの処理が両方完了したらCの処理へ進む、といった並列処理をしてみます。
ネイティブ(Promise)
allメソッド内のすべてのPromiseがresolveされたらthenが呼び出されます。
もちろん結果も渡すことができます。
Promise.resolve()
.then(function() {
return Promise.all([
new Promise(function(fulfilled, rejected) {
fulfilled('Qiitaは、');
}),
new Promise(function(fulfilled, rejected) {
fulfilled('プログラミングに関する知識を');
})
]);
})
.then(function(result) {
// allで呼び出された順にresultへ格納されている
return result[0] + result[1];
})
.then(function(result) {
// 重たい処理
return createMessage(result);
})
.then(function(result) {
// ちょっと重たい処理
return new Promise((resolve, reject) => {
setTimeout(function() {
if (result) {
var message = result + 'サービスです。';
resolve(message);
} else {
reject();
}
}, 100)
});
})
.then(function(result) {
console.log(result);
})
.catch(function() {
console.log('error');
});
createMessage = function(val) {
return new Promise((resolve, reject) => {
setTimeout(function() {
if (val) {
resolve(val + '記録・共有するための');
} else {
reject();
}
}, 300);
});
}
jQuery(Deferred)
ネイティブとは異なりwhenを使います。
結果の受け取り方も少し異なります。
$(function() {
var defer = new $.Deferred();
$.when(
new $.Deferred(function(defer) {
defer.resolve('Qiitaは、');
}).promise(),
new $.Deferred(function(defer) {
defer.resolve('プログラミングに関する知識を');
}).promise()
)
.then(function(result1, result2) {
return result1 + result2;
})
.then(function(result) {
// 重たい処理
return createMessage(result);
})
.then(function(result) {
// ちょっと重たい処理
return new $.Deferred(function(defer) {
setTimeout(function() {
if (result) {
var message = result + 'サービスです。';
defer.resolve(message);
} else {
defer.reject();
}
}, 100)
}).promise();
})
.done(function(result) {
console.log(result);
})
.catch(function() {
console.log('error');
});
function createMessage(val) {
var defer = new $.Deferred();
setTimeout(function() {
if (val) {
defer.resolve(val + '記録・共有するための');
} else {
defer.reject();
}
}, 300);
return defer.promise();
}
});
おわりに
iOS9も動作対象の場合は気を付けましょう。