SharedWorker について、仕様にある例は大きすぎて何が起こってるのかつかみにくいので、簡単な例を載せてはどうか、というメールがあったので紹介。
僕も SharedWorker は例が面倒なので今日までちゃんと読んだことがなかった。
簡単な順に3段階。
step 1
- test.html
<pre id="log">Log:</pre> <script> var worker = new SharedWorker('test.js'); var log = document.getElementById('log'); worker.port.onmessage = function(e) { // note: not worker.onmessage! log.textContent += '\n' + e.data; } </script>
- test.js
onconnect = function(e) { var port = e.ports[0]; port.postMessage('hello'); }
test.html を開くと test.js が SharedWorker として呼ばれる。
普通の Worker と違って worker.port.onmessage
で通信するらしい。
test.js のほうで var port = e.ports[0]
とやっているのは、MessageChannel のほうに詳しく書いた。
ただしそこは var port = e.target
でもいいらしい。
step 2
- test.html
<pre id="log">Log:</pre> <script> var worker = new SharedWorker('test.js'); var log = document.getElementById('log'); worker.port.addEventListener('message', function(e) { log.textContent += '\n' + e.data; }, false); worker.port.start(); // note: need this when using addEventListener worker.port.postMessage('ping'); </script>
- test.js
onconnect = function(e) { var port = e.ports[0]; port.postMessage('hello'); port.onmessage = function(e) { port.postMessage('pong'); // not e.ports[0].postMessage! } }
worker.port.onmessage をセットすると暗黙の了解で Worker スレッドを走らせてくれるけど、addEventListener('message',..) の場合は start() を明示的に呼んでやらないといけない。
The first time a MessagePort object's onmessage IDL attribute is set, the port's port message queue must be enabled, as if the start() method had been called.
http://www.w3.org/TR/html5/comms.html#messageport
step 3
- test.html
<pre id="log">Log:</pre> <script> var worker = new SharedWorker('test.js'); var log = document.getElementById('log'); worker.port.addEventListener('message', function(e) { log.textContent += '\n' + e.data; }, false); worker.port.start(); worker.port.postMessage('ping'); </script> <iframe src=other.html></iframe>
- other.html
<pre id=log>Inner log:</pre> <script> var worker = new SharedWorker('test.js'); var log = document.getElementById('log'); worker.port.onmessage = function(e) { log.textContent += '\n' + e.data; } </script>
- test.js
var i = 0; onconnect = function(e) { i++; var port = e.ports[0]; port.postMessage('hello, ' + i); port.onmessage = function(e) { port.postMessage('pong'); } }
test.html と other.html から同じ URL の SharedWorker が呼び出され、それらは共有される。どちらかを開いたままもう一方をリロードすると、数字が増えていくはず。
感想
何に使えるか。たしか Chrome のドキュメントのどこかで、Gmail のようなアプリケーションでタブを複数開くと、それぞれが独立にサーバーと通信して面倒なことになるので、セッションをまとめる役割で使えるとか読んだ気がする。
普通の Worker だったら単にグローバルに onmessage = function() ...
と書くところを、onconnect = function(e) { e.ports[0].onmessage = ...}
として port を自分で収集しないといけないらしい。大変気持ち悪い。
例えば step 3 の場合、test.html と other.html を両方開いた状態で片方をリロードすると、そのたびに var port = e.ports[0]
されて port.onmessage = function(e) { port.postMessage('pong'); }
が呼ばれることになるけど、そのぶんのメモリはいつ解放されるんだろう。たぶん全部の親ウィンドウを閉じたときに Worker ごと消える? だったら適当な間隔で port が繋がっているかを監視する必要があると思うんだけど、isConnected のようなプロパティは無いみたいだし、実際に送ってみないと相手が存在するか分からないようになっているっぽい。
Hixie さん曰く、上のケースでは適切に解放されるらしい。下のケースでは手動で解放しないといけないらしい。
別の例を考えてみる。以下のような Shared Worker Script があったとして、
var myPorts = []; onconnect = function(e) { var port = e.ports[0]; myPorts.push(port); port.onmessage = function(e) { // あるタブからメッセージを受け取ったら myPorts.forEach(function(p) { if (p !== port) p.postMessage(e.data); // 他のすべてのタブに同じメッセージを送る }); } }
複数の親タブのうちの一つがリロードされたとする。すると、myPorts がどんどん増えていくことになる。だけど、リロードされる前のタブと繋がっている port は明らかに不要なので、どこかのタイミングで消してあげたい。これを簡単にやる方法は、今のところないっぽい。
このへんは気が向いたらメールを投げてみようと思う。↓メールした。