ふしぎの国のバード を読んだ

読んだ。

明治初期にイギリス人の探検家 イザベラ・バード が日本を探検した際の実話を基に書かれた漫画。当時の日本の風景や庶民の生活が描かれていて面白い。明治初期は、300年続いた鎖国が終わり、日本がグローバリゼーションの影響を著しく受けた時期と言える。その時期に失われた何かを知るきっかけとなる作品ではないだろうか。

2016年のふりかえり

今年も残すところあと少し。今年のふりかえりです。

去年のふりかえりはこれです。 2015年のふりかえり - kitak.blog

仕事

去年に続き、ウェブフロントエンド中心に JavaScript のコーディングなど。サービスや大きめの機能をいくつかリリースできたのと、その中で大量のオブジェクトやトラフィックを扱う案件をこなすことができて、ひとつ自信に繋がったような気がします。自己否定がちょい強めなので、ひとつひとつ形にしていって肯定感を高めていきたいところ。

前半、Java や Spring Boot ( Spring MVC ) の勉強をしていて、正直仕事や趣味でそれらをがっつり書くことはなかったんですが、上半期の仕事でデバッグをするときに抵抗なくサーバーサイドのコードを読んで、問題特定に役立てることができたので、やっておいてよかったかな、と。

去年も同じことを書いていた気がするんですが、やることを限定しすぎているような気がするので、そこは来年の課題。

アウトプット

発表

Vue.js meetup で発表しました。 Vue.js Tokyo v-meetup="#1"でLTしてきた、と発表からカットしたMVVMのあれこれ - kitak.blog
これ一回だけだったので、うーむ...というかんじです。スライドにまとめるのは自分自身の理解のためにも良いので、居酒屋で酒を呑みながらカジュアルに発表するようなのを定期的にやりたいね、と同じことを考えている人と話していたり。

執筆

gihyo.jp で Vue.js の入門記事を執筆しました。執筆の機会をいただきありがとうございました。

gihyo.jp

ブログ

特に明確な目標があったわけではないんですが、週に1回はブログを更新するように意識していました。気分が乗らないときは、とりあえず続けばいいやぐらいの気持ちで漫画やドキュメンタリーの感想を書いたり。
書いた記事のうち、何個かホットエントリ入りして、たくさんの人に読んでもらえたのがうれしかったです。

生活

風邪をひくことが多い一年でした。下半期は、一ヶ月半に一回くらいのペースで風邪をひいていたように思います。運動不足で、体重が増えているのもありますし、来年はもっと健康に気を使いたい。 外は寒いですが、とりあえずウォーキングから始めて、引っ越したら近くのジムに通おうかな、と思っていたり。

上でも書きましたが、来年、会社のオフィスが新宿に移転するので、それに合わせて引っ越す予定です。今のところ、祐天寺、中目黒、新宿御苑、代々木あたりを考えています。

去年に引き続き、浮いた話は全く無いので、そこは来年も引き続きがんばりましょう。

2016年に使った&来年使いそうな JavaScript 関連のものと所感

2016年に使った&来年使いそうな JavaScript 関連のもの

以下のカテゴリでなんとなく書いてみます。去年と比べて大きく変わらないかんじです。動きの早かった 2, 3年前から、年々、動きはゆっくりになってきているというのが個人的な印象。

トランスパイラ

2016年に使った:

Babel。各ブラウザの JavaScript エンジンで ES2015~ES2017 の実装が進んでいるので、関わっている案件のブラウザのシェア次第なところもありますが、ES2015~ES2017 の仕様だけトランスパイルしたいのであれば、再来年あたりから要らなくなるんじゃないかな、という気持ち。

来年使いそう:

静的型付けがメインで、ES20XX から ES5 へのトランパイルは副次的ですが、TypeScript。先日、久しぶりに触ったら( v2.1 )、v1.6 か v1.8 の頃に触って微妙だな、と思った諸々が改善されていました。

  • サードパーティの型定義ファイルの取得がそれ用のツールではなく npm でできるようになった
  • 別ファイルに定義された型情報を参照するために reference タグを書く必要がなくなった
  • async/await を es5 target でダウンパイルできるようになった ( 以前は、いったん es2015 に変換した後に babel でさらにトランスパイルをしないとダメで、ビルドチェーンが複雑になるからスルーしていた )
  • targetに esnext, es2015, es2017 が指定できるので、型宣言だけ抜いた JavaScript ファイルが生成できる。やめたくなったらすぐにやめれる
  • Visual Studio Codeとの連携が良くできている ( 最近 Visual Studio Code 使っているので )

