ワイ「なに!?ジェネレーター関数を使えば複雑なCSSアニメーションも簡単やと!?」


業務中ワイ

ワイ「お、また株式会社ブラックはんからお仕事依頼のメールが来てるで」
ワイ「どれどれ・・・」

暗井「お世話になっております。株式会社ブラックの暗井 暗人(くらい・あんと)です」
暗井「クリックするとフワフワっとだんだん縦横に大きくなるボックスを作ってください」
暗井「予算は800万円です」

ワイ「これまた何ちゅうフワフワっとした指示や」
ワイ「でもjQueryanimateメソッド使えば余裕やろ」

ワイ「かしこまりました」
ワイ「何とか800万円以内に抑えます」

ワイ「送信っと」
ワイ「お、早速お返事や」

暗井「なお、発注元の都合でjQueryの使用はできません

ワイ「ファッ!?
ワイ「まじか・・・jQuery禁止か」
ワイ「800万円で出来るかな・・・」

社長「(それは出来るやろ・・・何年かける気や・・・)」

ザコーダーワイ、jQuery禁止で困る

ワイ「jQueryなしか・・・」
ワイ「jQueryのanimateみたいなこと、素のJSとCSSだけで出来んのかな・・・」

ハスケル子「できますよ
ハスケル子「今回の仕様ならジェネレーター関数なんて良いんじゃないですか」

ワイ「何やっけそれ」

ジェネレーター関数とは

ハスケル子「ジェネレーター関数っていうのは」
ハスケル子「実行すると、ジェネレーターを返す関数です」

ワイ「そのジェネレーターが分からんわ」

ハスケル子「ジェネレーターは、ある種のイテレーターです」

ワイ「今度はそのイテレーターが分からん。。。」

ハスケル子「・・・」
ハスケル子「簡単にいうと、関数の途中で一旦止まれるんです」
ハスケル子「具体的にはyieldって書くと、そこで関数の処理が一旦止まります」

やってみよう

ハスケル子「じゃあ実際やってみますね」


function* geneFunc () {
  console.log("1回目");
  yield;

  console.log("2回目");
  yield;

  console.log("3回目");
  yield;

  console.log("4回目");
  yield;

  console.log("最後");
}

ハスケル子「↑こんな感じでfunction*をつけるとジェネレーター関数になります」
ハスケル子「yieldが来るたびに処理が一旦止まって関数の外の世界に出るので」
ハスケル子「この関数は合計5回に分けて実行することができるんです。」

ワイ「ほえ〜」

ハスケル子「まず、geneFuncを実行して、戻り値であるジェネレーターを受け取ります」

const generator = geneFunc();

ワイ「この時点でconsole.log("1回目");が実行される感じ?」

ハスケル子「いえ、まだです」
ハスケル子「geneFuncを実行した時点では、関数の中の処理は何も実行されません

ハスケル子「↓こうするとconsole.log("1回目");が実行されます」

generator.next();

ワイ「なるほどな」
ワイ「generatorの持ってるnextメソッドを実行すると」
ワイ「yieldの所まで実行して、一旦止まってくれんねやな」

ハスケル子「はい」
ハスケル子「一旦止まって、関数の外で何か処理をして」
ハスケル子「またgenerator.next()で関数の中に戻ったりできるわけです」

ワイ「おお〜、ほな今回の例やとgenerator.next()を5回実行すると」
ワイ「geneFuncの中の処理を全部やり切ってくれるわけやね」

ハスケル子「そうです」
ハスケル子「慣れないうちはちょっと、いつどこで何をやってるのか分かりにくいかもしれませんけど」
ハスケル子「処理の順番とかタイミングを自在にコントロールできる強力な機能です」

ハスケル子「非同期処理と組み合わせて使うこともできて」
ハスケル子「redux-sagaなんかは」

ワイらはasync/awaitやなくてジェネレーター関数を使っていくんや!

ハスケル子「って表明してたりします」

ワイ「おお〜・・・」

そんで、どうやってフワフワっとさせんの?

ワイ「そんでハスケル子ちゃん」
ワイ「クリックするとフワフワっとだんだん縦横に大きくなるボックス・・・」
ワイ「これとジェネレータ、関係あるん・・・?」

ハスケル子「大ありです」
ハスケル子「ジェネレータ関数を使うとだいぶスッキリ書けますよ」
ハスケル子「まずは・・・」

const box = document.querySelector(".box");

ハスケル子「↑大きさを操作する対象となるDOM要素を取得します」
ハスケル子「そして・・・」

