Web Worker で ででで 〇〇 Niigata.js #1
Web Workerで〇〇する話Niigata.js #1
自己紹介
ushiboy
プログラマ
SPAアプリ開発が多め
使う言語は JavaScript 8割、 pythonが 2割みたいな感じ
Web Worker ご存知ですか or おぼえてますか
Web Workerについて三行で
Webブラウザにマルチスレッドをもたらした。
Worker内だけの独自コンテキストをもつ。
Workerの外部とはメッセージングでやり取り。
ちょっと振り返り( JavaScriptの同期処理)
基本的にメインスレッド( UIスレッド)だけなので、注意が必要だった。
例 1 Busyなループfunction loopSync() { for (var i = 0; i < 1000000000; i++) { // busyなループ } console.log('finish!');}
document.getElementById('btn').addEventListener('click', function(evt) { loopSync(); // これが終わるまで何もかも待たされる}, false);
例 2 同期な HTTP通信document.getElementById('btn').addEventListener('click', function(evt) {
var xhr = new XMLHttpRequest(); xhr.open('GET', '/api/slow', false); // 第 3引数の asyncフラグをオフにして "同期 "通信に xhr.send(null); // レスポンスくるまで待たされる console.log(xhr.responseText);
}, false);
そして出てくるダイアログ
※ブラウザに寄ります
なので
ふつうは非同期な APIを使う。
setTimeout駆使したり
XMLHttpRequestは非同期で使う
Workerを使うと ...
function loopSync() { for (var i = 0; i < 1000000000; i++) { // busyなループ } console.log('finish!');}loopSync();
document.getElementById('btn').addEventListener('click', function(evt) { new Worker('worker.js');}, false);
重い部分を worker用にスクリプトごと分離
Workerを作って使う
これによって、ブラウザが怒らない
※繰り返しますが、ブラウザに寄ります
もうちょっと詳しく見ていきます
というわけで ...
Workerを使うためには
ワーカー用のスクリプトの URI渡してインスタンスを生成する
var worker = new Worker('worker.js');
このときの URIは同一生成ポリシーに従う必要がある。
メッセージのやりとり
Worker
UI
postMessageで送る。
onmessageで受け取る。
worker.postMessage('start');
worker.onmessage = function(evt) { // MessageEvent console.log(evt.data);};
(もうちょっと)メッセージのやりとり
Worker
UI
postMessageで送るデータは共有ではなくコピーされる。
onmessageで受けとったものはコピー。
worker.postMessage({ count: 1000 });
worker.onmessage = function(evt) { // MessageEvent console.log(evt.data);};
同一のインスタンスを共有せずに双方で複製。
(番外)メッセージのやりとり
postMessageで送るデータは共有ではなくコピーされる。
巨大なデータを送る場合にパフォーマンスを良くするため、
所有権の譲渡( Transferable Objects)という仕組みがある。
が、今回は省略 ...
参考https://developer.mozilla.org/ja/docs/Web/Guide/Performance/Using_web_workers#Passing_data_by_transferring_ownership_(transferable_objects)
Workerの中selfがグローバルスコープショートカット( UI側の window)
windowや documentなど触れないものがある
詳しくは https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Functions_and_classes_available_to_workers
外部スクリプトは importScriptsで読み込む
WorkerがさらにWorkerを生成とかもできる。self.onmessage = function(evt) { if (evt.data === 'start') { self.postMessage('fin'); } else if (evt.data === 'stop') { self.close(); }};
importScripts(‘foo.js’, ‘hoge.js’);
Workerの止め方
terminate呼ぶ。
ワーカー自身が内部から close。
worker.terminate();
self.close();
Worker内でのエラー
ErrorEventになるので onerrorで拾える。
worker.onerror = function(evt) { // ErrorEvent console.log(evt.message); // “Uncaught Error:/ (^o^) ”\ console.log(evt.lineno); // 12};
Workerの種類
Dedicated Worker : 今まで見てきたやつ
Shared Worker : 複数のスクリプトから共有できるやつ
Service Worker : オフラインアプリにするときに役に立つやつ
Chrome Worker : Firefox限定(アドオン用?)
Audio Worker : オーディオ処理用(らしい)
ブラウザの対応状況(デスクトップ系)
細かい機能レベルになるともっと分かれる模様
参考 https://developer.mozilla.org/ja/docs/Web/Guide/Performance/Using_web_workers#Browser_compatibility
Internet Explorer 10.0+
Firefox 3.5+
Chrome 4+
Opera 10.6+
Safari 4+
ちなみに polyfillがあるっぽい
が、そもそも polyfillでどうにかなるものではない ...
参考 : https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills#web-workers
ホントそれ
Web Worker使ってますか?
ちなみに私は ...
まったく使ってない\ (^o^)/
自分の対象範囲だと重い処理やるケースない ...
サーバーと JSON …でお話するのがほとんど
…このままではタイトル詐欺になってしまう
というわけで、ここから本題
ここでもう一度図をみてみる
じーっと見ていると ...
Worker
UI
Fluxと似てますね
※強引な気がするのは気のせいです。
というわけで
この辺りをまるっとワーカーに入れてみる
Web Workerで(雑に) Fluxするお題:カウンターアプリ
WARNING!!!!!
ここからコードが EcmaScript2015になります
UI側
function Counter(props) { const { count, plus, minus } = props; return ( <div> <div>{count}</div> <button onClick={plus}>+</button> <button onClick={minus}>-</button> </div> );}
Counterコンポーネントを用意
UI側
import React from 'react';class App extends React.Component {
constructor(props) { super(props); this.state = {}; this.worker = props.worker; this.worker.onmessage = evt => { this.setState(evt.data); }; } /** 右へ続く -> **/
/** 続き **/ render() { const { count } = this.state; return ( <Counter count={count} plus={this.handlePlus.bind(this)} minus={this.handleMinus.bind(this)} /> ); } /** 次のスライドへ続く **/
ControllerViewとして Appを用意
UI側
/** 前のスライドからの続き **/ handlePlus() { this.worker.postMessage({ type: 'UPDATE_COUNT', payload: { value: 1 } }); } /** 右へ続く -> **/
/** 続き **/ handleMinus() { this.worker.postMessage({ type: 'UPDATE_COUNT', payload: { value: -1 } }); }}
アプリケーションコンテナを用意
UI側
import { render } from 'react-dom';
render( <App worker={new Worker('back.js')} />, document.getElementById('app'));
アプリの起動部分
Worker側const state = { count: 0};
onmessage = evt => { postMessage(store(state, evt.data));};// initializepostMessage(state);
function store(state, action) { const { type, payload } = action; switch (type) { case 'UPDATE_COUNT': return updateCount(state, payload); default: return state; }}
function updateCount(state, payload) { const { value } = payload; state.count += value; return state;}
やってみた感じ
一方通行のデータフローの制限が自然に生まれる
メッセージングのデータは複製されるので、 UI側に渡すときに自分でステートをイミュータブルにしなくて良い
Immutable.jsとかでやらなくて良い
(雑すぎて Dispatcher 端折っちゃった)
同じようなこと考えてる人いるっぽい
参考 https://medium.com/@nsisodiya/flux-inside-web-workers-cc51fb463882#.8apt7vhfh
まとめ
Workerの中ならば必ずしも非同期じゃなくてもよい。
状況に応じて同期・非同期を選択できる。
Workerを積極的に取り入れたアーキテクチャ時代が来る(かもしれない)