Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
6
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

@niisan-tokyo

Koaへの道: JavaScriptのジェネレータを使って非同期処理をコールバックを(あまり)使わずに実現する - 実践編

前回、ジェネレータを使用することで非同期処理を順次処理として書くことが可能となりました。
しかし、前回書いた書き方だと、少々物足りないというか、汎用性にかけるものとなっています。
今回は、まずジェネレータによる非同期処理の記法をもう少し洗練させて、汎用性を高めてみましょう。
ついで、パッケージであるcoを使って、非同期処理を書く方法を習得していきたいと思います。

ジェネレータによる非同期処理の順次記述

非同期処理が終了した時に、その値をnextに入れて処理を再開する、という形で、ジェネレータによる非同期処理の順次記述が実現できていました。
これをより手軽にできるような仕組みを作りましょう

非同期処理の外出し

前回の実装を思い出しましょう

'use strict'

//無理やり順次処理
function* hell(n) {
  console.log(n);
  const x = yield setTimeout(() => gen.next(n * n), 1000);
  console.log(x);
  const y = yield setTimeout(() => gen.next(x * x), 1000);
  console.log(y);
  const z = yield setTimeout(() => gen.next(y * y * y), 1000);
  console.log(z);
}

const gen = hell(2);
gen.next();

これだと、非同期処理の内容がジェネレータの中に入り込んでいます。
まずはこれらを外に出しましょう

'use strict'

function* somethings() {
  const n = yield;
  const x = yield afterSqure(n);
  const y = yield afterSqure(x);
  const z = yield afterCube(y);
  console.log([n, x, y, z]);
}

//1秒後に2乗した値を返す
function afterSqure(n) {
  setTimeout(() => gen.next(n * n), 1000);
}

//1秒後に3乗した値を返す
function afterCube(n) {
  setTimeout(() => gen.next(n * n * n), 1000);
}

const gen = somethings();
gen.next();
gen.next(2);

こんな感じです。
ちょっと結果も変えていますが、ご容赦願います。
しかし、最後の3行がいけませんね。
裸の処理が外に出てしまっていて、少々ブサイクです。
これらは関数にラッピングしてしまいましょう

'use strict'

function* somethings() {
  const n = yield;
  const x = yield afterSqure(n);
  const y = yield afterSqure(x);
  const z = yield afterCube(y);
  console.log([n, x, y, z]);
}

function afterSqure(n) {
  setTimeout(() => gen.next(n * n), 1000);
}

function afterCube(n) {
  setTimeout(() => gen.next(n * n * n), 1000);
}

// ジェネレータの制御関数
function processor(generator) {
  const gen = generator();
  gen.next();
  gen.next(2);
}

processor(somethings);// 失敗します。。。

あれ?こんなエラーが出ます
ReferenceError: gen is not defined

これまではグローバルでconst genを定義していたため、関数から参照できていましたが、今回は関数processorの中でgenを定義しているので、当然ながらafterSqureなどの別の関数からは参照できないということです。

非同期処理へのジェネレータの渡し方

上述のコードには最大の欠点があります。afterSqureafterCubeが、非同期関数のくせにコールバック関数が固定化されているというところです。
一般的には、非同期関数はユーザーが任意にコールバック関数を指定できるのが普通です。
つまり、

function afterSqure(n, callback) {
  setTimeout(() => callback(n * n), 1000);
}

function afterCube(n, callback) {
  setTimeout(() => callback(n * n * n), 1000);
}

このように書いてしかるべきでした。
とすると、今度はジェネレータ側でも非同期関数のコールバックを指定してあげなければなりません。
しかし、どうやってアクセスしたものか。
そこで、クロージャを使ってアクセスする方式が考案されています。
次のコードを見てください

'use strict'

// ジェネレータの定義
function* somethings(next) {
  const n = 2;
  const x = yield afterSqure(n, next);
  const y = yield afterSqure(x, next);
  const z = yield afterCube(y, next);
  console.log([n, x, y, z]);
}

