JavaScript Promiseとアニメーション

JavaScriptの非同期処理管理オブジェクトPromiseを使用してアニメーションを連続で行うサンプルを作成しました。

Promiseはほとんど使ったことがなかったのですが、今回requestAnimationFrameメソッドによるアニメーション関数をつなぐため使用しました。アニメーション関数から直接次の関数を呼んだりsetTimeoutメソッドを使ってつなぐこともできますが、Promiseを使うとアニメーション関数群の依存関係をなくすことができたり関数の呼び出しを一か所にまとめて記述できるので、保守性や可読性の向上が期待できます。

Promiseについて参考にしたサイト

アニメーションについて参考にしたサイト

サンプル1

See the Pen Promise anim by senmyou (@senmyou) on CodePen.

サンプル2

See the Pen Promise anim by senmyou (@senmyou) on CodePen.

サンプルについて

  • Arrayオブジェクトのreduceメソッドを使ってPromiseオブジェクトをつなぐ方法は、JavaScript Promiseの本の4.8. Promiseによる逐次処理やMDNの合成 (Composition)で解説されています。
  • requestAnimationFrameメソッドに渡すコールバック関数は、引数でタイムスタンプ(ミリ秒)を受け取ります。このタイムスタンプについてAPIのドキュメントを見てもわからないことがあり実験してみました。結果的にタイムスタンプの説明に次の2点を追記できると思います。
    • タイムスタンプはhtmlのページが読み込まれてから経過した時間(ミリ秒)を示す
    • タイムスタンプをプログラムから0に戻すことはできない

    また、これらのことからタイムスタンプはある基準点との差分をとって使うことになると思います。サンプルではアニメーションの基準(開始)点として初回のrequestAnimationFrameメソッドのタイムスタンプをstartTimeに保存して、毎回現在のタイムスタンプと差分をとって進捗度(progress)の算出に使用しています。

Promise備忘録

Promiseについての個人的な備忘録です。

  • Promiseのコンストラクタはexecutorと呼ばれる関数を引数にとる。
  • executorの引数にresolve関数とreject関数が渡される。基本的にexecutorで非同期処理を行い、そのなかでresolve関数またはreject関数を実行してPromiseオブジェクトの状態を変更させる。これが次段のコールバック関数の実行トリガになる。
  • executorはすぐに(コンストラクタがPromiseオブジェクトを返すよりも前に)実行される。これに対しthenメソッドへ渡すコールバック関数はすぐには実行されない。
  • thenメソッドはコールバック関数を登録してすぐに返る。この戻り値は新しいPromiseオブジェクトになるため、戻り値からthenメソッドを使うことでメソッドチェーンを作成することができる。
    例:promiseA.then(callbackA).then(callbackB).then(callbackC)
  • Promiseインターフェイスは作成時点では分からなくてもよい値へのプロキシ(代理)で、Promiseオブジェクトをthenメソッドによりハンドラ(コールバック関数)に関連付けることができる。

    Promiseオブジェクトをconsole.logで表示

    • PromiseオブジェクトはPromiseStatusプロパティとPromiseValueプロパティを持つ
    • コールバック関数がresolve()を実行すると対応するPromiseオブジェクトのPromiseStatusプロパティが”resolved”になる
    • これらのプロパティはプログラムから参照できないので実際は気にしなくていい
  • thenメソッドで登録したコールバック関数が値を返した場合は、暗黙的にPromise.resolve(値)によりPromiseでラップされる(値を返さない場合はundefinedをラップ)。続いて次段のコールバック関数が呼び出される。このとき引数に値が渡される。
  • thenメソッドで登録したコールバック関数がPromiseオブジェクトを返した場合は、そのPromiseオブジェクトがFulfilledまたはRejectedになるまで次段のコールバック関数は呼び出されない。この仕組みを利用して非同期処理の完了を待って次のコールバック関数を呼び出すことができる。
    (個人的なイメージ:コールバックから返されたPromiseオブジェクトがexecutorのresolve/reject関数によりFulfilled/Rejectedに状態を変えると、その状態がthenメソッドで生成したPromiseオブジェクトに伝搬し、関連付けられたコールバック関数が呼び出される)

Chart.js パン操作サンプル

Chart.jsでグラフをパン(PANまたはドラッグ、スクロール)操作するサンプルを作成しました。

Chart.jsはプラグインを読み込むことで機能拡張することができます。この拡張性・柔軟性の高さがChart.jsの人気の一因かと思います(メンテする方は大変そうですが)。プラグインは公式サイトでも公開されており、信頼性についても心配なさそうです。

