フロントエンドエンジニアをやっています。
頑張るから読んでほしい。

ハイパフォーマンスブラウザネットワーキングを読みました(HTTP)

最近ハイパフォーマンスブラウザネットワーキングの10章~12章を読んだのでまとめました。

どんな本?

  • ブラウザに関連する、インターネットで使用される様々なネットワーク技術をまとめたもの
  • ハイパフォーマンスを誇るアプリケーションを構築するためには、なぜネットワークがそのような挙動を見せるのか理解する必要がある
    • 「良い開発者はどのように動作するか知っている。素晴らしい開発者はなぜ動作するかを知っている」
  • Webアプリケーションを提供するには、ブラウザとネットワークの動作の関連性について、硬い基盤となる知識が必要とされる
  • ゴールはネットワークについてすべての開発者が知っておくべきすべてを説明すること
    • どのようなプロトコルが用いられてる?
    • どのような制限を持つ?
    • アプリケーションをその基盤ネットワークに対して最適化する方法はどのようなもの?
    • ブラウザにが提供するネットワーク機能とその使いどころは?

目的

ハイパフォーマンスを誇るアプリケーションを構築するために、なぜネットワークがこのような挙動なのかを理解し、理由が述べられるようになる。(なりたい...)

とりあえず10章~12章に絞って読みました。

10章 Web パフォーマンス入門

パフォーマンス最適化のプロセスの大部分は、システム内で明確に分離している。 制限と制約を持った各レイヤーの間に存在する相互作用を理解し紐解くこと。

すべての異なるレイヤーの相互作用を最適化すること?

相互依存しているいくつかの方程式を説いて回答を導き出すことではなく、場合によって多数の解が存在する。

それぞれのパフォーマンスのベストプラクティスを数値化・分析する前に一歩下がって、問題そのものを定義しておくことが重要。

  • モダンWebアプリケーションとは?
  • どのようなツールが利用可能なのか?
  • Webアプリケーションをどのように計測するのか?

など

モダンWebアプリケーションの解剖学

モダンWebアプリケーションとは?

以下のサイトが参考になる。 主要Webサイトのコンテンツを自動分析してレポートしている。 https://httparchive.org/reports/state-of-the-web

image.png (234.5 kB)

webアプリケーションにはインストールプロセスがない。 URLを入力し、enterキーを押すだけで使える状態になる

最高のweb体験提供するには、数百のリソース、数MBのデータ、いくつもの異なるホスト。 これらすべてを数百ミリ秒以内に。

スピード、パフォーマンス、そして人間の知覚

生活のペースが歴史上一番早くなっている。 にもかかわらず、反応時間は常に一定。

アプリケーションが瞬時に反応したと感じさせられるためには、ユーザの入力に対して知覚できるレスポンスを数百ミリ秒のうちに提供しなければならない。

一般的なWebページのリクエストにかかるDNSルックアップ、TCPハンドシェイク、Webページリクエストに通常かかるいくつかのパケット往復時間を合計してみる

100-1000ミリ秒というレイテンシの予算はネットワークオーバーヘッドだけで、すべてではないにせよ、その大半が簡単に消化せれてしまう。

リソースのウォーターフォールチャートを分析する

Webパフォーマンスの議論はリソースのウォーターフォールチャートに言及するまで完結しない。

ウォーターフォールチャート → 増減を表すのに便利なグラフ

WebPageTestを使って診断する。

repro.ioの測定結果

Waterfall View

f:id:cidermitaina:20190420141457p:plain

  • fontの読み込みが多い印象
  • jsの読み込みが多い印象

f:id:cidermitaina:20190420142517p:plain

f:id:cidermitaina:20190420142450p:plain

  • DNS Lookup
  • Initial Connection
    • TCP接続の確立
  • SSL Negotiation
    • SSLを介してリソースをセキュアに読み込んでいる場合、ブラウザがその接続を確立している時間
  • Time to First Byte
    • リクエストがサーバーに送られ、サーバーがそれを処理して、必要な情報を送信し始め、レスポンスの最初の1バイトがブラウザに届くまでにかかる時間

パフォーマンスの柱:演算、レンダリング、ネットワーク

webプログラムの実行の主要なタスク

レンダリングスクリプトの実行モデルはシングルスレッド、インターリーブ型 同時並行して変更を加えることはできない

早く効率的なネットワークリソースの配信がブラウザで動作するすべてのアプリケーションのパフォーマンスにおいて必要

より大きい帯域幅は効果なし

動画よりもはるかに小さいWebアプリケーションがなぜ難題?

パフォーマンスのボトルネックとしてのレイテンシ

SPDYプロトコルの作者の一人、Mike Belsheによる定量的調査 「帯域幅とレイテンシに対するページロード時間」の画像検索結果

  • 帯域幅のアップグレードを行ってもWebページの速度はあまり変わっていない
  • 逆にレイテンシの改善を行うと比例してペイロード時間も下がる

一般的にインターネットをスピードアップするには、

  • RTT(Round-Trip Time) を下げる方法を探すべき
  • ページロードに必要とされるパケット往復の数を減らすこと
    • クライアントとサーバ間で結構な量のやり取りが発生している
    • パケットの往復の数は主にクライアントとサーバ間で通信を開始するためのハンドシェイク(DNSTCP、HTTP)によるもの、通信プロトコルにより引き起こされる往復がある
    • より少ない往復でデータ転送することができるのであれば、短縮ができる(SPDYの目標)

レイテンシ データ転送における指標のひとつで、転送要求を出してから実際にデータが送られてくるまでに生じる、通信の遅延時間のことをいいます。 この遅延時間が短いことをレイテンシが小さい(低い)、遅延時間が長いことをレイテンシが大きい(高い)と表現しています。

RTT(Round-Trip Time) 通信相手に信号やデータを発信してから、応答が帰ってくるまでにかかる時間。

ブラウザ最適化

パフォーマンスはブラウザベンダにおける最も重要な競争力の要素であり、ネットワークパフォーマンスがその重要な基準であれば、ブラウザが日々進化していることに驚くべきことではない。

ブラウザ最適化は2つの大きなカテゴリに分類される

  • ドキュメント認識最適化
    • 早くリクエストを送り、ページを取得そて操作可能なインタラクティブ状態にする
    • リソース優先度付け、先読み解析などの技術で実現されている
  • 投機的最適化
    • DNSの事前開発やホストへの事業接続を行い、ユーザーが行う可能性が高いアクションを予測する投機的最適化を行う

↑の最適化はユーザや開発者の代わりにブラウザが自動的に行う

内部的にこれらの最適化がどのように、なぜ行われているのかの理解が大切

開発者はどのようにブラウザを補助できるのか?

最初に、ページの構造と配信によく注意を払う

  • CSSJavaScriptのような重要なリソースはドキュメント上で可能な限り発見できるべき
  • レンダリングJavaScriptの実行をブロックしないために、CSSは可能な限り早く配信されるべき
  • 重要度の低いJavaScriptはDOMとCSSOMの構築をブロックしないように後回しにすべき
  • HTMLドキュメント上はサーバに徐々に解析されるため、ドキュメントはサーバ上で生成された次第、部分的にも随時送信されるべき
