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_objec
ts)
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));
};
// initialize
postMessage(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を積極的に取り入れたアーキテクチャ時代が来る(かもしれない)

Web Workerで○○する話