Popular Extensions – chartjs.org

今回グラフをパン操作するにあたり、公式サイトで公開されているchartjs-plugin-zoomプラグインを使用しました。サンプルに加えこのプラグインの使い方をさらっと説明します。どなたかの参考になればと思います。

See the Pen Chart.js(pan) by senmyou (@senmyou) on CodePen.

プラグインの読み込み

chartjs-plugin-zoomプラグインではhammer.jsを利用しています。このためchartjs-plugin-zoomプラグインを読み込む前にhammer.jsを読み込む必要があります(読み込む順番が逆の場合エラーは出ませんがジェスチャーイベントがリスナーに登録されません)。

<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.min.js"></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-zoom/0.6.3/chartjs-plugin-zoom.js'></script>
if (Hammer) {
  var mc = new Hammer.Manager(node);
  mc.add(new Hammer.Pinch());
  mc.add(new Hammer.Pan({
    threshold: panThreshold
  }));
  .....
  mc.on('panstart', function(e) {
    currentDeltaX = 0;
    currentDeltaY = 0;
    handlePan(e);
  });
  mc.on('panmove', handlePan);
  mc.on('panend', function(e) {
    currentDeltaX = null;
    currentDeltaY = null;
    zoomNS.panCumulativeDelta = 0;
    setTimeout(function() { panning = false; }, 500);
  });

オプションの設定

  • options.pan.enabledとoptions.zoom.enabledをtrueに設定します。なにやらPANのみ行う場合もzoom.enabledをtrueにする必要があるみたいです(modeは空文字)。
    Pan doesn’t work without `zoom.enabled` #132

  • options.pan.rangeMin/rangeMaxにパン操作の有効範囲を設定します。パンやズーム操作時にrangeMin/rangeMaxに達すると、そこで操作を止めてくれます。設定するかどうかは任意で、設定しなくても問題なく動きます。

  • options.xAxes.min/maxにCanvas要素内に表示する範囲を設定します。これを設定しないと全データをCanvas要素いっぱいに拡大縮小して表示します。

options: {
  pan: {
    enabled: true,
    mode: "x",
    rangeMin: {
      x: rangeMin
    },
    rangeMax: {
      x: rangeMax
    },
  },
  zoom: {
    enabled: true,
    mode: ""
  },
  xAxes: [{
    type: 'time',
    time: {
      min: min,
      max: max
    },

最後に現在作成中のアプリで使用する予定のグラフを掲載します。データの作成部分はデバッグ中なのでオプションの設定だけ参考にして頂けたらと思います。Chart.jsはグラフのスタイリングを細かく設定できますしプラグインもたくさんあるのでデザインを決めるのに迷ってしまいますね。

See the Pen Chart.js(pan) by senmyou (@senmyou) on CodePen.

JavaScript よく使う配列メソッド

個人的によく使うJavaScriptの配列メソッドをまとめました。

ループ処理

forEach / map / filter

全要素について処理を実施(中断しない)

forEach(コールバック関数)
for文の代用
新しい配列 = map(コールバック関数)
コールバック関数が返した値を新しい配列に格納
新しい配列 = filter(コールバック関数)
trueを返す要素のみを新しい配列に格納
const items = ["A", "B", "C", "D", "E"];
items.forEach((value, index) => {
    console.log(value, index);
});
// A 0
// B 1
// C 2
// D 3
// E 4

const items1 = ["A", "B", "C", "D", "E"];
const label1 = items.map((value, index) => {
    return index + ": " + value;
});
console.log(label1); // ["0: A", "1: B", "2: C", "3: D", "4: E"]
console.log(items1); // ["A", "B", "C", "D", "E"]

const items2 = [{id: 1, name: "A"},
                {id: 2, name: "B"},
                {id: 3, name: "C"},
                {id: 4, name: "D"},
                {id: 5, name: "E"}];
const label2 = items2.map((item, index) => {
    return item.name;
});
console.log(label2); // ["A", "B", "C", "D", "E"]

const items3 = [{id: 1, name: "A"},
                {id: 2, name: "B"},
                {id: 3, name: "C"},
                {id: 4, name: "D"},
                {id: 5, name: "E"}];
const label3 = items3.filter((item, index) => {
    return item.id > 3;
});
console.log(label3); // [{id: 4, name: "D"}, {id: 5, name: "E"}]

要素の追加

push / unshift / splice

push(要素)
配列の一番後ろに追加(複数可)
unshift(要素)
配列の一番前に追加(複数可)
削除された要素 = splice(index, 削除する数, 追加する要素)
削除する数を0にすると追加のみ実施、元の配列が書き換わる
const items1 = ["A", "B", "C", "D", "E"];
items1.push("*");
console.log(items1); // ["A", "B", "C", "D", "E", "*"]

const items2 = ["A", "B", "C", "D", "E"];
items2.unshift("*");
console.log(items2); // ["*", "A", "B", "C", "D", "E"]

const items3 = ["A", "B", "C", "D", "E"];
items3.splice(1, 0, "*");
console.log(items3); // ["A", "*", "B", "C", "D", "E"]

要素の削除(取り出し)

pop / shift / splice

一番後ろの要素 = pop()
配列の一番後ろを取り出す、空の場合はundefined
一番前の要素 = shift()
配列の一番前を取り出す、空の場合はundefined
削除された要素 = splice(index, 削除する数, 追加する要素)
追加する要素を省略すると削除のみ、削除する数も省略するとindex以降をすべて削除、元の配列が書き換わる
const items1 = ["A", "B", "C", "D", "E"];
items1.pop();
console.log(items1); // ["A", "B", "C", "D"]

const items2 = ["A", "B", "C", "D", "E"];
items2.shift();
console.log(items2); // ["B", "C", "D", "E"]

const items3 = ["A", "B", "C", "D", "E"];
items3.splice(1, 2);
console.log(items3); // ["A", "D", "E"]

const items4 = ["A", "B", "C", "D", "E"];
items4.splice(2); // items4[2]以降を削除
console.log(items4); // ["A", "B"]

切り取り

slice

切り取った要素 = slice(開始index, 終了index(含まない))
終了indexを省略すると最後まで切り取り、開始indexも省略すると全要素切り取り(配列のコピー)、元の配列を書き換えない
const items1 = ["A", "B", "C", "D", "E"];
const parts1 = items1.slice(1, 2);
console.log(parts1); // ["B"]
console.log(items1); // ["A", "B", "C", "D", "E"]

const items2 = ["A", "B", "C", "D", "E"];
const parts2 = items2.slice(1); // items2[1]以降を切り取り
console.log(parts2); // ["B", "C", "D", "E"]
console.log(items2); // ["A", "B", "C", "D", "E"]

Memo

直訳

  • splice … 解いて組み継ぎする、つなぎ合わせる
  • slice … 切る、切り取る
見つけた要素のindex = indexOf(探す要素, 検索開始index)
検索開始indexは省略可、見つからない場合は-1を返す
見つけた要素のindex = lastIndexOf(探す要素, 検索開始index)
検索開始indexは省略可、見つからない場合は-1を返す
見つけた要素のindex = findIndex(コールバック関数)
コールバック関数がtrueを返すまで検索、見つからない場合は-1を返す
見つけた要素 = find(コールバック関数)
コールバック関数がtrueを返すまで検索、見つからない場合はundefinedを返す
const items = ["A", "B", "C", "B", "A"];

const index1 = items.indexOf("B"); // indexOf(探す値, 開始index)
console.log(index1);     // 1

const index2 = items.lastIndexOf("B"); // lastIndexOf(探す値, 開始index)
console.log(index2);     // 3

const index3 = items.findIndex(value => value === "B"); // findIndex(callback)
console.log(index3);     // 1

const found = items.find(value => value === "B"); // find(callback)
console.log(found);     // B

テスト判定

some / every

true or false = some(コールバック関数)
コールバック関数で1要素でもtrueを返す場合はtrue
true or false = every(コールバック関数)
コールバック関数で全要素がtrueを返す場合はtrue(1要素でもfalseを返す場合はfalse)
const items = [0, 0, 1, 0, 1];

const some1 = items.some(value => value === 1); // 0->0->1->break
console.log(some1);     // true

const every0 = items.every(value => value === 0); // 0->0->1->break
console.log(every0);    // false

ソート

sort

sort(比較関数)
比較関数の戻り値の正負に従い要素を並べ替える
const items1 = [2, 0, 1, 4, 3];
items1.sort((a, b) =>  a - b);
console.log(items1);     // [0, 1, 2, 3, 4]

const items2 = [2, 0, 1, 4, 3];
items2.sort((a, b) =>  b - a);
console.log(items2);     // [4, 3, 2, 1, 0]

Memo

Array/Stringオブジェクトの同名メソッド

  • indexOf
  • lastIndexOf
  • slice