ビューライブラリ/フレームワーク

2016年に使った:

Vue。既存のサービスでのちょっとした UI の実装から、SPA のフルスクラッチの実装まで幅広く使いました。良さは色々あるんですが、以下の3点が大きいです。

  • 上で書いたようにライブラリとしてのスケーラビリティが高い
  • API がシンプルで学習コストが低い
  • API や 新しいバージョンへの移行のドキュメントがしっかり整備されている

周囲だと React , Redux を使っている人もちらほら。React、良いと思うんですが、今の会社のワークフローだと HTML を JSX に手で書き換える作業が発生するので、個人的にそれがつらいので選択していません。( React に限らず、Vue も含めたコンポーネント指向のライブラリを使うとどうしても二度手間感が否めないワークフローなので、来年はそこを改善したい、と思っていたり )
今年の9月末にリリースされた Vue v2.0 で パフォーマンス改善 & Server Side Rendering もできるようになったので、特別 React を選ぶ理由がなくなったというのもあります。

来年使いそう:

今年と同じ感じ。新しいライブラリを実戦で使うことは今のところなさそうですが、Shadow DOM v1 や Custom Element v1 の実装が ChromeSafari で進んでいる背景も踏まえつつ、https://github.com/skatejs/skatejs のような WebComponent のAPIを薄くラップしたライブラリはウォッチしておこうと思っています。

モジュールバンドラ

2016年に使った:

Browserify。それなりの規模のプロジェクトだと生成されるファイルサイズが大きくなる以外は不満はなく。ビルドにかかる時間も watchify を使えばだいぶ短くできます。ただ、開発が活発ではなくなっているのと、モジュールバンドラとしては Webpack を使う人が増えて、Browserify を使う人が減ってきているような気がするのが気がかり( ソース: The State of Front-End Tooling 2016 - Results - AshleyNolan.co.uk - Blog and Portfolio for Ashley Nolan )。

来年使いそう:

Browserify でもまだ困らないとは思うんですが、Webpack2 を検討しています。理由はビルドにかかる時間が短い、コード分割(と動的ローディング)、Tree Shaking で生成されるファイルのサイズを減らせるというあたりです。ただ、先日のブログ( JavaScript のサイズを減らすことと効率良くキャッシュさせるためのメモ - kitak.blog )の最後に書いたようにツールの自由度が高く plugin と loader の使い過ぎが後々の負債に繋がりそうな予感がするので、いったんは JavaScript のモジュールバンドラとして使い、stylesheet や画像は扱わないつもりです。

スクランナー

2016年に使った:

npm-scripts(yarn run も含む)。npm run ほにゃらら で browserify のコマンドを実行したり、コマンドをパイプで繋いだり、複雑になってきたら JavaScript で書いたスクリプトを node で実行したり、シェルスクリプトを実行しています。

数年前からあるプロジェクトや一部のタスクは Grunt を使ってます。Grunt、今でもメンテはされているので、使っている Grunt プラグインが全くメンテされなくて業務に支障が出るとかがないのであれば、既存のプロジェクトで急いで違うツールに乗り換える必要はなさそうです( 時間があれば乗り換えてもよさそうですが )。

来年使いそう:

多分、これまでと変わらず。

パッケージマネージャ

2016年に使った:

Yarn。リリース日に触ったらパッケージを git リポジトリの url で指定すると動かなくて、しばらく様子見していたんですが、一ヶ月ぐらい前に触ったら修正されていて、それ以来使っています。体感ですが、パッケージのインストールが非常に速い。その他、npm の生成する package.json と互換性があったり、バージョン固定の仕組み( npm shrinkwrap はバグやハマりどころが多かった )など、諸々鑑みて、使わない理由がない。

来年使いそう:

多分、今年と変わらず。

スティング

2016年に使った:

あんまりこだわりがないので昔から使っている Karma(テストランナー) + Jasmine(テスティングフレームワーク) + PhantomJS。

来年使いそう:

来年使うものというより、よっぽど強い理由がないのであれば、使っているビューライブラリやフレームワークが、自身のテストに使っていたり、ドキュメントで推奨しているものを使うのが無難だと考えています。

所感

最近、心の中でふつふつと思っていたことです。

動き早い?