dns-prefetch

<link rel="dns-prefetch" href="//somewidget.example.com">

DNS事前解決

外部URLからリソースを取得する要素がある場合、ブラウザはdocumentの上部から解釈し、外部URLを見つけた時点で外部URLへリクエストを送ります。この時に、前もって名前解決を済ませておけば、どこにリクエストするのかがわかっているから処理するのが早い

subresource

<link rel="subresource" href="/css/style.css">

重要度は高いがページの後ろで読み込まれるリソースの優先プリフェッチ

subresourceは同一ページ内で使用する任意のリソースを裏で読み込んでおくことができる。

prefetch

<link rel="prefetch" href="//example.com/future-image.jpg">

リソースのプリフェッチ

ユーザーが次に訪問する可能性が高いページを開発者がわかっている場合、リソースを前もって取得しておくことができる。ただし、JSやCSSなどキャッシュ可能なリソースに限られる。

prerender

<link rel="prerender" href="//example.com/future-page.html">

指定ページのプリレンダリング

指定したページのCSSを読み込み、JSを実行、ページ全体の不可視バージョンを作成する。

10章まとめ

DNSTCPSSLの遅延はほとんどのユーザと、Web開発者にはまったく透過的で、ネットワーク層で処理される。 ブラウザがこれらの往復を予測することを補助することによって、これらのボトルネックを排除しより速くより良いWebアプリケーションにつながる

11章 HTTP1.x

HTTP1.0の最適化はHTTP1.1にアップグレードすること この標準は多くの重要なパフォーマンス強化や機能を追加した

  • 接続の再利用を可能とする、永続的接続
  • レスポンスのストリーミングを可能とするチャンク化された転送エンコード
  • 並列リクエストの処理を可能とするリクエストパイプライン
  • リソース要求のバイトレンジ指定を可能にするバイト単位の転送
  • 改善され、より効果的にしていされたキャッシュメカニズム

永遠の最適化項目

  • DNSルックアップを減らす
    • ルックアップ中はリクエストがブロックされる
  • HTTPリクエスト数を減らす
    • ページから不要なリソースを排除
  • CDNを利用
    • データを地理的に近い場所に配置することでTCP接続のネットワークレイテンシを劇的に減らせる
  • Expiresヘッダを追加し、ETagを設定
  • 同じリソースの取得のために何度もリクエストを送信しないように必要なリソースはキャッシュのしておくべき
  • リソースをGzip圧縮する
    • テキストベースのリソースが転送される際はGzipで圧縮するべき
    • Gzipはファイルサイズを平均で60~80%削減し、高い効果が得られる
  • HTTPリダイレクトを避ける
    • HTTPリダイレクトは、特に異なるホストにリダイレクトする際に発生する追加DNSルックアップやTCP接続レイテンシなどによって極端に高コストになる可能性がある

HTTP1.1で追加されたパイプラインは実質的には失敗しており、基礎に亀裂を生じさせている 常に独創的であり続けるWeb開発者コミュニティが数々の自家製の最適化を発明 (ファイル結合、スプライト、インライン化)

しかしこれらの技術はHTTP1.1の制限を回避する一時しのぎのためのもの 本来はこのような最適化を気にすべきではない

キープアライブ接続の利点

HTTP1.1の主要なパフォーマンス向上の一つが持続的接続な接続、または、キープアライブ接続の採用

なぜこの機能がパフォーマンス戦略における重要な要素なのか?

キープアライブ

一つのTCP接続を一回のHTTP通信で切断せず、複数のHTTPリクエスト/レスポンスを送受信するよう維持する機能。

  • キープアライブなしでは、それぞれのリクエストが2往復分のレイテンシを発生させる
  • キープアライブを有効にすると、最初のリクエストは2往復分、そしてそれ以降のリクエストは一往復分のレイテンシを発生させる

言うまでもなく、すべてのアプリケーションにとって持続的なHTTP接続は重要な最適化

HTTPパイプライン

HTTPパイプライン

一つのTCPコネクション上で、複数のHTTPリクエストを応答を待つことなく送信する技術である。 リクエストをパイプライン化することにより、ウェブページの読み込みが大幅に高速化される。

リクエストを早いうちに送信しておくことによって、それぞれのレスポンスをブロックすることなくもう一往復分のネットワークレイテンシを削減できる

しかし、HTTP1.1では多重化送信がサポートされていいないため、パイプラインはHTTPサーバや中間装置、そしてクライアントに様々な微妙かつドキュメント化されていない影響を及ぼす

http-pipeline 参考

しかし、パイプラインはサーバー側できちんとした対応がされていないと、ブラウザ側のリクエストを正しく処理できないということになり実装にも課題がありました。さらに、HTTPパイプラインには「サーバーはリクエストの順番通りにレスポンスを返さなければならない」という制限があります。

5個あるリクエストのうち1番目のリクエスト処理が遅い場合、2個目以降のレスポンスは待ち状態(ヘッドオブラインブロッキング)になり結果全体の速度が遅くなるといった課題がある。このような状況から、パイプラインはOPERAブラウザを除きほとんどのモダンブラウザはデフォルトでOFFになっており、残念ながらほとんど利用されていないのが現状。

  • 遅いレスポンスが一つ存在すると、その後のすべてのリクエストがブロックされる
  • 並列処理を行う際、サーバはパイプライン化されたレスポンスをバッファリングする必要があり、サーバのリソースを圧縮する可能性がある。
  • レスポンスに失敗するとTCP接続が終了する場合があり、クライアントは未処理リクエストを再送して重複処理になる可能性がある
  • 中間装置が存在する可能性がある場合、パイプラインの互換性を信頼できる形で検出することは難しい。
  • 中間装置にはパイプラインをサポートしないものがあり、接続切断する可能性がある、

↑のような複雑さと、発生する問題に対する指針がHTTP1.1標準で示されてないために、ある程度の改善効果が見込めるにも関わらずHTTPパイプラインはほとんど採用されていない

複数のTCP接続を使用する

モダンブラウザでは、デスクトップでもモバイルでも、ホストあたり6接続まで並列使用できる。

なぜ?

接続数が多いほどクライアントとサーバのオーバヘッドは増大するが、リクエストの並列性による利点は増す。 ホストあたりの6接続はバランスをとった。

オーバーヘッド

付加的に発生した処理(負荷)のこと

ドメインシャーディング

シャーディング(分割)

HTTP1.xプロトコルが持つ欠如のために、ブラウザベンダは1ホストあたり6つまでのTCPストリームを管理する接続プールの採用と維持を強いられてきた。

HTTPArchiveによると、平均的なWebページは90以上のリソースで構成されている。 これらのリソースがすべて同じホストから配信されている場合は相当なキューイング遅延が発生。

すべてのリソースを同じホストから配信しなければならない?