function* fuwafuwa () {
  let boxHeight = 30; // デフォルトの高さは30px
  let boxWidth = 30; // デフォルトの横幅は30px

  boxHeight += 30;
  box.style.height = boxHeight + "px"
  // boxを30px高くする
  yield;
  // 一旦止まる

  boxWidth += 30;
  box.style.width = boxWidth + "px"
  // boxを30px横長にする
  yield;
  // 一旦止まる
}

ハスケル子「↑fuwafuwaっていうジェネレータ関数を定義します」
ハスケル子「この関数の中の処理は・・・」

ワイ「box30px高くしてyieldで止まる」
ワイ「box30px横長にしてyieldで止まる」
ワイ「って感じやね」

ハスケル子「そうです」
ハスケル子「この処理を関数の中にいっぱい書きます」

ワイ「なるほど!」
ワイ「↓こんな感じやな・・・!」

  boxHeight += 30;
  box.style.height = boxHeight + "px"
  // boxを30px高くする
  yield;
  // 一旦止まる

  boxWidth += 30;
  box.style.width = boxWidth + "px"
  // boxを30px横長にする
  yield;
  // 一旦止まる

  boxHeight += 30;
  box.style.height = boxHeight + "px"
  // boxを30px高くする
  yield;
  // 一旦止まる

  boxWidth += 30;
  box.style.width = boxWidth + "px"
  // boxを30px横長にする
  yield;
  // 一旦止まる

  boxHeight += 30;
  box.style.height = boxHeight + "px"
  // boxを30px高くする
  yield;
  // 一旦止まる

  boxWidth += 30;
  box.style.width = boxWidth + "px"
  // boxを30px横長にする
  yield;
  // 一旦止まる

ハスケル子「・・・」
ハスケル子「まあ、それでもいいんですけど」
ハスケル子「while文使いましょう・・・」

  while (boxHeight < 300) {
    boxHeight += 30;
    box.style.height = boxHeight + "px"
    yield;

    boxWidth += 30;
    box.style.width = boxWidth + "px"
    yield;
  }

ハスケル子「こうしておけば」
ハスケル子「boxの高さが300pxになるまで繰り返してくれますよ」

ワイ「おお、ループの中でもyieldを使えるんやね」

関数をチョイチョイ止めながら実行できるようになった

ワイ「でも、これでクリックするとフワフワっと大きくなるボックス作れる・・・?」

ハスケル子「はい、ここでtransitionendイベントを使うんです」

ワイ「transitionendイベント・・・もしかして」
ワイ「transitionによるCSSプロパティの変化が終わった時に発火されるイベントかいな・・・!?」

ハスケル子「はい」

ワイ「おお、そんな便利なイベントが。。。」

ハスケル子「そのイベントと、ジェネレーターを組み合わせるんです」

const generator = fuwafuwa();

ハスケル子「↑まずはfuwafuwa関数を実行してジェネレータを受け取ります」

box.addEventListener("transitionend", () => generator.next());

ハスケル子「↑そして、boxtransitionが終了するたびに」
ハスケル子「generator.next()が実行されるように設定しておくんです」

ワイ「なるほどな」
ワイ「transitionによる動きが終了するたびに、イベントが発火してgenerator.next()が実行されて」
ワイ「それによって次のtransitionが始まり」
ワイ「またそのtransitionの動きが終わることでイベントが発火して・・・」
ワイ「って相互再帰してる感じなんやな」

ハスケル子「そうです」
ハスケル子「あとは、はじめにboxをクリックした時にもgenerator.next()が実行されるように」
ハスケル子「イベントリスナを登録しておきます」

box.addEventListener("click", () => generator.next());

ハスケル子「↑こうですね」
ハスケル子「実際に動かしてみると↓こんな感じです」

See the Pen fuwafuwa generator by Yametaro (@Yametaro) on CodePen.

ワイ「おお、フワフワっと大きくなってる!」
ワイ「ジェネレーターを活用すると、関数内の処理のタイミングを自在にコントロールできんねやな・・・!」
ワイ「ありがとう、ハスケル子ちゃん」
ワイ「お礼にお小遣い400万円あげるわ!」

社長「(なに売り上げを半分にしてんねん・・・!)」

〜おしまい〜

※登場人物、価格設定等はフィクションです。

追記1

ジェネレーター関数について学ぶなら↓こちらのサイトがおすすめです。
JavaScript初級者から中級者になろう 十六章第六回 ジェネレータ

追記2

IEも対応する場合は、Babel使ってな!

追記3 新しい記事を書いたで!

↓こちらの記事もよろしくやで!
すごいHaskell、ハスケル子と学ぼう!