冒頭にも書きましたが、年々、落ち着いてきているような気がします。今年に関して言うと、去年から耳にしていた・ある程度使われていたものが今年になって、改善・改良されて、さらに使われるようになったというところではないでしょうか。それは、The State of Front-End Tooling 2016 - Results - AshleyNolan.co.uk - Blog and Portfolio for Ashley Nolan の結果にも現れています。

とはいえ、上で書いたカテゴリだけでも 6つあって(他にも色々 Lint とかあるんですが ESLint が当たり前になっているので省きました)、JavaScript を普段書かない人からすると、学ぶべきことがたくさんあるように感じる、適当にググったら、過去に広く使われていたものや「それぞれのツールを組み合わせてみた」みたいな記事が大量に出てきて、あたかも動きが早いように感じるのではないでしょうか。

実際のところ、全ての案件で上のカテゴリのツールやライブラリ全てが必要なわけではありません。極端な例をあげれば、公開期間が数週間のキャンペーンページだと、(マサカリ飛んできそうですが) jQuery 互換のライブラリを npm で入れて、ES5の仕様でシュッとコード書いて、ファイルを concat (これもいらなくなりつつある), uglify しておしまいということもあります。自分が必要なものを理解して、選んで使えばいいだけです。なので、コンテキストをすっ飛ばして「React, Redux, Webpack, ... このスタックでやっとけば間違いない!」 みたいな記事を見ると、眉をひそめたくなります。

個人的に、JavaScript やその周辺のことを学び始めた人が自分の作っているものや関わっている案件に対して、必要なものを適切に選ぶことができるガイドやYes/Noチャートのようなものが世の中に足りないんじゃないかな、と感じています。

あとは、2016年にJavaScriptを学ぶとこんな感じ – Medium Japan – Medium に書かれているような普段 JavaScript を書いている人がそうじゃない人に「イマドキはこれだから!」とその人がやりたいことに対して過剰なものを押し付けたり、不安を掻き立てるようなことをしないということですね...(自戒もこめて)

新しいライブラリ・ツールとの向き合い方

落ち着いてきているように感じるとはいえ、新しいライブラリやツールは次々と出てきます。そういったライブラリやツールとはどう向き合えばいいのか。個人的には次のような感じでやっています。

  • README を読んで、それが何を解決するかやどのカテゴリに属するかを考える
  • 良さそうであれば sandbox プロジェクトで試す
  • そのプロジェクトが作られて2, 3年未満の場合は2, 3年経つまで放置
  • 2, 3年経っていたら、それを使うことで生産性が数倍向上しそうか(ちょっと向上するぐらいだったらやめる)、その間にあったリリース内容やコミュニティの発展、直近のWeb標準やプロコトルの進化や発展で不要にならないか等を鑑みて実戦に投入するか判断

2, 3年というのは、けっこう適当なんですが、新しく出てくるツールは大抵 2, 3 年の内に消えているように思うので、2, 3 年生き延びたのであれば、今後も存続し、実戦で使うに耐えうるクオリティになっているだろうという考えです(yarn, 今年出てきたやつやんとツッコミ受けそうなんですが、package.json そのままで npm を置き換えればいいだけなので例外)。一時期、新しいものを使うのが正しいと思っていた時期もあったんですが、kintoneフロントエンド開発 モダン化への道 のスライドやロードマップ指向とエコシステム指向 - アンカテ を読んで、いわゆる中心部のレッドオーシャンを避けるようになりました。白洲次郎が、ツイードは2, 3年雨ざらしにして着ろと言っているように、JavaScript 関連のツール・ライブラリも2, 3年様子見するぐらいがちょうどいいんじゃないでしょうか(ツイード着たことないですが…)。

おしまい。

WebPagetest を使わずに Speed Index を算出する

題の通り。

WebPagetest を使うと、Speed Index というページのコンテンツがいかに速くレンダリングされたかの総合的なスコアが算出できます。Speed Index は、定期的にこのブログで取り上げていますが、算出方法の内容は https://github.com/t32k/webpagetest-doc-ja/blob/master/using-webpagetest/metrics/speed-index/index.md に詳しく書かれています。( Speed Index とは異なりますが、First Paint のタイミングを計測するための Web 標準は GitHub - WICG/paint-timing: A proposal for a Time To First Paint specification. で議論されているようです )