すべてのリソースを同じホスト(www.example.com)から配信する代わりに{shard1, shardN}.example.comといった複数のサブドメインにシャーディング(分割)できる。

接続先のホスト名が異なるので

  • ブラウザの接続制限を増加させてより高いレベルの並列性を手に入れることができる。
  • より多くのドメインに分割することで、より並列性が高まる。

デメリット

  • それぞれのホストについてDNSルックアップが必要になる
  • サイト管理者がリソースをどのように分割配置するかを管理しなければならない

実際にはドメインシャーディングは過度に利用される傾向があり、数十の利用されていないTCP接続を低下させてしまう。 HTTPSを使用しなければならない場合はTLSハンドシェイクのために追加のパケット往復が発生し、更にコストが高くなる

まとめ

ドメインシャーディングの実施に際して以下を考慮する必要がある

  • まずTCPの最適化
  • ブラウザは6つまでの接続を自動的に開始
  • それぞれのリソースの数、サイズ、そしてレスポンス時間がシャードの最適数を左右する
  • クライアントレイテンシと帯域幅がシャードの最適数を左右する
  • ドメインシャーディングはDNSのルックアップとTCPスロースタートによってパフォーマンスを低下させる可能性がある

ドメインシャーディングは合理的ですが、不完全な最適化。 まずリソースの数を減らしてリクエスト数を削減することで大きな利益を得ることができる。

プロトコルオーバーヘッドの計測と制御

HTTP1.0はリクエストとレスポンスのヘッダに書式を追加しメタデータの交換ができるようにプロトコルを拡張した

ブラウザが開始するHTTPリクエストじゃ500-800バイトのHTTPのメタデータを運んでいるい。 すべてのHTTPヘッダは無圧縮のプレーンテキストで送信されるため、すべてのリクエストで高いオーバーヘッドにつながる可能性がある。

無圧縮で繰り返し送信されるヘッダの転送量を減らすことで、パケット往復によるネットワークレイテンシを削減でき、パフォーマンス向上に繋がる。

ファイル結合とスプライト

より早いリクエストは、そもそもリクエストが行われないこと。

リクエスト数を減らすことは、使用プロトコルやアプリケーションに関係なく最高の最適化。

リソースを一つのリクエストにまとめる

ファイル結合 複数のJavaScriptcssファイルを一つのリソースにまとめること

スプライト 複数の画像を統合した画像

デメリット

複数の独立したリソースのバンドルはキャッシュのパフォーマンスやページの実行速度を低下させる。

  • 統合されたファイルは、現在のページには必要ないリソースを含んでいる可能性がある
  • どれか一つのファイルをアップデートするとキャッシュは無効化され、リソースのバンドルを再度ダウンロードする必要があり、オーバーヘッドが追加発生する
  • JavaScriptCSSはどちらも、転送が終了してから構文解析を開始し、実行する。したがってアプリケーションの実行を遅くする可能性がある。

まとめ

結合とスプライトはHTTP1.xプロトコル向けのアプリケーション層における最適化。 正しく使用された場合はかなりのパフォーマンス向上をもたらすが、アプリケーションの複雑さやキャッシュ時の注意、アップデート時のコスト、スクリプトレンダリングの実行時間に悪い影響を与える可能性がある。

  • アプリケーションは多くの小さなリソースのダウンロードによってブロックされているか?
  • アプリケーションは特定のリクエストを結合することで利益を得ることができるか?
  • キャッシュの粒度が落ちるとアプリケーションに悪い影響があるか?
  • 結合された画像は高いメモリオーバヘッドに繋がるか?
  • 実行が遅くなることで最初のレンダリングまでの時間に影響を与えるか?

リソースインライン化

ドキュメントに直接リソースを埋め込むことにより、リクエストの数を減らす

data:image/gif;base64,R0lGODlhAQABAAAAACw=

dataURIスキームを利用すれば、画像や音声、PDFファイルを埋め込める

dataURIは小さくて、汎用的ではないリソースに向いている。 インライン化されるとブラウザやCDNにキャッシュされない

リソースをインライン化する大まかな指針としては、リソースの大きさが2KB未満程度であること。

検討すべき基準は以下

  • ファイルが小さく、特定のページにのみ配置される場合は、インライン化を検討
  • ファイルが小さく、多くのページで再利用される場合は、バンドル化を検討
  • ファイルが小さくても頻繁にアップデートされる場合は個別ファイルのまま扱う

12章 HTTP2.0

HTTP2.0はアプリケーションをより速く、よりシンプルに、そして堅牢にする

HTTP2.0の目標は、リクエストとレスポンスの多重化によるレイテンシの削減、HTTPヘッダフィールドの効率的な圧縮によるプロトコルオーバーヘッドの最小化、リクエスト優先度設定とサーバプッシュの実現

HTTP2.0はHTTPメソッド、ステータスコードURI、などすべてのコアコンセプトを全く変更しない。

目的は最高のパフォーマンス

HTTP2.0の歴史、そしてSPDYとの関係。

SPDYはHTTP2.0ではない。 HTTP-WG内の様々な議論を経てSPDYがHTTP2.0の出発点に採用される

設計と技術的目標

HTTP1.xは実装のシンプルさを意識して設計される。 しかし残念ながら、実装のシンプルさによってアプリケーションのパフォーマンスが犠牲になる。

HTTP2.0はこの犠牲を取り戻すよう設計されている。

バイナリフレーミングレイヤー

HTTP2.0のすべてのパフォーマンス強化の中心となる存在は、バイナリフレーミングレイヤー。

ストリーム、メッセージ、フレーム

HTTP/2 バイナリ フレーミング レイヤー 参考

ストリーム

確立した接続内の双方向のフレームの流れ

メッセージ

フレームの完全なシーケンス。論理的なメッセージを構成する。

フレーム

HTTP2.0における通信の最小単位。それぞれのフレームはヘッダを持ち、ヘッダは最低でもそのフレームが所属するストリームを識別する

HTTP/2 ストリーム、メッセージ、フレーム 参考

リクエストとレスポンスの多重化

HTTPメッセージをフレームに分割し、インターリーブし、相手側で再構成を行う一連の機能は、HTTP2.0において最も重要な強化ポイント

http2-ストリームの多重化 参考

HTTP/2では1つの接続上にストリームと呼ばれる仮想的な双方向シーケンスを作ること(ストリームの多重化)で問題を克服している。

リクエスト優先度付け

HTTP2.0は優先度の扱いについてのアルゴリズムを指定していない。 ただ、優先度付けが行われたデータをクライアントとサーバ間でやり取りできるメカニズムを提供しているだけ。

クライアントは適切な優先度データを送信すべきで、サーバはその優先度に従って処理と配信を行うべき。

フロー制御

ひとつのストリームがリソースを占有してしまうことで、他のストリームがブロックしてしまうことを防ぐこと

サーバプッシュ

クライアントのリクエスト一つに対してサーバが複数のレスポンスを返すことができる

リソースはドキュメントの構文解析を行うことによって発見されるが、あらかじめサーバからプッシュしてもらう

