コールバック/Promise/Await、それぞれの実装の違いを見る
ES7ではPromise処理を簡素化できるawait/asyncが追加されています。これによってコールバック地獄からPromiseによって抜け出せたとの同様に、Promiseによる then/catch 地獄から抜け出せるようになります。
サンプルコード
例えば処理を2秒間遅らせて、その結果を取得すると言った処理を考えてみます。ES5で書くと次のようになるでしょう。
| function delay(x, f) { | |
| setTimeout(function() { | |
| f(x); | |
| }, 2000); | |
| } | |
| function main() { | |
| delay(10, function(x) { | |
| console.log(x); | |
| }); | |
| } | |
| main(); |
さらにdelayを2回呼び出すと次のようになります。ネストが一つ深くなります。
| function delay(x, f) { | |
| setTimeout(function() { | |
| f(x); | |
| }, 2000); | |
| } | |
| function main() { | |
| delay(10, function(x) { | |
| console.log(x); | |
| delay(20, function(y) { | |
| console.log(y); | |
| }); | |
| }); | |
| } | |
| main(); |
Promiseの場合
Promiseを使った場合は次のようになります。ネストが深くならず、thenを使ったメソッドチェーンが可能になります。
| function delay(x, f) { | |
| return new Promise(function(res) { | |
| setTimeout(function() { | |
| res(x); | |
| }, 2000); | |
| }); | |
| } | |
| function main() { | |
| delay(10) | |
| .then(function(x) { | |
| console.log(x); | |
| return delay(20); | |
| }) | |
| .then(function(y) { | |
| console.log(y); | |
| }); | |
| } | |
| main(); |
awaitを使った場合
さらにawaitを使った場合です。ネストが一段減りますが、処理を行う関数を async で囲む必要があるので、最低一つの関数の中で処理する必要があります。Promiseだけの場合は必ずしもmain関数は必要ではありません。
| function delay(x) { | |
| return new Promise(res => { | |
| setTimeout(() => { | |
| res(x); | |
| }, 2000); | |
| }); | |
| } | |
| async function main() { | |
| var x = await delay(10); | |
| console.log(x); | |
| var y = await delay(20); | |
| console.log(y); | |
| } | |
| main(); |
awaitのコードをES5に変換する
ではこのawaitのコードをBabelを使ってES2015のコードに変換した場合、どのようなコードになるのでしょうか。以下のコードはそのままnodeで動くわけではありませんが、次のようなコードが生成されます。
await的な機能を実現するために while ループを続けているのが分かります。ちなみにこれは delay を一回しか使っていない場合です。
| "use strict"; | |
| var main = function () { | |
| var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() { | |
| var x; | |
| return regeneratorRuntime.wrap(function _callee$(_context) { | |
| while (1) { | |
| switch (_context.prev = _context.next) { | |
| case 0: | |
| _context.next = 2; | |
| return delay(10); | |
| case 2: | |
| x = _context.sent; | |
| console.log(x); | |
| case 4: | |
| case "end": | |
| return _context.stop(); | |
| } | |
| } | |
| }, _callee, this); | |
| })); | |
| return function main() { | |
| return _ref.apply(this, arguments); | |
| }; | |
| }(); | |
| function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } | |
| function delay(x) { | |
| return new Promise(function (res) { | |
| setTimeout(function () { | |
| res(x); | |
| }, 2000); | |
| }); | |
| } | |
| main(); |
delay を2回実行すると次のようになります。 context.next/context.prevを使ってステータスを管理することで、ネストが深くなるのを防いでいます。ですが、switchで使っている数字は非同期処理が追加されるごとに数字が増えていきます。Babelの生成したコードを読み取るのは大変になっていくことでしょう。
| "use strict"; | |
| var main = function () { | |
| var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() { | |
| var x; | |
| return regeneratorRuntime.wrap(function _callee$(_context) { | |
| while (1) { | |
| switch (_context.prev = _context.next) { | |
| case 0: | |
| _context.next = 2; | |
| return delay(10); | |
| case 2: | |
| x = _context.sent; | |
| console.log(x); | |
| _context.next = 6; | |
| return delay(20); | |
| case 6: | |
| x = _context.sent; | |
| console.log(y); | |
| case 8: | |
| case "end": | |
| return _context.stop(); | |
| } | |
| } | |
| }, _callee, this); | |
| })); | |
| return function main() { | |
| return _ref.apply(this, arguments); | |
| }; | |
| }(); | |
| function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } | |
| function delay(x) { | |
| return new Promise(function (res) { | |
| setTimeout(function () { | |
| res(x); | |
| }, 2000); | |
| }); | |
| } | |
| main(); |
なお、Promiseの場合はrejectを使いますが、awaitを使った場合はtry/catchでエラー処理を行います。
| async function main() { | |
| var x = await delay(10); | |
| console.log(x); | |
| try { | |
| var y = await delay(20); | |
| console.log(y); | |
| }catch(e) { | |
| console.log('Error'); | |
| } | |
| } |
非同期処理はJavaScriptに付きもので、一番厄介な存在でしょう。しかしES7のawaitによってネストがなくなったり、変数を同じスコープで扱えるようになります。現在、デスクトップやスマートフォンのほとんどのブラウザでサポートされていますので(IE系は未サポート)、HTML5を活用する際にはぜひ使ってみてください。
コメントは受け付けていません。