AngularJSで$timeoutを使わずに、ng-repeatが描画完了してから処理したい場合

AngularJSでng-repeatの描画後に処理したいとき、
多くの場合は、$timeoutを使って、描画後にevent通知をします。

この方法はすでに多くの方が、
やり方を紹介しています。

qiita.com note.onichannn.net

でも、まれではありますが
この方法を使わずに、描画後に処理を入れたい時があります。

具体的には、

  1. $timeoutを使いたくない。
  2. ng-repeatのタグに、directiveを置きたくない。

という場合です。

この記事では、
こんな場合の対処方法を紹介します。

なぜ紹介の方法以外を使いたいのか?

$timeoutを使えば描画後に通知を出す処理を簡単に作れます。
でも、この方法には以下デメリットがあります。

  1. ng-repeatの全タグに、directiveが生成され、directive生成コストがかかる。
  2. $timeoutを使うことで、描画直後ではなく、少しタイムラグが発生する。

directive生成コスト

ng-repeat記載のタグに、directiveを記載すると、 繰り返しタグの全タグに、そのタグが記載され、 インスタンス化されます。

そのため、ng-repeatの要素数が100以上の場合などには、
スペックによっては、性能劣化を引き起こします。

ng-repeatの最後の要素の描画のタイミングを知りたいのに、
繰り返しの数だけdirectiveが生成されるのは、
不要なインスタンス化にあたるので、避けたいところです。

少しタイムラグってどれくらい?

$timeoutの呼び出し時に、
他にどれだけの処理をしているか?に依存して、タイムラグが発生します。

$timeout処理には、何秒後に実行するかを引数に指定できるので、
0を指定すれば、すぐ処理してくれると思いますよね。
でも、そうなりません。。

$timeout以外の処理を一通り実施した直後に、
$timeoutの処理が実行されます。

なので、例えばng-repeatの下に他のタグがある場合は、
他のタグがある程度描画されてからでないと$timeoutの処理は実行されません。

このタイムラグが無視できれば、$timeoutを用いて大丈夫ですし、
だめなら、これから紹介する方法を用いて実現します。

では以下に具体的な実現方法を記載します。

DOMが配置され、値がbindされたタイミングを狙う。

ここで念のため、描画直後について整理します。

今回実施したい、描画直後とは、 DOMがブラウザ上に配置されて、
scopeの値がbindされたタイミングと言っていいと思います。

値がbindされないと、ng-styleやng-ifなど機能しないため、
このタイミングですね。

ではこのタイミングで通知を入れるのですが、
そのために、まず単純に通知するだけの、directiveを作ります。

具体的なコードは以下の通りです。

(function () {
  'use strict';

  angular
    .module('ngFinishDisplay')
    .directive('ngFinishDisplay', ngFinishDisplay);

  /** @ngInject */
  function ngFinishDisplay() {
    var directive = {
      restrict: 'E',
      scope: {
        notifyId: '<'
      },
      /**
       * インスタンス化されたタイミングで通知する。
       */
      link: function (scope) {
        scope.$emit(scope.notifyId);
      }
    };
    return directive;
  }
})();

ここからがポイントです。

作ったDirectiveを
検知したい描画対象のタグの閉じタグの直上に入れます。

たとえばng-repeatタグが以下のようにあります。

<div class="container">
  <div class="col-xs-2 row" ng-repeat="item in main.itemList track by $index">
    <div>
      <div class="col-xs-3">ID</div>
      <div class="col-xs-9">{{item.id}}</div>
    </div>
    <div>
      <div class="col-xs-3">Name</div>
      <div class="col-xs-9">{{item.name}}</div>
    </div>
  </div>
</div>

この場合、以下のようにng-repeatの閉じタグの直上に配置します。

<div class="container">
  <div class="col-xs-2 row" ng-repeat="item in main.itemList track by $index">
    <div>
      <div class="col-xs-3">ID</div>
      <div class="col-xs-9">{{item.id}}</div>
    </div>
    <div>
      <div class="col-xs-3">Name</div>
      <div class="col-xs-9">{{item.name}}</div>
    </div>
    <ng-finish-display notify-id="main.eventName" ng-if="$last"></ng-finish-display>
  </div>