ヘッダ圧縮

ヘッダのメタデータを圧縮。

効率的なHTTP2.0アップグレードと発見

HTTP2.0のサーバサポートについて情報が存在しない場合、クライアントはHTTPアップグレードを使用して適切なプロトコルネゴシエートを行う。

このアップグレードを利用して、サーバがHTTP2.0に対応していない場合はHTTP1.1のレスポンスを返します。

まとめ

最高のパフォーマンスのために、「フレーム」という層を新しく設ける そのためにHTTP メッセージをテキスト→バイナリに

その結果 1つの接続上にストリームと呼ばれる仮想的な双方向シーケンスを作ること(ストリームの多重化)でパフォーマンス(レイテンシの改善がされた)が向上。

最後に(まとめと感想)

HTTP Archiveを眺めてると、モダンなwebアプリケーションを見てみるとリクエスト数やファイルサイズの年々増加していることが分かりました。

現在の平均的なWebアプリケーションは約1.8MBのサイズ、約70以上の従属リソースで構成されています。

また、サイトパフォーマンス向上のため多くのWeb開発者が数々の最適化(ファイル結合、スプライト、インライン化)を行っていますが、一時しのぎのためのものであって、本質的な解決策ではないです。

このような背景から、近年のモダンwebアプリケーションにおいて、HTTPのあるべきを考え、HTTP2が開発されたのでは? と読んでて思ったり。

HTTP2を使えば以前は不可能だった多くの最適化が可能になると思っていたのですが、QUICやHTTP/3の使用策定が行われているという話を聞き、 QUICやHTTP/3の話にも興味を持ちました。(これからどうなっていくのだろう...?)




参考

HTTP pipelining - Wikipedia

HTTP/2 の概要  |  Web Fundamentals  |  Google Developers

はじめてのコンパイラ

ブラウザの仕組みを勉強したのですが、言語処理系の部分やコンパイルの仕組みが気になったので、 コンパイルの仕組みを調べてみました。

具体的には、 the-super-tiny-compilerのコードリーディング、写経を行い実際にブラウザで動かしてみて、コンパイルの仕組みを調べてみました。

- 実際にブラウザで動かしてみる。

- コード

今回の記事は

  • なぜthe-super-tiny-compiler?
  • コンパイラの仕組み
  • the-super-tiny-compilerのコードリーディング

について書いています。

the-super-tiny-compilerとは?

JavaScriptで書かれたコンパイラの主要部分が非常に単純化された例。

なぜthe-super-tiny-compilerを読む?

コンパイラは身の回りにたくさんあるし、多くのツールはコンパイラのコンセプトを取り入れているのでthe-super-tiny-compilerを読むことで良い経験になるかなあと思いました。

どんなものを作る?

Lispのような関数呼び出しをCのような関数呼び出しにコンパイルするものを作成します。

LISP-style C-style
(add 2 (subtract 4 2)) add(2, subtract(4, 2))

Stages of a Compiler

ほとんどのコンパイラは、解析、変換、およびコード生成の3つの主要な段階に分類されます。

  1. 構文解析は生のコードを受け取り、それをコードのより抽象的な表現に変換します。
  2. 変換はこの抽象表現を受け取り、コンパイラーが望んでいることを何でもするように操作します。
  3. コード生成はコードの変換された表現を受け取り、それを新しいコードに変えます。

Parsing(解析)

構文解析は通常、2つのフェーズに分けられます。

字句解析(Lexical Analysis)

生のコードを受け取り、それをトークナイザー(またはレクサー)と呼ばれるものによってトークンと呼ばれるものに分割する。

トークンは、孤立した構文の一部を表す小さな小さなオブジェクトの配列

(add 2 (subtract 4 2))

トーク

[
  { type: 'paren',  value: '('        },
  { type: 'name',   value: 'add'      },
  { type: 'number', value: '2'        },
  { type: 'paren',  value: '('        },
  { type: 'name',   value: 'subtract' },
  { type: 'number', value: '4'        },
  { type: 'number', value: '2'        },
  { type: 'paren',  value: ')'        },
  { type: 'paren',  value: ')'        },
]

構文解析

トークンを受け取り、抽象構文木( abstract syntax tree、AST)を作成します。 (構文の各部分およびそれらの相互関係を記述する表現に再フォーマットしている。)

抽象構文木(AST)

{
  type: 'Program',
  body: [{
    type: 'CallExpression',
    name: 'add',
    params: [{
      type: 'NumberLiteral',
      value: '2',
    }, {
      type: 'CallExpression',
      name: 'subtract',
      params: [{
        type: 'NumberLiteral',
        value: '4',
      }, {
        type: 'NumberLiteral',
        value: '2',
      }]
    }]
  }]
}

Transformation(変換)

ASTを受け取り、変更を加えることができます。 ASTを同じ言語で操作することも、まったく新しい言語に翻訳することもできます。

Traversal(通過、横断する)

{
  type: 'Program',
  body: [{
    type: 'CallExpression',
    name: 'add',
    params: [{
      type: 'NumberLiteral',
      value: '2'
    }, {
      type: 'CallExpression',
      name: 'subtract',
      params: [{
        type: 'NumberLiteral',
        value: '4'
      }, {
        type: 'NumberLiteral',
        value: '2'
      }]
    }]
  }]
}

↓ 以下のように通過します

Program - ASTのトップレベルから開始
CallExpression(add) - プログラム本体の最初の要素に移動する
NumberLiteral(2) - CallExpressionのパラメータの最初の要素への移動
CallExpression(減算) - CallExpressionのパラメータの2番目の要素に移動する
NumberLiteral(4) - CallExpressionのパラメータの最初の要素への移動
NumberLiteral(2) - CallExpressionのパラメータの2番目の要素への移動

Visitors

ツリーの各ノードにアクセスするには?

"visitor"オブジェクトを作成

var visitor = {
  NumberLiteral(node, parent) {},
  CallExpression(node, parent) {},
};

ASTを通過するときに、一致するタイプのノードに「入る」たびに、このvisitorメソッドを呼び出します。 ノードと親ノードへの参照も渡します。

  • → Program (enter)
    • → CallExpression (enter)
      • → NumberLiteral (enter)
      • ← NumberLiteral (exit)
      • → CallExpression (enter)
        • → NumberLiteral (enter)
        • ← NumberLiteral (exit)
        • → NumberLiteral (enter)
        • ← NumberLiteral (exit)
      • ← CallExpression (exit)
    • ← CallExpression (exit)
  • ← Program (exit)

"exit"で呼ぶ可能性があるので最終的には

var visitor = {
  NumberLiteral: {
    enter(node, parent) {},
    exit(node, parent) {},
  }
};

Code Generation(コード生成)

コードを新たに作成します。

これで完成です!

the-super-tiny-compilerのコードリーディング

Parsing(解析)

1-tokenizer.js

(add 2 (subtract 4 2))トークンにします。