個人的な興味で、とあるウェブアプリの2つの実装方法のうち、どちらが速くレンダリングされるか調べたくなり、Speed Index が指標として適切だと判断しました。Speed Index を算出するために、WebPagetest のページを何回もポチポチやったり、API を呼んだりしてもよかったんですが、なんとなく WebPagetest のリモートのマシンではなく、ローカルのマシンのブラウザでページが表示されるのを見つつ、算出したかったという次第です。ようするに気持ちの問題です。

たまたま、https://github.com/GoogleChrome/lighthouse という Google が提供している Progressive Web App にアプリが即しているかチェックするツールで Speed Index を算出していることに気づいたので、コードを読んだり、grep して https://github.com/pmdartus/speedline を使って Speed Index を算出していることが分かりました。speedline の README に書かれている通りですが、Chrome の Timeline のレコードが記録された JSON を渡せば算出できそうです。なぜ、Timeline のレコードを使うのかは、Speed Index の算出方法を解説した記事の「描画イベントによるVisual Progress」を参照してください。

Timeline のトレースは、https://github.com/cyrus-and/chrome-remote-interface を利用して、Remote Debugging Protocol を喋ることで可能です。chrome-remote-interface は、2年前にこのブログでも紹介していました( chrome-remote-interface を試してみた - kitak.blog )。これ、2年前か…。 chrome-remote-interface を使った Timeline のトレースは、GitHub - paulirish/automated-chrome-profiling: Node.js recipe for automating javascript profiling in Chrome のコードを参考にしました。他にも CPU のプロファイルを取ったりといった各種パフォーマンスメトリクスの収集のサンプルコードがあるので、頭の片隅に置いておくとよさそうです。

Speed Index を算出する手順

材料が揃ったので、手順を示します。

適当に npm でプロジェクトを作成して(npm init)、chrome-remote-interface と speedline をインストールします(npm install chrome-remote-interface --save, npm install -g speedline)。グローバルにインストールしたくない場合は、--save でインストールして、npm-scripts から呼び出すようにしてください。

Debbuging port を開いて Chrome を起動します。Macだと、/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=$TMPDIR/chrome-profiling --no-default-browser-check というコマンドです。

以下のスクリプトを用意し、node で実行します。

const fs = require('fs');
const Chrome = require('chrome-remote-interface');

const TRACE_CATEGORIES = ["-*", "devtools.timeline", "disabled-by-default-devtools.timeline", "disabled-by-default-devtools.timeline.frame", "toplevel", "blink.console", "disabled-by-default-devtools.timeline.stack", "disabled-by-default-devtools.screenshot", "disabled-by-default-v8.cpu_profile"];

const rawEvents = [];

Chrome(function (chrome) {
    with (chrome) {
        Page.enable();
        Tracing.start({
            "categories": TRACE_CATEGORIES.join(','),
            "options": "sampling-frequency=10000"  // 1000 is default and too slow.
        });
        Page.navigate({'url': '自分が Speed Index 値を算出したいサイトの URL に置き換える'})
        Page.loadEventFired(function () {
           Tracing.end()
        });

        Tracing.tracingComplete(function () {
            var file = 'profile-' + Date.now() + '.devtools.trace';
            fs.writeFileSync(file, JSON.stringify(rawEvents, null, 2));
            console.log('Trace file: ' + file);
            chrome.close();
        });

        Tracing.dataCollected(function(data){
            var events = data.value;
            rawEvents = rawEvents.concat(events);
        });
    }
}).on('error', function (e) {
    console.error('Cannot connect to Chrome', e);
});

トレースした内容が profile-1482725139003.devtools.trace のようなファイル名で保存されるので、speedline コマンドに食わせて Speed Index を算出させます。 (speedline ./profile-1482725139003.devtools.trace)

First Visual Change: 50 Visually Complete: 1586

Speed Index: 1177.4 Visual Progress: 0=0%, 50=77%, 161=17%, 330=17%, 1420=60%, 1507=72%, 1586=100%

Perceptual Speed Index: 440.0 Perceptual Visual Progress: 0=0%, 50=91%, 161=72%, 330=72%, 1420=73%, 1507=86%, 1586=100%

ヒストグラムを表示する場合は、speedline ./profile-1482725139003.devtools.trace --pretty

f:id:kitak:20161226191109p:plain

単純に算出された Speed Index だけ扱いたい場合は speedline の JavaScript API を以下のようなスクリプトで呼び出します。

const speedline = require('speedline');