</div>

また、ng-repeatの描画完了後に通知がほしいので、
ng-if=“$last”
を使ってng-repeatの最後の要素の描画時に通知するようにしました。

動作確認

動作確認のために、
以下のControllerでitemListに24個の要素を入れた場合の
通知時の描画を見てみます。

(function() {
  'use strict';

  angular
    .module('ngFinishDisplay')
    .controller('MainController', MainController);

  /** @ngInject */
  function MainController($scope, $log) {
    var vm = this;
    var itemList = [];
    for(var index = 1; index <= 24; index++) {
      var object = {
        id: 'book-' + index,
        name: 'book name ' + index
      };
      itemList.push(object);
    }
    vm.itemList = itemList;
    vm.eventName = 'finish-draw-event';

    $scope.$on(vm.eventName, function(){
      // ここをDevToolで止めて、描画状況を確認
      $log.info('通知');
    });
  }
})();

以下はデバッガで通知直後に止め、
ブラウザでの表示内容をキャプチャしたものです。
全ての値が画面に表示されていますので、
描画直後に通知が来ています。
f:id:s-nakagawa:20170708170630p:plain

これだけだと、本当に直後なのかがわからないので、
以下のように、場所に変えて配置して実験しました。

<div class="container">
  <div class="col-xs-2 row" ng-repeat="item in main.itemList track by $index">
    <div>
      <div class="col-xs-3">ID</div>
      <div class="col-xs-9">{{item.id}}</div>
    </div>
    <ng-finish-display notify-id="main.eventName" ng-if="$last"></ng-finish-display>
    <div>
      <div class="col-xs-3">Name</div>
      <div class="col-xs-9">{{item.name}}</div>
    </div>
  </div>
</div>

すると以下のように、
最後の本の名前が{{item.name}}のままであることが分かります。
f:id:s-nakagawa:20170708170712p:plain

このように、$timeoutを使わずに、
ng-repeatの描画直後を検知できていることが分かります。

ちなみに、$lastを$firstにしてみましょう。

<div class="container">
  <div class="col-xs-2 row" ng-repeat="item in main.itemList track by $index">
    <ng-finish-display notify-id="main.eventName" ng-if="$first"></ng-finish-display>
    <div>
      <div class="col-xs-3">ID</div>
      <div class="col-xs-9">{{item.id}}</div>
    </div>
    <div>
      <div class="col-xs-3">Name</div>
      <div class="col-xs-9">{{item.name}}</div>
    </div>
  </div>
</div>

以下のように描画されます。
想像通りでしたでしょうか?

f:id:s-nakagawa:20170708171131p:plain

bind式が一つも評価されていないタイミングで通知を受け取れていますね。

このように、directiveを利用することで、
$timeoutを使わずに描画直後に通知を飛ばすことができます。

最適化

$timeoutを使わないという目的を達することはできましたが、

処理コストがかかるという点に着目すると、
もう一点最適化が必要です。

現状のコードでは、ng-ifにおいて双方向バインディングを使っています。
すると、何か他の値の変更をするごとに、
ng-ifのチェック処理が実行されます。

しかもこのチェックは、ng-repeatの繰り返し数分処理するため、
素数によっては性能への影響が生じます。

ですので、以下のようにOne-time Bindingを使って最適化します。

<div class="container">
  <div class="col-xs-2 row" ng-repeat="item in main.itemList track by $index">
    <div>
      <div class="col-xs-3">ID</div>
      <div class="col-xs-9">{{item.id}}</div>
    </div>
    <div>
      <div class="col-xs-3">Name</div>
      <div class="col-xs-9">{{item.name}}</div>
    </div>
    <ng-finish-display notify-id="::main.eventName" ng-if="::$last"></ng-finish-display>
  </div>
</div>