[
  { type: 'paren',  value: '('        },
  { type: 'name',   value: 'add'      },
  { type: 'number', value: '2'        },
  { type: 'paren',  value: '('        },
  { type: 'name',   value: 'subtract' },
  { type: 'number', value: '4'        },
  { type: 'number', value: '2'        },
  { type: 'paren',  value: ')'        },
  { type: 'paren',  value: ')'        },
]

数字や文字は1文字ではなく(123 456)の場合などが考えられるので以下の処理をします。

//数値が続く限り
      while (NUMBERS.test(char)) {
        value += char;
        char = input[++current];
      }

2-parser.js

1-tokenizer.jsでできたトークンをastにします。

[
  { type: 'paren',  value: '('        },
  { type: 'name',   value: 'add'      },
  { type: 'number', value: '2'        },
  { type: 'paren',  value: '('        },
  { type: 'name',   value: 'subtract' },
  { type: 'number', value: '4'        },
  { type: 'number', value: '2'        },
  { type: 'paren',  value: ')'        },
  { type: 'paren',  value: ')'        },
]

ast = {
  type: 'Program',
  body: [{
    type: 'CallExpression',
    name: 'add',
    params: [{
      type: 'NumberLiteral',
      value: '2'
    }, {
      type: 'CallExpression',
      name: 'subtract',
      params: [{
        type: 'NumberLiteral',
        value: '4'
      }, {
        type: 'NumberLiteral',
        value: '2'
      }]
    }]
  }]
};

walkという再帰関数でループしています。

Transformation(変換)

3-traverser.js

nodeにアクセスできるようにします。 astの配列を順番に見ていき、enterメソッド、exitメソッドがある場合は呼び出します。

function traverseArray(array, parent) {
    array.forEach(child => {
      traverseNode(child, parent);
    });
  }

4-transformer.js

3-traverser.jsを使って新しいastを作成します。

ast = {
  type: 'Program',
  body: [{
    type: 'CallExpression',
    name: 'add',
    params: [{
      type: 'NumberLiteral',
      value: '2'
    }, {
      type: 'CallExpression',
      name: 'subtract',
      params: [{
        type: 'NumberLiteral',
        value: '4'
      }, {
        type: 'NumberLiteral',
        value: '2'
      }]
    }]
  }]
};

newAst = 
{
  type: 'Program',
  body: [{
    type: 'ExpressionStatement',
    expression: {
      type: 'CallExpression',
      callee: {
        type: 'Identifier',
        name: 'add'
      },
      arguments: [{
        type: 'NumberLiteral',
        value: '2'
      }, {
        type: 'CallExpression',
        callee: {
          type: 'Identifier',
          name: 'subtract'
         },
        arguments: [{
          type: 'NumberLiteral',
          value: '4'
        }, {
          type: 'NumberLiteral',
          value: '2'
        }]
      }]
    }
  }]
};

Code Generation(コード生成)

5-code-generator.js

新しいastを元に構文作成。

.mapで新しい配列を作成。.joinで配列の中身を連結させて文字列にしています。

"add(2, subtract(4, 2));"

実際にブラウザで動かしてみた

- 実際にブラウザで動かしてみる。

- コード

まとめ

実際に簡単な例のコンパイラをコードリーディングをしてみることで、コンパイルの仕組みを理解することができました。

ESlintとかBabelだったり、pug→htmlの変換も同じような流れなのかなと、思ったり...!

今度はRubyでつくるRubyも読んでみたいなあと思っています。

ブラウザのレンダリングの仕組み

「ブラウザを立ち上げてアドレスバーにURLを打ち込んでEnter押してからページが表示されるまでに (裏側で) 何が起こっているかわかる限り説明してみてください。」

っていう問題を先輩から出題されたのですが、上手く答えられず...

ブラウザのレンダリングについてWebフロントエンド ハイパフォーマンス チューニングを読んで勉強したのでそのmemoです。

1. レンダリングエンジン

ブラウザ レンダリングエンジン JavaScriptエンジン
Google Chrome Blink V8
IE Trident Chakra
Microsoft Edge EdgeHTML Chakra
Firefox Gecko SpiderMonkey
Safari Webkit Nitro(JavaScriptCore)

Blink,WebKit,Geckoオープンソースレンダリングエンジン。 BlinkはWebKitをフォークして誕生。

ブラウザ内のコンポーネント

いくつかのソフトウェアコンポーネントによって構成されている。

知っておきたい2つの重要なコンポーネントは以下。

レンダリングエンジンとは?

JavaScriptエンジンとは?

CSSOMとは?

CSSOMは、CSS Object Modelを意味する略語であり、ブラウザでロードされたCSSのツリー構造を保持する仕組み。ブラウザは、このCSSのツリー構造をDOMに対して適用することで、上位のスタイルからより具体的な下位のスタイルへと連鎖的にスタイルを決定していく。

2. ブラウザのレンダリングの流れ

レンダリングの大まかな流れ

Loading(リソース読み込み)

Scripting(JavaScript実行)

Rendering(レイアウトツリー構築)

Painting(レンダリング結果の描画)

4つの工程からレンダリングが始まって、最終的に描画されるまでをフレーム(Frame)と呼ぶ。

3. リソース読み込み

まず行われるのがリソース読み込み

ブラウザは、与えられたURLからHTMLを読み込んで、そこからさらにレンダリングに必要な付属するリソースを読み込んで解釈する このフェーズでは、次の2つの処理がある。

  • リソースのダウンロード
    • HTMLを含むリソースをサーバーからダウンロードする。
  • リソースのパース
    • ダウンロードしたリソースをパース(構文解析)してレンダリングエンジンの内部表現に変換する
      • HTMLやCSSは、それぞれDOMツリーやCSSOMツリーの変換される

リソース

  • HTMLファイル
  • CSSファイル
  • JS画像ファイル

3.1 リソース取得に用いるネットワークプロトコル

ブラウザは与えられたURLをもとにレンダリングをもとにレンダリングに必要なリソースを様々なネットワークプロトコルを通じて取得する。

ブラウザでよく利用されるネットワークプロトコルHTTP

  • URLに含まれるホスト名の解決
  • HTTPによる取得
    • TCP接続の確立
    • HTTPSであれば)TLS接続の確率
    • HTTPリクエストの送信とHTTPレスポンスの受け取り

IP(Internet Protocol)

ネットワークのノード間のパケットのやり取りを中継するプロトコル

HTTPでネットワーク越しにリソースを取得する場合、リソースの取得の速さはIPのパケットの届く速度に依存する。 →HTTPでやり取りされるデータパケット内に格納されるから。

パケット データの通信に利用される最小単位のデータ。 通信するデータの入れ物としてやり取りされる

TCP(Transmission Control Protocol)

TCPは、IPに対して以下のような機能を付加する上位プロトコル

  • 相手先に確実にデータが届いているかどうか確認
  • データの欠損や破損をけんちして再送
  • データの送信順を保証する

TLS(Transport Layer Security)