speedline('./profile-1482725139003.devtools.trace').then((results) => {
  console.log('Speed Index value:', results.speedIndex);
});

意外と簡単にできました。何回か算出をおこないましたが、Timeline のトレースがうまくできないケースもあるようです(原因はちょっと不明)。今回はツールの使い方だけでしたが、算出方法を解説しながら、speedline のコードリーディングをしてもよさそうですね。

JavaScript のサイズを減らすことと効率良くキャッシュさせるためのメモ

題について、頭に浮かんだ考えや、手を動かして試したことのメモ。試行錯誤中なので、都度追記するか、そのときの考えを別途書く。

背景

  • モジュールバンドラに Browserify や Webpack を利用して一枚岩の bundle.js を生成している
  • 機能追加を繰り返したことによって、 bundle.js のサイズが増え、ロードにかかる時間が気になってきた

サイズを減らす心がけ

ファイルを分割する前に

  • ライブラリの選定時に、lodash のように関数ごとにパッケージが分割されており、必要なものだけ install, load できるかを選定の条件にする
  • Webpack2 や Rollup のような Tree Shaking に対応しているモジュールバンドラを利用している場合、Tree Shaking が効くかどうかをライブラリ選定の条件にする
  • ライブラリの機能のほんの一部しか使わない場合は、自身で利用する機能と同じ内容のコードを記述できないか検討する(ついでに元のライブラリに敬意を表しつつ、それを公開できないか検討する)
  • Browserify を使ったプロジェクトでファイルサイズを大きくしているライブラリを探す - kitak.blog に書かれているようなツールを使ってサイズが大きくなる原因のライブラリを特定し、置き換えを試みる

コード分割で効率良くキャッシュさせる

サイズを減らすように心がけてもサイズが大きくなってきたら

  • 前提: ライブラリのコードはアプリケーションのコードよりも変更頻度が少なく、サイズが大きい
  • ライブラリのコードとアプリケーションのコードを分離して、ライブラリのコードをブラウザに長くキャッシュさせるようにする
  • Multi Page, Single Pageを問わず、エントリ(URL)ごとに読み込むスクリプトが異なる場合は、ライブラリのコード(各エントリで共通のJS)のキャッシュが効けば、各エントリで必要なJSのみ取得すればよい
  • 以下のような Webpack2 の設定で、ライブラリのコードとアプリケーションのコードを分離できる (Webpack2 は、この記事を書いている時点でrcですが、あとからバージョンを上げる手間を鑑みて、今のうちから2を使ったほうがよい)
'use strict';

const webpack = require("webpack");
const AssetsPlugin = require('assets-webpack-plugin');