こうすることで、
ng-repeatの要素数に比例した不要なインスタンスや、Watch式を生成せずに、
描画後のイベントを検知することができました。

※ソース一式は、以下にありますので、
 ご参考まで github.com

他言語を長年扱ってきたエンジニアがPythonに入門するときにどう勉強するか。

今までJavaJavascript系の言語で ずっと仕事をしてきたが、 Pythonの言語習得も必要かなと感じたので、 Pythonを勉強がてら、どう勉強するのがよいのか考えたので、 忘備録として残します。

www.python.org

今回の前提は以下の通り

  1. 他の言語は複数習得済
  2. Pythonはど素人

ざっと勉強法を調べると、

  1. pythonJapanで1から勉強
  2. ドットインストーPython入門を利用
  3. Python-izmサイトを利用

など見つかる。

ただ、すでにほかの言語は習得していて、 もっと別の やる気を刺激する勉強方法はないかと思い 調査した。

1. 自分の性格から見る、よいやり方

当たり前のことではありますが、 ある程度普遍的な勉強方法の共通性はあるものの、 自分の性格に合うやり方に沿って勉強するのが、 最も効率がいい。

自分の勉強に関して寄与する性格は、 ざっと以下のようなところ。

1. 習うよりも慣れろのほうが好き。

2. 必要に迫られるような状況のほうがやる気が出る。

3. 達成感が感じられる方がよい。

なので、以下のように考えた。

1. 習うよりも慣れろのほうが好き。

文法の勉強などすっ飛ばして、 最初から、CodeAcademyのような課題を出してくれるような 勉強サイトを利用する。

課題を解く上で必要になったら 検索を使って、文法など調べる。

また、ある程度慣れたら、 おすすめ書籍を購入して勉強する。

2. 必要に迫られるような状況のほうがやる気が出る。

最初のゴールは、仕事に絡める。

仕事で使うツールを Pythonを用いて開発することをゴールとする。

3. 達成感が感じられる方がよい。

CodeAcademyはPython対応なので、 そちらを利用させてもらう形でよいと思うが、 どうせなら違う方法を試したい。

最近だと、Gameをクリアすることで、 プログラミングスキルを向上できるサイトが あるとのことなので、それをやってみる。

2. Gameを通して、勉強できるサイトはあるが、どれにするか?

検索すると十数種類のサイトが見つかる。

medium.mybridge.co

この中で、グラフィックがきれいで、Pythonが使えるサイト、以下だった。 checkio.org codecombat.com www.codingame.com

それぞれのサイトについて、調べたところ 他の人の回答を見ることができるという機能 がCheckIOにあるということだった。

習うより慣れろで大事なことは、 自分が手を動かすことも大事だが、 それと同時に、よい回答を見ることも大事と思う。

なので、 今回の勉強には、CheckIOを使わせてもらうこととする。

3. 実際にCheckIOをやってみて

CheckIOは各島ごとに課題が設定されており、 その課題をプログラミングで解くことで、レベルが上がり、 次の島に行くことができるようになる。

最初の島は無料でできるということなので、 まずはやってみた。

やってみてのよい点はまとめると

  1. 問題は日本語の用意もあり、英語読めなくても安心
  2. 他の人の回答には、いろんなテーマに沿ったコード回答があり、勉強になる。
  3. 問題レベルが適切だった。

回答には、創造的(どちらかというとどれだけ短いコードで書けるか?)、 スピード、サードパティ利用などあり、 それぞれの回答が見れて勉強になります。

また最初の島の最終問題が、 迷路の経路探索問題だったので、 全くの初心者向けよりは、難度が高く、 私の目的にはちょうどいいレベルだったと思います。

習うより慣れろ派のとっかかりとしては、 楽しくできたのでよかったです。

4. まとめ

習うより慣れろの私にとって、 最初に取り掛かるのがCheckIOによるプログラミングは、 難度がちょうどよく、楽しめたのでよかったです。

次の島には約2$必要なので、 次の島ぐらいまで遊んだら、 ツール作成に移行しようかなと思います。

今日はここまで。