与えられたURLのプロトコル部分がhttpsだった場合TCPとHTTPの間でTLSプロトコルを利用する。 TLSは一般にSSLと呼ばれる。

  • クライアントとサーバーの認証機能
  • 通信データの暗号化
  • データの改ざんの検出

UDP (User Datagram Protocol)

IPに対して機能を付加。 TCPと似ている。 TCPプロトコルは、IPに対してデータの欠損の検知やデータの送信順の保証などにお信頼性のあるデータ通信を提供するがUDPではそのような機能は特に追加しない。

HTTP

  • ウェブページのコンテンツを送受信するために利用されるプロトコル
  • シンプル
  • クライアントはサーバーに対してHTTPリクエストを送信し、サーバーはクライアントに対して- - HTTPレスポンスを返す。

DNS(Domain Name System)

IPアドレスを解決するためのシステム、プロトコル

TCP接続では、接続を開始するのに相手方のIPアドレスが必要。 ブラウザはURLに含まれているホスト名をIPアドレスに変換した上でHTTPリクエストをサーバーへと送信する。

DNSはホスト名に紐づくIPアドレスを検索するための分散システム。

4. それぞれのリソースの読み込み

  • ブラウザはホスト名の解決などを経て、HTML,CSS,JacaScriptや画像といったリソースを取得する。

  • 一番最初に読み込まれるリソースはHTMLファイル。

  • HTMLファイル内に記述されているリソースの参照があれば、さらにそのリソースを読み込む。
  • 取得したリソースは、パースされてブラウザの内部表現に変換される。
    • HTMLコンテンツの場合はDOMツリーに
    • CSSの場合はCSSOMツリーに
    • それ以外にリソースも、レンダリングエンジンの実装に沿った内部表現に変換される。

HTMLの読み込み

  1. ブラウザは与えられたウェブページのURLを元にサーバーへHTTPリクエストを送信。 HTTPレスポンスとしてHTMLを取得する。
  2. 読み込んだHTMLを解釈してドキュメントのDOMツリーを構築する。DOMツリーへの変換過程の中でツリーに含まれる画像やCSSなどのドキュメントに紐づくリソースに取得や読み込みを行う。
  3. ブラウザは構築したDOMツリーを元にしてRenderingの処理を行う。

