【JavaScript】同時並行モデル
JavaScriptが動作する仕組み
JavaScriptで書かれたスクリプトがブラウザから読み込まれた時、ブラウザに組み込まれたレンダリングエンジンによってその内容が解釈され、実行されます。スクリプト上では変数が定義されたり、オブジェクトが生成されたり、関数呼び出しが行われたりします。このスクリプトを解釈するレンダリングエンジンは、どのような規則に則って処理を実行していくのでしょうか。
同時並行モデル
JavaScriptでは、同時並行モデルと呼ばれる実行モデルに従います。同時並行モデルは関数呼び出しを管理する「スタック」、処理の単位である「メッセージ」、そして関数やメッセージから生成・参照されるオブジェクトの格納場所である「ヒープ」からなります。MDNでは、このモデルを以下のような図で表現していました。*1
このモデルはJavaScriptランタイムをかなり簡潔に表現したものですが、大まかなJavaScriptの動きを把握したり、仕事などで議論する際には役に立ちそうです。
スタック
上記モデルにおけるスタックの役割は、呼び出された関数の順序と、各関数内における処理で必要な情報を保持することです。スクリプト上で関数が呼ばれるたびに、フレームと呼ばれる、ローカル変数と関数が必要とする引数を含むセットがスタックに積まれます。
呼び出された関数内でさらに関数が呼ばれる度、フレームがスタックの上から積まれていき、関数が終了あるいはreturn
することでフレームがポップされます。
function g(b){ return b + 1; // ③ } function f(a){ return g(a) + 1; // ② } // ① var result = f(3); // result : 5
実際のスクリプトとモデルの動きを照らし合わせてみます。最初、①で関数fが呼ばれ、スタックに一番目のフレームが積まれます。②の時点で関数fは処理を終了しますが、その前に関数gを呼び出します。このとき二番目のフレームもスタックに積まれます。関数gは③で引数bに+1をした値を返戻して終了するので、この時点で二番目のフレームがポップされます。再び②に戻り、関数fは関数gの返戻値にさらに1を加え返戻し、処理を終了します。ここで最後のフレームがポップされます。
関数スタックの概念は関数を扱う様々な言語の中で取り入れられていますが、JavaScriptも例外ではないようです。
キュー
キューの役割は、処理されるメッセージをキューイングすることです。JavaScriptの場合、スクリプト中の動的な処理だけでなく、ブラウザイベント等の非同期な処理も扱える必要があります。キューはそのような処理をメッセージと言う単位で格納しておき、順番に実行していきます。
メッセージの実行は、スタックが空になったタイミングで行われます。メッセージが実行されるとスタックフレームが開始され、関数呼び出しによってスタックにフレームがプッシュ/ポップされながら処理が進んでいきます。スタックが空になると処理は終了し、メッセージは破棄され、また次のメッセージの実行が開始されます。
ヒープ
ヒープは、メッセージ中の処理、すなわち関数内の処理で扱われるデータの置き場所となります。JavaScriptではデータを格納するために必要なヒープを動的に確保する一方で、不要になったデータは破棄し、ヒープの領域を確保しようとします。
// Dateオブジェクトの生成 // Dateオブジェクトを格納するのに必要なヒープ領域を確保する var date = new Date();
動的な領域の確保で必ず議論になるのが、不要になったデータを破棄する処理、すなわちガーベジコレクションについてです。ガーベジコレクションは誰からも利用されなくなったオブジェクトを探し出し、その名の通り「ごみ」として扱います。ごみとなったオブジェクトは実質破棄された状態となり、破棄されたオブジェクトが確保していた領域は開放されます。
このガーベジコレクションを実現するための手法はたくさんありますが、基本的なガーベジコレクションアルゴリズムとして挙げられるのは、決まって以下の二つです。
- Reference Counter
- Mark & Sweep
Reference Counter
オブジェクトに対する参照の数をガーベジコレクタがカウントし、参照が0となったオブジェクトを破棄の対象とするアルゴリズムです。
// objが参照するオブジェクトへの参照カウンタ:1 var obj = { a : "foo", b : { c: "bar" } }; // objが参照するオブジェクトへの参照カウンタ:2 var obj2 = obj; var obj_a = obj.a; var obj_b = obj.b; var obj_b_c = obj.b.c; // ① objが参照するオブジェクトへの参照カウンタ:0 obj = null; obj2 = null; // ② obj_a = null; obj_b = null; // ③ obj_b_c = null;
注意しておきたいのが、オブジェクトへの参照が0になってもプロパティへの参照があれば、そのオブジェクトはガーベジコレクションの対象とはならない点です。例えば上記コードでは①で変数obj``obj2
の参照先をnullとしており、obj
に格納されていたオブジェクト(便利のためO
と表記します)を参照する変数がない状態です。しかし、変数obj_a
がオブジェクトのプロパティa
を参照しているため、O
はガーベジコレクションの対象にはなりません。同様に、②でobj.a``obj.b
への参照を0としていますが、obj.b.c
への参照があるため、この段階でも対象となることはありません。③でobj.b.c
への参照を無くして初めて、O
はコレクションの対象となります。
また、Reference Pointerアルゴリズムでは、参照のサイクルを構成されるとコレクト不可能になる問題があります。参照のサイクルとは、以下のようにあるオブジェクトの参照先が自身のオブジェクトを参照しているような場合を指します。
var obj = {}; var obj2 = {}; obj.a = obj2; obj2.a = obj; obj = null; obj2 = null;
この場合、常に互いが参照し合う状態になるため、どちらも参照カウンタが(少なくとも)1の状態になり、ガーベジコレクションの対象になりません。
Mark & Sweep
JavaScriptにおけるMark & Sweepアルゴリズムでは、rootと呼ばれるトップレベルのオブジェクトから参照可能なオブジェクトを次々に辿っていき、参照可の印をつけていきます(この処理をトラバースと言います)。トラバースは複数回実行され、印がつかなくなるまで実行されます。ヒープ領域に存在するにも関わらず参照されないオブジェクトには印がつけられず、そのようなオブジェクトはガーベジコレクションの対象となります。
rootオブジェクトからの参照可能性を検証するため、上記のような参照サイクルが原因でヒープを開放できない問題を回避できます。また、参照カウントの仕組みを持たないため、ガーベジコレクションが動かない間はReference Pointerよりも動作が高速になります。
まとめ
今回はJavaScriptの動作モデルと、ヒープ領域に関連してガーベジコレクションについて少し掘り下げました。JavaScriptがレンダリングエンジン内で動作する仕組みの詳細は今回扱ったモデルよりずっと複雑みたいですが、興味はあるので後日また調べてみたいと思います。
参考文献
Concurrency model and Event Loop - JavaScript | MDN
Memory Management - JavaScript | MDN
*1:掲載した図はMDNを参考に独自に作成したものです
【JavaScript】Generator
pythonなどのように、JavaScriptでもES6からGeneratorが使えるようです。
Generatorとは
呼び出すたびに値やオブジェクトを生成して返す仕組み。
Generatorを使うことで、オブジェクトや配列値の遅延評価を行うことができます。
遅延評価とは、必要なタイミングになるまで値の評価を遅延させることです。
この遅延評価は協力で、例えば無駄にオブジェクトをプールさせることなく、必要なときにジェネレータを呼び出してオブジェクトを生成、といったことができるようになります。
Generatorの使い方
Generatorを使って実現可能な概念の一つに、無限数列があります。
今回は要素が0を含む正の整数からなる無限配列を例として作ってみました。
function *infinityArrayGenerator(){ var i = 0; while (true) { yield i++; } } var infinityArray = infinityArrayGenerator(); console.log(infinityArray.next().value); // 0 console.log(infinityArray.next().value); // 1 console.log(infinityArray.next().value); // 2 console.log(infinityArray.next().value); // 3
Generatorの生成
function *infinityArrayGenerator(){ var i = 0; while (true) { yield i++; } }
infinityArrayGenerator
はジェネレータです。
関数名の頭に*(アスタリスク)を付加することで、関数がGeneratorであることを表します。
Generatorではyield
キーワードを用います。Generatorではこのyieldが呼ばれた行で一旦値を評価して返し、Generatorが再度呼び出されるとこの地点から再び処理が再開されます。
var infinityArray = infinityArrayGenerator(); console.log(infinityArray.next().value); // 0 console.log(infinityArray.next().value); // 1 console.log(infinityArray.next().value); // 2 console.log(infinityArray.next().value); // 3
上のコードではinfinityArray
にジェネレータオブジェクトを格納し、
next()
を呼ぶことでGeneratorに値の「生成」を行わせています。
next()
を呼ぶたびにinfinityArray
内ではyield
時点で処理が一旦中断され、その時点でのi
の値が返されます。
なぜGeneratorを使うのか
先程も述べましたが、Generatorの強みは遅延評価が可能な点にあります。
上記の例ではnext()
を読んで初めて整数の生成が行われています。逆に言うと、next()
が呼ばれるまでは値が生成されていません。infinityArray
は無限の整数列として振る舞うことができるにもかかわらず、それに必要なメモリ量を(現時点では)食いつぶしてはいません。
このように、「必要なときに必要なだけ」値・オブジェクトの生成を行ってくれるのがGeneratorの強みです。
(厳密にはメモリ容量もストレージ容量も有限なので、infinityArrayは無限になりませんが・・・)
おまけ
nextの引数に値を渡すと、渡した値をyield式の返戻値とすることができます。
function *infinityArrayGenerator(){ var i = 0; while (true) { // nextに値を設定すればiに+100、デフォルトで+1 var value = yield i ; i += value || 1; } } var infinityArray = infinityArrayGenerator(); console.log(infinityArray.next().value); // 0 console.log(infinityArray.next().value); // 1 console.log(infinityArray.next(100).value); // 101 console.log(infinityArray.next().value); // 102
参考文献
【JavaScript】Web Worker
Web Workerとは
UIスレッドとは別のスレッドに処理を移行し、バックグラウンドで実行するための仕組み。
UIスレッドの描画を阻害することなく、実行時間を要するタスクを処理することができます。
Web Workerの使い方
参考文献を頼りに、Web Workerを使ってメインスレッドと別スレッド(以下ワーカースレッド)間でメッセージを送り合う処理を書きました。
まずは全体のソースから。
index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> <meta name="viewport" content="width=device-width" /> <title>Web Worker</title> </head> <body> <h1>Hello, Web Worker.<h1> <script src="script.js"></script> </body> </html>
script.js
'use strict'; // // メインスレッド上の処理 // // ブラウザがWeb Workerをサポートしている場合のみ処理実行 if (window.Worker){ // Workerオブジェクト let worker = new Worker('worker.js'); // ワーカースレッド上で非同期に実行されるイベントハンドラ // ワーカースレッド上のWorker::postMessage()で発火する worker.addEventListener('message', function(e){ console.log(e.data); }, false); // doWork.jsで登録したイベントハンドラが発火する worker.postMessage('Hello, Web Worker!'); }
worker.js
// // ワーカースレッド上の処理 // // ワーカースレッド上で非同期に実行されるイベントハンドラ // メインスレッド上のWorker::postMessage()で発火する self.addEventListener('message', function(e){ self.postMessage(e.data); });
Workerの生成
// script.js let worker = new Worker('worker.js');
new Worker('script.js')
でスクリプト'script.js'内の処理を実行するワーカーオブジェクトを生成します。
(script.js
は同一生成元ポリシに従うので注意)
メインスレッドからpostMessage()された時の処理の登録
// worker.js self.addEventListener('message', function(e){ self.postMessage(e.data); });
Main→Worker でメッセージが来たときの処理をaddEventListenr
に登録します。
ちなみに、このイベントハンドラは以下のようにしても登録することができます(が、Googleのドキュメントでは上記を推奨しています)。
onmessage = function(e) { self.postMessage(e.data); };
ワーカースレッドからpostMessage()された時の処理の登録
// script.js worker.addEventListener('message', function(e){ console.log(e.data); }, false);
Worker→ でメッセージが来たときの処理をaddEventListenr
に登録します。
今回は返ってきたメッセージをコンソール出力するだけ。
メインスレッドからワーカースレッドへメッセージ送信
// script.js worker.postMessage('Hello, Web Worker!');
この処理によってworker.jsで登録したイベントハンドラ→script.jsで登録したイベントハンドラの順に処理が走ります。
上手くいきました。
Web Workerの特徴
直接DOM操作はできない
Web Workerは直接のDOM操作が許可されていません。ワーカースレッドがどうしてもDOM操作を行いたい場合、メインスレッドにDOMへのアクセスをお願いするなど、一工夫必要になります。
window
オブジェクトの一部プロパティを使用できない
Worker自体が通常のスクリプトとは異なるグローバルコンテキストから呼び出されるため、普通のスクリプトのようにwindow
オブジェクトを参照できません(DedicatedWorkerGlobalScope と呼ばれるコンテキストで実行されます)。
Web Workerの種類
Web Workerにも幾つか種類があります。
Dedicated Worker
今回例として紹介したWorker。呼び出し元のスクリプトのみが参照できます。
複数のスクリプトから一つのWorkerオブジェクトを参照したい場合、次項のShared Workerを利用する必要があります。
Shared Worker
異なるwindow
をグローバルコンテキストとする複数のスクリプトから利用できるWorker。ただし、各スクリプトは同一ドメイン内にある必要があります。
ServiceWorker
オフライン上でのユーザ体験向上を実現するための特殊なWorker。複数アプリ間・ブラウザ間・ネットワーク間のプロキシ機能を提供します。例えばネットワークのオンライン・オフラインの状況によって、クライアントに返戻するリクエスト結果を変えたりすることができるほか、プッシュ通知やバックグラウンド同期のAPIも利用することができます。Googleの提唱するPWAで用いられるWorkerでもあります。
Chrome Worker
Firefoxがアドオン開発時にWebWorkerを利用したいとき・js-ctypesを使いたいときに利用するWorker。
Audio Worker
音声処理をWorker内で実行したい時に使います。
参考文献
https://www.html5rocks.com/ja/tutorials/workers/basics/
Web Workers API - Web API インターフェイス | MDN
【PWA】App Shellを作成する
前回の記事の続きです。
App Shellの作成
Google Developersの公式チュートリアルより、App Shellを実際に作ってみます。
App Shell の作成で気をつけること
UIとアプリ基盤をデータから切り離す
App Shellの強みは、コンテンツだけをネットワーク越しにやり取りし、UIとアプリ基盤をキャッシュすることで遅延のないアプリケーションを作成できることです。そのため、予めUI+アプリ基盤と、それに含まれるデータが予め分離されるようにアプリを構築する必要があります。
初回の読み込みはシンプルに
ページ遷移前処理をなるべく簡潔にして、初回読み込みを素早く実行させるべきです。アプリが開かれたときにはすぐにページのレイアウトだけでも表示させることで、ユーザの体感を向上させることができます。
【PWA】App Shell
前日の記事「PWAとは」の続きになります。
App Shellとは
App ShellはPWA(Progressive Web App)を構築する際に使われるモデルの一つです。
UI部品のみからなる「シェル」をコンテンツから分離してキャッシュすることで、アプリケーションをオフライン環境下でも快適に動作させることができます。
シェルはHTML、CSS、JavaScriptからなりますが、先述の通りテキストや画像などのデータに該当する部分を含みません。App Shell ではコンテンツのみをネットワーク経由で読み込み、シェルを積極的にキャッシュしていきます。
App Shellの活かしどころ
App Shellモデルが特に力を発揮するのは、SPA(Single Page Application)です。
SPAは一つの画面をサーバから取得したあとは、画面内コンテンツを中心にサーバから取得して画面更新を行うので
App Shell と相性が良く、既存のフレームワークで構築されたSPAやそれに基づくサイトはApp Shellモデルに基づいたアプリケーションやサイトに置き換え可能だと思われます。
現在多くのフロントエンドフレームワークがSPAを採用しているので、App Shellに置き換えられるアプリも多いと考えられます。
SPAを採用するFEフレームワーク
- Angular
- React
- Vue.js
- Ember.js
- Meteor.js
App Shellのメリット
パフォーマンス
初回のアクセスでシェルがキャッシュされるため、再アクセス時にページを即座に読み込めます。またコンテンツに関しても、キャッシュが効く可能性があるので読み込みはさらに高速になります。
ネイティブアプリライクな操作性
オフライン状態でも動作するWebアプリを構築可能なモデルなので、 ネイティブアプリのような操作性を得ることが可能です。
データ使用量の効率化
UIのキャッシュにより、ネットワーク間を行き来するデータ量を削減できます。
App Shellの要件
公式によると、App Shellには以下の要件が求められるそうです。
- 読み込みが瞬時に行われること
- データは最小限であること
- 静的アセットはローカルのキャッシュを用いること
- コンテンツとナビゲーションは分離されていること
- ページ固有のコンテンツの取得と表示が実装されていること
- 必要に応じ、動的コンテンツのキャッシュを行うこと
これらの項目を満たすことで、パフォーマンスの高いApp Shell、ひいてはPWAを提供することができるそうです。
パフォーマンスの測定には、Googleが提供するWebページパフォーマンステストツールLighthouseで検証できます。
Lighthouseを使う
Chromeで使うことを前提とします。
ChromeウェブストアからLighthouseをインストール。
検証したいサイトに移動し、バーに追加されたアイコンをクリック→「Create Report」を押す。
すると検証がスタートし、しばらくすると結果が表示されます。
世知辛い。
点数低いと凹みますが、App Shell準拠かどうかに限らず、
Webサイト作成に非常に役に立つツールなので積極的に使っていこうと思います。
次回あたりには実際にApp Shellのモデルに則ったアプリ作成をしてみたいと思います。
参考文献
PWAとは
ちょっと乗り遅れた感じありますが、PWAのメモ。
PWAとは
Progressive Web Applicationの略で、2016年にGoogleが提唱しました。
Webブラウザ上のアプリをあたかもネイティブのアプリのように動作させることで、
より良いユーザ体験を提供しようというものです。
例
twitterをAndroid上のChromeで開き、メニューの「ホーム画面に追加」をタップすると、
普通のネイティブアプリのようにPWA製「Twitter Lite」がインストールされます。
このように、PWAはストアを介さず軽快にインストールできるという特徴を持ちます。
なぜPWAが良いのか
PWAが推進される理由として、以下のような特徴が挙げられます。
段階的(Progressive)
プログレッシブ・エンハンスメントという考え方があります。
最先端のブラウザに合わせた高級な実装をデフォルトとしつつ、一部機能をサポートしていないブラウザなどにもコンテンツが損なわれないような実装を行い、サポートされていくに従ってよりリッチな実装にしていく・・・というものです。
PWAはこの考え方を踏襲しており、全てのユーザに利用してもらえるアプリケーション作成を意識しています。
レスポンシブ
今でこそ当たり前ですが、PWAもブラウザ・デスクトップ・モバイル等どんな環境でも最適なUI/UXを提供することを念頭においています。
ネットワーク接続状況から独立した動作
後述のService Workerがローカルで動くプロキシのような役割をすることで、オフラインやネットワーク環境の良くないところでもユーザ体験を損なうことなく動作します。
ネイティブアプリ感覚
あたかもネイティブアプリのように操作できます。
これは後述のApp Shellモデルに基づく実装によって実現します。
最新の状態を維持
後述のService Workerの更新プロセスによって、
コンテンツの状態は常に最新に保たれます。
安全
HTTPS経由の通信のみ許可します。そのため、経路上での盗聴に対して頑強です。
検索が容易
プログレッシブ・エンハンスメントに従っていること、後述のService Workerによる登録スコープによって、検索エンジン上で検索対象とすることができます。
再エンゲージメント可能
プッシュ通知のような機能を実装可能で、それを通じてユーザにアプリを使用して貰う機会を増やすことができます。
インストール可能
ストアを介さず、ユーザは気に入ったPWAをそのままローカルに保持しておくことができます。
リンク可能
あくまでWeb上のアプリなので、URLで簡単に共有できます。
PWAを構成する概念
App Shellモデル
App ShellモデルはPWAを構築する手法です。
ユーザインタフェースが機能するために必要な最低限のHTML,CSS,JavaScriptをまとめて「シェル」とし、シェルをキャッシュすることでオフラインでのアプリ利用を可能にします。
シェルはコンテンツ、すなわち文章や画像を含みません。あくまでシェルはUIの骨格のみを含み、データは保持しません。従って、ネットワークでやり取りされるのはデータの部分のみとなり、高いパフォーマンスでアプリを動作させることができます。
Service Worker
PWAのバックグラウンドで実行されるスクリプトで、Webページやユーザのインタラクションを必要としない機能を提供します。 現在、Service Workerはプッシュ通知とバックグラウンド同期をサポートしています。また以下のような特徴を持ちます。
プレゼンテーションのDOMに直接アクセス不可能。
プログラム可能なネットワークプロキシである。ページからのネットワークリクエスト/レスポンスをキャッシュしたりできる。
Webページとは異なるライフサイクルで動作する。 アプリが終了してもバックグラウンドで動き続ける場合がある。
JavaScriptのPromiseを多用する。
このService Workerのお陰で、アプリがオフラインでも快適に動作することを可能になり、
コンテンツを自動更新し、プッシュ通知を実装することが可能になるようです。
後日各構成要素について掘り下げる予定。
間違っている箇所があればご指摘頂けると幸いです。
参考文献
はじめてのプログレッシブ ウェブアプリ | Web | Google Developers
【AngularJS】ng-classで動的にhtmlタグのclassを変更する
ng-class
AngularJSではHTMLタグに付加するクラスを動的に変化させるng-class
ディレクティブが用意されています。
タグに割り当てるクラスを状態や条件で変更できるので、非常に便利です。
使い方
<div ng-class=" <割り当てるクラス名1> : <条件1> , <割り当てるクラス名2> : <条件2> , ... "></div>
ng-class
で割り当てたいクラスと条件は上記のように複数指定できます。
例えば、チェックボックスが有効かどうかを真偽値で返すisBoxChecked()
がfalseのときはdisabled
をつけたい場合は
<div ng-class=" disabled : isBoxChecked() "></div>
となります。