//1秒後に2乗した値を返す
function afterSqure(n, callback) {
  setTimeout(() => callback(n * n), 1000);
}

//1秒後に3乗した値を返す
function afterCube(n, callback) {
  setTimeout(() => callback(n * n * n), 1000);
}

// ジェネレータの制御
function processor(generator) {
  const gen = generator(val => gen.next(val));
  gen.next();
}

processor(somethings);// [2, 4, 16, 4096]

ジェネレータの制御構造は非常に単純です。

  1. まずジェネレータsomethingは引数として関数をとる(ここではnextと名付けられている)
  2. ジェネレータ定義内の各非同期関数は、コールバック関数としてジェネレータの引数で与えられた関数を使用する
  3. ジェネレータの制御関数内で、ジェネレータを生成する際に、無名関数を引数に渡す。この無名関数はジェネレータのnextメソッドに与えられた引数を渡して実行する関数である。

こんな感じで、ジェネレータを使って、非同期処理をスッキリした形で書くことができます。

Promiseとco

Promise、ぶっちゃけよくわかりません。
ですが、こいつとcoを組み合わせることにより、ジェネレータのnextを一切意識することなく非同期処理を順次処理として書くことができます。

必要なPromiseの知識

とりあえず、目的達成に必要な知識だけ

  1. Promiseをnew するときは関数を引数にする(new Promise(function(){}))
  2. Promiseのコンストラクタの引数である関数は、引数に2つまで関数を指定できる(new Promise(function(resolve, reject){})
  3. コンストラクタで指定した関数の中で、「成功した」場合はresolve関数に値を渡し、「失敗した」場合はreject関数に値を渡す
  4. 生成したPromiseオブジェクトに対して、thenメソッドを使用し、成功時と失敗時の動作メソッドを登録すると、処理が始まる。

実装はこんな感じです

function prom(n) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (n % 2 == 1) {
        resolve('成功!!');
      } else {
        reject('失敗');
      }
    }, 500)
  });
}

function test(n) {
  p = prom(n);
  p.then(val => console.log(val), val => console.log('残念ながら' + val));
}

test(2);// 残念ながら失敗です
test(3);// 成功!!
test(4);// 残念ながら失敗です

thenメソッドはじめに登録した関数が「成功時」の処理で、後に登録した関数が「失敗時」の処理です。

coで非同期処理を書く

coはジェネレータとPromiseを使って非同期処理を順次処理の書き方にすることができます。
ジェネレータだけで書いた処理を以下のように書き換えることができます

'use strict'

const co = require('co');

// ジェネレータの定義
function* somethings() {
  const n = 2;
  const x = yield afterSqure(n);
  const y = yield afterSqure(x);
  const z = yield afterCube(y);
  console.log([n, x, y, z]);
}

//1秒後に2乗した値を返す
function afterSqure(n) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(n * n), 1000);
  });
}

//1秒後に3乗した値を返す
function afterCube(n) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(n * n), 1000);
  });
}

co(somethings);// [2, 4, 16, 4096]

coパッケージが必要ですので、使用前にnpm i coコマンドを打っておく必要があります。
コードを見ると、もともとジェネレータの制御関数があった部分をco(somethings)に置き換えています。
そして、ジェネレータのみの時はnextメソッドをジェネレータに渡していましたが、今回は必要なくなっています。
yieldに展開される値は、Promiseの成功時、resolve関数に渡された値になる模様です
Koaはcoと同調しているはずなので、多分使っているのはこっちの方法のはずです

まとめ

説明とかを見ただけではなんのことだかわからなかったものですが、自分で打つことで、なんとか理解することができました。
これできっとKoaの実装ができるはずだ!
。。。そう思いたいところです

参考

generatorとJavaScriptの非同期処理

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
6
Help us understand the problem. What are the problem?