module.exports = {
  context: __dirname + "/src",
  entry: {
    home: "./home.js",
    events: "./events.js",
    contact: "./contact.js",
    vendor: [
      'vue',
      'moment'
      // ...
    ]
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: "vendor",
      minChunks: 2,
    }),
    new AssetsPlugin({
      filename: 'assets.json',
      processOutput: function (assets) {
        const result = {};
        for (let key of Object.keys(assets)) {
          result[key] = assets[key].js;
        }
        return JSON.stringify(result);
      }
    })
  ],
  output: {
    path: __dirname + "/dist",
    filename: "[name].[chunkhash].bundle.js",
  },
}
  • CommonsChunkPlugin を使うと各エントリで共通の JS をチャンクとして取り出してくれるが、各エントリで利用するライブラリが変わると共通のチャンクも変わるので、vendor エントリでライブラリを明示的に指定する
  • ( 共通コードを抜き出すだけであれば、Browserify でもできる https://github.com/substack/factor-bundle )
  • 毎日ライブラリのバージョンアップ & デプロイを起こっている場合はキャッシュが高々1日しか効かないことを理解しておく
  • Webpack2 で生成される JS ファイルに変更があった場合にブラウザのキャッシュではなく、新しく取得するようにファイル名に chunkhash を入れる
  • AssetsPlugin で生成されたファイル名をファイルに出力できる。html-webpack-plugin で JS をロードする HTML を生成してもよいが、いったん JSON で出力させておけば、サーバ・クライアント両方で扱うことができ、使い方の幅が広がる。出力される内容は processOutput オプションの関数で調整する
  • Single Page の場合、Webpack2 の require.ensure や System.import でオンデマンドにダイナミックローディングを行うのとは別に、Resource hints の preload でブラウザが暇になったタイミングで他エントリのスクリプトをロードさせる手法も有効ではないか
  • Multi Page の場合は、Resource hints の prefetch で事前に他のエントリのスクリプトをフェッチさせてもよいのではないか。先で生成した JSON を使って、以下のアプローチのいずれかで prefetch, preload を実現できる
    • アプリケーションサーバで生成する HTML に <link rel="prefetch" href="/assets/events.56d94462197c3b18193b.bundle.js" as="script"> のような link タグを加える
    • タグではなく、レスポンスヘッダーでも指定できる。アプリケーションサーバ、リバースプロキシのどちらでヘッダーを追加するかは適切に判断する
    • サーバではなく、クライアントでも以下のようなスクリプトで prefetch できる。先の JSON をあらかじめ取得して、他のエントリのスクリプトをフェッチするイメージ。Safari は prefetch ( http://caniuse.com/#feat=link-rel-prefetch ) に対応していないので、Image や XHR を使った polyfill を書く
const link = document.createElement("link");
link.rel = "prefetch";
link.as = "script";
link.href = "/assets/events.56d94462197c3b18193b.bundle.js";
document.head.appendChild(link);

おわり

  • リソースのキャッシュ戦略はサービス・アプリケーションの性質に依って変わるものなので、常に正しいやり方はないことを頭に入れておく
  • 効率の良い配信・取得をおこなおうとすると、(自動化できる部分もあるが)その分、管理や運用のコストが発生する。トレードオフ
  • (題と話が反れるが) Webpack は、JavaScript だけでなく css や画像もモジュールとして扱うことができ、他のモジュールバンドラーよりも自由度が高い。便利なツールだが、以下の懸念から、プロジェクトに導入するときに「何をどこまで Webpack でおこなうのか」協業者と決め、都度それを見直したほうが良いと思う。
    • stylesheet の中で require を呼んだり、plugin や loader を多用して、ビルドパイプラインが複雑化して、内容の把握や修正が困難になる( plugin や transform を多用して複雑化するのは Browserify でも同じ)
    • Dynamic Loading( require.ensure や System.import ) や 上の「stylesheet 内での require」のようなことをおこなうと、ソースコードが Webpack というツールに強く依存する形になる。Webpack 自体、比較的息の長いツールではあるが、長く機能追加やメンテナンスを行うプロジェクトでは、いつでも今使っているツールを捨てて、別のツールに乗り換えるぐらいの心構えでビルド環境を維持したほうがよいと思う。コードの機械的な置換やちょっとした書き換えでツールを捨てることができるのであれば問題ないと判断する ( Browserify を単純に使う場合は、ソースコードは common.js のモジュールの仕様に依存していて、Browserify というツールには依存していない )

Vue.js 2.0 の算出プロパティ周辺のコードリーディング

この記事は Vue.js Advent Calendar 2016 の21日目です。

https://skyronic.com/blog/vuejs-internals-computed-properties の記事に算出プロパティのミニマムな実装例が示されているのですが、実際に Vue のコードでどのように実装されているか調べました(省けるところは都度省いたのですが、けっこう長いです…)。対象の Vue のバージョンは v2.1.6 です。

そもそも算出プロパティとは

そもそも算出プロパティとはなんぞや、という話から。Vue には算出プロパティという機能があり、以下のように data の状態から派生した値をプロパティとして定義できます。

var vm = new Vue({
  el: '#app',
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})
<div id="app">
  <p>{{ fullName }}</p>
</div>

この算出プロパティには、依存している状態が変更されない限り、一度計算した値がキャッシュされるという特徴があります。以下、公式ドキュメントからの引用です。

算出プロパティの代わりに、同じような関数をメソッドとして定義することも可能です。最終的には、2つのアプローチは完全に同じ結果になります。しかしながら、算出プロパティは依存関係にもとづきキャッシュされるという違いがあります。算出プロパティは、それが依存するものが更新されたときにだけ再評価されます。これはつまり、message が変わらない限りは、reversedMessage に何度アクセスしても、関数を再び実行することなく以前計算された結果を即時に返すということです。

必要なときだけ再計算を行う。とてもスマートですが、どのように実現されているのでしょうか? 次のような疑問が湧いてきます。

  • 算出プロパティ(computed)と状態(data)の依存関係をどのように管理しているか
  • 再計算をどのようにおこなうか

この記事のコードリーディングを通して疑問を解消する、あるいはその手がかりを示せたらと思います。

リアクティブプロパティ

算出プロパティは、それが依存するものが更新されたときにだけ再評価されます。

とあるので、「算出プロパティが依存している状態」は、自身に依存している算出プロパティを把握する必要があります。
Vue は算出プロパティの評価時に状態の参照を検知して、依存関係を記録する一方で、状態が変更された場合には、その状態に依存している算出プロパティに再評価を促します。ここで述べたような、状態の参照・変更が検知できるプロパティをリアクティブプロパティと呼んでいます。Vue インスタンスの初期化時にdataオプション の各プロパティに getter/setter を定義して、これを実現しています。

それを行っているのが、https://github.com/vuejs/vue/blob/v2.1.6/src/core/observer/index.js#L128 です。

具体的な getter / setter の内容については後で説明します。

算出プロパティの定義

算出プロパティを computed オプションから実際に定義しているのは、makeComputedGetter 関数( https://github.com/vuejs/vue/blob/v2.1.6/src/core/instance/state.js#L135 )です。

function makeComputedGetter (getter: Function, owner: Component): Function {
  const watcher = new Watcher(owner, getter, noop, {
    lazy: true
  })
  return function computedGetter () {
    if (watcher.dirty) {
      watcher.evaluate()
    }
    if (Dep.target) {
      watcher.depend()
    }
    return watcher.value
  }
}

Watcher インスタンスを生成して、生成したインスタンスを含んだクロージャを返しています。Watcher は、Vue 内部では $watch の実装も含めて様々な所で使われていますが、算出プロパティの実体も Watcher インスタンスということになります。ここでの Watcher インスタンスは以下の役割を持っています。

  • computed オプションで指定した関数(サンプルコードでの fullName プロパティの値)の保持( getter プロパティ )
  • 上の関数の評価( Watcher.prototype.evaluate )
  • 上の関数の評価結果の保持( value プロパティ )
  • 再評価が必要か判断するフラグの保持( dirty プロパティ )
  • 自身が依存するリアクティブプロパティの把握( deps プロパティ )

クロージャの内容から、再評価が必要なときだけ評価がおこなわれ、それ以外の場合は事前に評価された結果が返されることが分かります。ちなみに dirty は初期レンダリング時も真で、評価がおこなわれます。

依存関係の記録

依存関係の記録は、評価をおこなう過程でリアクティブプロパティが参照されたタイミングでおこなわれます。関数の呼び出しは、Watcher インスタンスの evaluate メソッド(算出プロパティの評価)、computed オプションで指定した関数、さらにその内部でのリアクティブプロパティの getter と続きます。

Watcher.prototype.evaluate は、Watcher.prototype.get を呼び出して、dirty プロパティを偽にするだけの単純な内容です( https://github.com/vuejs/vue/blob/v2.1.6/src/core/observer/watcher.js#L191 )。
Watcher.prototype.get をみてみましょう。

  get () {
    pushTarget(this)
    const value = this.getter.call(this.vm, this.vm)
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
    return value
  }

pushTarget 関数は、依存するリアクティブプロパティを記録したい Watcher インスタンス(ここでは算出プロパティ)を登録する関数です。push という名前から分かる通り登録した内容はスタックに追加され、pop で取り出されます。スタックのトップは、Dep.target で参照できます。
this.getter は、computed オプションで定義した関数(サンプルでは fullName )です。呼び出すとリアクティブプロパティ(firstName, lastName)が参照されます。ここで、リアクティブプロパティの getter の定義をみてみましょう( https://github.com/vuejs/vue/blob/v2.1.6/src/core/observer/index.js#L149 )。

  const dep = new Dep()

  // ...

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return value
    },
    //...
  });

Dep は、リアクティブプロパティが依存している Watcher インスタンスを記録し、リアクティブプロパティに変更があった時、 Watcher インスタンスに変更を通知する役割を持っています。getter の定義から分かるように、Dep インスタンスは、リアクティブプロパティにつき、必ずひとつ生成されます。
Dep と Watcher の関係はすこし分かりづらいですが、Dep インスタンス(リアクティブプロパティ)は自身「に」依存している Watcher インスタンス(算出プロパティ)を知っており、一方でWatcher インスタンス(算出プロパティ)は自身「が」依存している Dep インスタンス(リアクティブプロパティ)を知っています。

Dep.prototype.depend ( https://github.com/vuejs/vue/blob/v2.1.6/src/core/observer/dep.js#L30 ) の呼び出しは、定義を調べると分かりますが、Dep.target (この場合は、算出プロパティの Watcher インスタンス) の addDep メソッドの呼び出しです( https://github.com/vuejs/vue/blob/v2.1.6/src/core/observer/watcher.js#L100 )。これで算出プロパティは自身が依存しているリアクティブプロパティを知ることができたのと、さらに Dep.prototype.addSub の呼び出しでリアクティブプロパティは自身に依存している算出プロパティを知ることができました。Sub は 変更の通知を受け取る Subscription の Sub です。  

  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

depIds, newDepIds という 二種類の配列があるのは、前回の評価から依存関係に変更があったか調べて、適切に後始末を行うためです。後始末を行っているのは、Watcher.prototype.get で呼び出されている Watcher.prototype.cleanupDeps です( https://github.com/vuejs/vue/blob/v2.1.6/src/core/observer/watcher.js#L120 )。

ここまでで、算出プロパティがどのように依存関係を記録しているか、みてきました。

リアクティブプロパティの変更の検知と算出プロパティの再計算

リアクティブプロパティの変更は、setter が検知します( https://github.com/vuejs/vue/blob/v2.1.6/src/core/observer/index.js#L162 )。setter では、値の変更がない場合は関数を return して、変更がある場合はプロパティを変更し、Dep.prototype.notify を呼び出します。

    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }

      // ...

      val = newVal
      
      // ...

      dep.notify()
    }

Dep.prototype.notify は、リアクティブプロパティ( Depインスタンス )に依存している算出プロパティ( Watcher インスタンスの配列 subs )に更新を通知します( https://github.com/vuejs/vue/blob/v2.1.6/src/core/observer/dep.js#L36 )。 通知は Watcher インスタンスの update メソッドの呼び出しで行います。Subscription や update から分かるように Watcher と Dep には Observer パターンが適用されています。

  notify () {
    // stablize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }

Watcher.prototype.update をみてみましょう( https://github.com/vuejs/vue/blob/v2.1.6/src/core/observer/watcher.js#L142 )。算出プロパティの場合は lazy プロパティが真なので、再評価が必要か判断するフラグ dirty プロパティに true を代入します。これで次回のレンダリング時、算出プロパティが参照されると、再評価がおこなわれることになります。

  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

まとめ

Vue の算出プロパティは、以下の要素で成り立っていることが分かりました。Vue に限らず、自身で同じような仕組みを実装するときに参考になりそうですね。

  • プロパティの参照と変更をgetter/setterで把握できる仕組み
  • 計算と評価結果、再評価が必要か判断するフラグを保持するオブジェクト
  • 参照が行われたときに、依存関係を記録する仕組み
  • 変更が行われたときに、変更を通知する仕組み(Observer パターンの適用)

Dev Morning の幹事を引き継ぎました

Dev Morning という朝活イベントに常連として参加しているんですが、幹事をしていた @tejitak さんが起業してタイに移住されるので、幹事を引き継ぎました。 今後は iOS エンジニアの @shingt さんと共同で幹事を行います。

Dev Morning って?

渋谷周辺の会社で働くエンジニアやデザイナーが集まって、ビュッフェを食べた後に、もくもくしたり、気になっている技術について話し合ったり、何かサービスを作ったりする朝活イベントです。

毎週日曜の朝10時から渋谷駅の新南口のカフェでやっています。日にもよりますが、時間は大体2~3時間ぐらいで、途中参加・退出も自由です。

僕が参加し始めたのは、1~2年前で、最近はライブラリのドキュメントの翻訳をやったり、新しく出てきたツールやBrowser APIの検証、個人で使うウェブアプリの開発をやっています。 参加しているエンジニアは、Rails や Go を扱っているウェブアプリケーションエンジニア、iOS エンジニア、ウェブフロントエンドエンジニアなど様々で、互いの領域で最近耳にするキーワードについて雑談することで、未知の領域について知る・学ぶ・実際の業務に生かすきっかけになっているように思います。

朝活イベントというと、お堅いイメージがあるかもしれませんが、のんびり、ゆるいかんじです。そのゆるさもあってか、かなり続いていて、来週で124回目の開催です。
「普段、日曜日をダラダラと過ごしてしまうので、日曜の朝の時間を有意義に過ごしたい」という方にぴったりのイベントなので、気が向いたらぜひご参加ください。

devmorning.connpass.com