DOM(Document Object Model

HTMLのドキュメントを表現するオブジェクト。 DOMツリーは、レンダリングエンジンが利用する木構造を持つ内部表現。



f:id:cidermitaina:20190303224436p:plain

DOMツリーへの変換の工程

  1. 字句解析によるトークンのリスト化
  2. 構文解析による構文木構築
  3. 構文木内にあるJavaScriptを実行しつつDOMツリーの構築

字句解析

コンパイラーがソースコードを解析し、目的のプログラムを生成する際の処理工程のひとつ。字句解析は、ソースコードに記述された変数や定数などの値を実際の値に展開するまでを担当し、その結果を次の工程である構文解析に引きわたす。

トーク

1つの塊になっている文字列

CSSの読み込み

読み込まれたCSSは、レンダリングエンジンによってパースされてCSSOM(CSS Object Model)ツリーへと変換される。

f:id:cidermitaina:20190303224508p:plain



DOMツリーと違ってCSSOMツリーの深さは可変ではなく、一定ですが、ルールセットが増えれば増えるほどDOMツリー内の要素に適用されるスタイル計算にかかる時間が大きくなる。

5. JavaScript実行 Scripting

リソースを一式読み込んだ後、JacaScript実行(Scripting)へ移行。

レンダリングエンジンは、JavaScriptのコードをJavaScriptエンジンに引き渡して実行させる。

JavaScript実行の流れ

JavaScriptコード)

    ↓

  字句解析

    ↓

 (トークン列)

    ↓

  構文解析

    ↓

 (抽象構文木

    ↓

  コンパイル

    ↓

 (実行可能コード)

    ↓

   実行

JavaScriptエンジンの実装によって異なる。

JavaScriptの実行は、最初にJavaScriptファイルを読み込んだとき以外にも、DOMイベントが発火し、イベントリスナが起動するときに起こる。

5.1 字句解析と構文解析

JavaScriptエンジンは、与えられたコードを何らかの実行可能な形式に変換(コンパイル)した上でJavaScriptに書かれた処理を実行する。

コンパイルを行うために、JavaScriptのコードを抽象構文木(英: abstract syntax tree、AST)と呼ばれるコンパイル可能な形に変換する。 JavaScriptの場合はJavaScriptオブジェクト(JSON)として表現される。

抽象構文木

構文構造をデータ構造に起こしたもの。JavaScriptの文法に沿った形で表現される木構造のデータ。

抽象構文木

{
  "range": [
    0,
    10
  ],
  "type": "Program",
  "body": [
    {
      "range": [
        0,
        10
      ],
      "type": "VariableDeclaration",
      "declarations": [
        {
          "range": [
            4,
            9
          ],
          "type": "VariableDeclarator",
          "id": {
            "range": [
              4,
              5
            ],
            "type": "Identifier",
            "name": "a"
          },
          "init": {
            "range": [
              8,
              9
            ],
            "type": "Literal",
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "module"
}

5.2 コンパイル

JavaScriptエンジン内部のコンパイラは、先ほど構築した抽象構文木を実行可能な形式にコンパイルする。

スクリプト言語の言語処理系の実装の方法

JavaScriptエンジンで多いのは、JIT(Just In Time)コンパイル型の実装

JIT(Just In Time)コンパイル

5.3 実行

実行可能な形式にコンパイルされたJavaScriptのコードは、処理系内部の仮想マシン、もしくはCPUで実行される。

6. レイアウトツリー構築 - Rendering

JavaScriptの実行が終わると、レイアウトツリー構築(Rendering)が行われる。

具体的に以下

  • スタイルの計算
  • レイアウト

レイアウトツリー

ブラウザで DOM と CSSOM を組み合わせて、ページ上の表示可能なすべての DOM コンテンツと、各ノードのすべての CSSOM スタイル情報を取り込んだもの

6.1 スタイルの計算

DOMツリー内の全てのDOM要素に対して、どのようなCSSプロパティが当たるのか計算する。

  1. CSSOMツリー内を全て参照して、CSSルールのCSSセレクタマッチング処理が行われる。
  2. CSSルールの詳細度を算出して個別のDOM要素に対して、どのようなCSSプロパティが適用されるか判断

CSSルールのマッチング処理

CSSOMツリーからCSSルールセットを走査、DOMツリーからDOM要素を走査。 どんなCSSルールが適用されるのかを計算する。

CSS セレクタのマッチング

CSSセレクタのマッチング処理は右側から行われる。

body > .container > .button {
    ...
}
  1. DOM要素のclass属性にbutton
  2. 親要素のclass属性にcontainer
  3. 親要素のDOM要素名がbody

レンダリングエンジンはセレクタを右から左に解釈してマッチング処理を行う

どのDOM要素に対してどのCSSルールセットが適合するかレンダリングエンジンには分かるようになる。

CSSルールセット

div.my-button, button {
    background-color:  green;
    color: white;
}

{}部分

適用されるCSSプロパティの算出

DOM要素にどのCSSプロパティと値が適用されるのか算出する。

(margin, padding,positionの算出ではない)

詳細度の計算など

<p class="foo">
    foo
</p>

.foo {
 color: red;
}

p {
  color: blue;
}

.fooのスタイルが当たることの計算

6.2 レイアウト

DOM要素に当たるCSSプロパティを算出した後、レンダリングエンジンはDOMツリー内のすべてのノードの視覚的なレイアウト情報の計算、レイアウトを行う。

設計図的なもの

レイアウト情報

  • 要素の大きさ
  • 要素のmargin
  • 要素のpadding
  • 要素の位置

7. レンダリング結果の描画 -Painting

DOMツリーのレイアウト情報の算出が終わると、レンダリング結果の描画(Painting)

レンダリングエンジンはユーザーが見ることができる実際のピクセルを描画。

3つの処理が行われている。

  • ペイント(Paint)
  • ラスタライズ(Rasterize)
  • レイヤーの合成(Composite Layers)

最後のレイヤーの合成が終わることで、ユーザーの目にはレンダリングエンジンが描画した表示になる。

7.1 ペイント

  • 内部の低レベルな2Dグラフィックエンジン向けの命令を生成。

    • RenderTreeを元にDisplay Listと呼ばれる内部の低レベルグラフィックエンジンのための命令の列を生成。
  • 組み込まれるグラフィックエンジンはブラウザの実装ごとに異なる

    • WebKit 異なるグラフィックエンジンを組み込めるように設計されている。
    • Blink  Skiaが使われている 。
    • Safari CoreGraphicsが使われている

7.2 ラスタライズ

  • 生成された命令を用いて実際にピクセルに描画する。
  • レイヤーごとに一枚一枚描画される。
  • レイヤーが生成されるのは、 positiontransform ,opacityなどのプロパティが適用されているとき

レイヤー単位でピクセルに描画するのはなぜ? 再レンダリングする場合、すでに描画が終わったレイヤーを再利用することで、素早く再レンダリングできる場合がある。

7.3 レイヤーの合成

ピクセルにしたレイヤーを合成して最終的なレンダリング結果を生成する。

  • CSS基本的にはCPUによって合成される
  • transformCSSプロパティに3D変形関数を指定するとGPUによって合成される

CPU

コンピューター全体の計算処理

GPU

3Dグラフィックなどの画像描写に必要な計算処理

7.4 コンテンツの表示

レイヤーの合成の処理を終えてやっと

レンダリングエンジンがコンテンツを表示します!

8 再レンダリング

ユーザーやブラウザの何らかのアクションやJavaScriptのコードの実行やドキュメント内のイベントによってレンダリングは再度引き起こされる。

  • 全てのレンダリングの処理が最初からやり直しになるわけではない。
  • 多くのレンダリングエンジンは、これまで描画のために構築した内部表現のオブジェクトをなるべく再利用しようとする

レンダリングが引き起こされるのは?

  • DOMイベントが発火するとき





「ブラウザを立ち上げてアドレスバーにURLを打ち込んでEnter押してからページが表示されるまでに (裏側で) 何が起こっているかわかる限り説明してみてください。」

はじめは、この質問を何も見ずに15分考えると、ぼんやりとした流れを説明することしかできなかったのですが、ブラウザの仕組みを勉強したあとは3倍くらいの量できちんと説明できるようになりました◎





参考

Webフロントエンド ハイパフォーマンス チューニング

Webフロントエンド ハイパフォーマンス チューニング

  • 作者:久保田 光則
  • 発売日: 2017/05/26
  • メディア: 単行本(ソフトカバー)

Vue.jsのリアクティブシステムみたいなのを作ってみた。

Vue.jsの中身を読んでみる!というのを最近やってみていて、算出プロパティ部分を読んでいたのですが、 実際に同じようなコードが書けないかなあと思い、少し書いてみました。

Vue.js 算出プロパティ部分のコード

Vue.jsでの実現方法は多分こんな感じ

initDataobserveが呼ばれている

vue/state.js at dev · vuejs/vue · GitHub

function initData (vm: Component) {
  let data = vm.$options.data
  // ...
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  
  // observe data
  observe(data, true /* asRootData */)
}

observeObserverオブジェクトを生成

vue/index.js at dev · vuejs/vue · GitHub

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

ObserverクラスでdefineReactiveが呼ばれてる。

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
  // ...
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

defineReactiveObject.defineProperty を使用してリアクティブシステムを実現している...?

vue/index.js at dev · vuejs/vue · GitHub

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
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })



自分で同じような仕組み書いてみる

Object.definePropertyを使用しているみたいなので調べてみました。

Object.defineProperty

あるオブジェクトに新しいプロパティを直接定義したり、オブジェクトの既存のプロパティを変更したりして、そのオブジェクトを返します。

developer.mozilla.org

実際に書いてみる

<html>
  <body>
    <ul>
      <li>
        <p>テキスト</p>
        <p id="message"></p>
        <input type="text" id="text">
      </li>
      <li>
        <p>チェックボックス</p>
        <input type="checkbox" id="checkbox">
        <label for="checkbox" id="label-checkbox"></label>
      </li>
      <li>
        <p>セレクトボックス</p>
        <select>
          <option disabled value="">Please select one</option>
          <option>A</option>
          <option>B</option>
          <option>C</option>
        </select>
        <p id="selectedText"></p>
      </li>
    </ul>
  </body>
</html>[f:id:cidermitaina:20190128100626g:plain]
const data = {};

const p = document.getElementById('message');
const label = document.getElementById('label-checkbox');

const input = document.querySelectorAll('input');
const inputTypeText = document.getElementById('text');
const inputTypeCheckbox = document.getElementById('checkbox');
const select = document.querySelector('select');
const selectedText = document.getElementById('selectedText');

p.textContent = inputTypeText.value = 'Hello, World!';
label.textContent = inputTypeCheckbox.value = 'false';
selectedText.textContent = inputTypeCheckbox.value = 'selected';

// リアクティブプロパティの定義
Object.defineProperties(data, {
  message: {
    get() { return message; },
    set(newVal) {
      message = newVal;
      p.textContent = message;  
    }
  },
  checked: {
    get() { return checked; },
    set(newVal) {
      checked = newVal;
      label.textContent = checked;  
    }
  },
  selected: {
    get() { return selected; },
    set(newVal) {
      selected = newVal;
      selectedText.textContent = selected;  
    }
  },
});

input.forEach((el) => {
  el.addEventListener('input', (ev) => {
    data.message = ev.target.value;
    data.checked = ev.target.checked;
  });
});

select.addEventListener('change', (ev) => {
  data.selected = ev.target.value;
});

以下のようになります。

f:id:cidermitaina:20190128100626g:plain

Vue.jsの中身を読んでみる (算出プロパティ1)

お仕事でVue.jsを書くことが多いのですが、なんとなく中身どうなってるんだろうって思ったので中身を読んでみることにしました。(今年はコードリーディングが目標.です..!)

Vue.jsのコード全て読もうとすると挫折しちゃいそうなので、面白そうなところ、読めそうなところつまみ食いしてみることにしました。

今回は算出プロパティのコードを読んで見ました。



算出プロパティのコードを読む

算出プロパティとは

任意に処理を含めることのできるデータ

テンプレート内にjavascriptが使用できるが、テンプレート内に多くのロジックを詰め込むと、コードが肥大化、メンテナンスが難しくなる。

<div>
  {{ message.split('').reverse().join('') }}
</div>

複雑なロジックには算出プロパティを利用すべき

<div id="example">  
 <p>Original message: "{{ message }}"</p>  
 <p>Computed reversed message: "{{ reversedMessage }}"</p>  
</div>
var vm = new Vue ({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    reversedMessage: function () {
      return this.message.split('').reverse().join('')
    }
  }
})
console.log(vm.reversedMessage) // =\> 'olleH'  
vm.message = 'Goodbye'  
console.log(vm.reversedMessage) // =\> 'eybdooG'

算出プロパティとメソッドの違い

<p>Reversed message: "{{ reverseMessage() }}"</p>
methods: {
    reverseMessage: function () {
      return this.message.split('').reverse().join('')
    }
  }

↑上記コードで同じ結果になる。

違いは?

算出プロパティは依存関係にもとづきキャッシュされる

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

messageが変わらない限り、reversedMessageが呼び出されてもfunction()以下の処理は実行されず、前に算出したプロパティの値がそのまま出力される。 messageの値が変われば再び処理が走るが、そうでなければ何回呼び出されても計算が発生しないので、不要に処理が遅くなることがない。

じゃあmethodsっていつ使うの?

computed: {  
 now: function () {  
 return Date.now()  
 }  
}

Date.now()は二度と更新されない。 → いつアクセスしても同じ日時

methodsはメソッドなので、入力値が変わらなくても、呼び出されるたびに毎回処理が走る。

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



算出プロパティ付近のコード読んでみる

どうやって実装してるんだろう?

予想を立ててみた

  • dataの値が更新されたら再評価される
    • なんかObserverっぽい気がする。
    • Vueの本にメソッドを呼び出しているわけではないのに何かの処理をしている。これがVue.jsの心臓ともいえるリアクティブシステムです。みたいなこと書いてた気がする

Intersection Observer API - Web API | MDN

リアクティブの探求 — Vue.js

やってみたこと

vueのコード読んでみる。

observerディレクトリがある...!

vue/src/core/observer at dev · vuejs/vue · GitHub

index.js見てみる

vue/index.js at dev · vuejs/vue · GitHub

defineReactive関数見つけた。

Vueの公式に Object.definePropertyを使用して getter/setter に変換します って書いてた気がする

リアクティブの探求 — Vue.js

Object.defineProperty getter/setterある!

Object.defineProperty?

あるオブジェクトのプロパティを明示的に追加または変更することができます。

Object.defineProperty() - JavaScript | MDN

defineReactive関数読んでみる

const dep = new Dep()?

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.js読んでみる?

vue/dep.js at dev · vuejs/vue · GitHub







続きます...!

Hyperappについて調べてみた。

毎週何かのライブラリをコードリーディングするという目標を立てました◎

はじめてのコードリーディング、何にしようかと迷ったのですが🤔

Hyperappを読んでみることにしました。

Hyperappなら1KBだし、行数も400行くらい。

(コードリーディング初心者の私でも読めるかも...🤔!)

今回の記事はコードリーディング前に事前にHyperappについて調べたのでそのmemoです。



Hyperappとは?

Web アプリのフロントエンド用 JavaScript ライブラリ。React, Preact, Vue といった代表的なものよりもずっと小さく、1 KB という超軽量サイズ。他のライブラリに依存することなく使えて、さらにスピードもある

  • QiitaがReactからHyperappに移行
  • SHOWROOMの新規開発に採用されている

ReactやVueなどで使われているVirtual DOM,Lifecycle Eventsの概念とRedux や Vuexなどで使われているState Management(状態管理)の概念を、「最小限」で、「実用的」に、一つにまとめた(「独立している」)ライブラリ。

github.com



concept

  • Minimal(最小限)
  • Pragmatic(実践的)
  • Standalone(独立)



Hyperappの関数

h関数, app関数の2つだけです。

  • h関数:仮想DOMを生成する関数
  • app関数: Hyperappを利用したApplicationを実行する関数

h

h(name, props, children) - name {String}…「div」など、HTML上でのタグ名 - props {Object} … Elementに挿入されるattributes - children {String | Array} … 子要素

h("a", { href: "#" }, "next page")

// return object
// {
//   name: 'a',
//   props: {
//     href: '#'
//   },
//   children: 'next page'
// }

app

HyperappによるWebアプリケーションを起動する

app(state, actions, view, container)






コードリーディングはまだ途中なので、改めて記事にする予定。

Chrome DeveloperToolのconsoleの使い方

Developer Toolのconsoleを上手く使いこなせていなかったのでmemoです。

console.log()しか使ったことない人とかいませんか?

consoleパネルで使える Command Line API を覚えるとデバックが捗るので よく使うものをまとめてみました。



コマンドライン API とは?

Developer ToolのConsole 内で使える便利関数のことです。

$_

$_には最後に評価した式の結果が保存されています。

f:id:cidermitaina:20181208150101p:plain

$0 〜 $4

$0から$4にはElementsパネルで選択した要素が5つ保存される

f:id:cidermitaina:20181208150913g:plain

$() と $$()

$(selector)は、指定された CSS セレクターを含む最初の DOM 要素への参照を返します。この関数は、document.querySelector() 関数のエイリアスです。

$$(selector) は、指定された CSS セレクターに一致す

f:id:cidermitaina:20181208152035g:plain

dir(object)

dir(object) は、指定されたオブジェクトのすべてのプロパティをオブジェクト スタイルのリストで表示します。このメソッドは、コンソール API の console.dir() メソッドのエイリアスです。

f:id:cidermitaina:20181208160419g:plain

inspect()

inspect(object/function) は、指定された要素またはオブジェクトを適切なパネルで開いて選択します。

f:id:cidermitaina:20181208152532g:plain

getEventListeners()

getEventListeners(object) は、指定されたオブジェクトに登録されているイベント リスナーを返します。戻り値は、各登録済みタイプ("click"、"keydown" など)の配列が含まれているオブジェクトです。各配列のメンバーは、各タイプに登録されているリスナーを記述するオブジェクトです。

f:id:cidermitaina:20181208153700g:plain

monitorEvents() & unmonitorEvents()

monitorEvents()は要素とイベントを指定することで、その要素に対するイベントの発生をモニタリングすることができます。

f:id:cidermitaina:20181208162134g:plain







ChromeのDeveloper Toolを使ったデバック方法についてもう少しいろいろ書きたいな...


参考

developers.google.com