Web Worker
学习阮一峰老师的web worker而记录下来
一概述
JavaScript语言采用的是单线程,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核CPU的出现,单线程带来很大的不方便,无法充分发挥计算机的计算能力。Web worker的作用是为JavaScript创作多线程环境,允许主线程创建worker线程,将一些任务分配给后者运行。在主线程运行的同时,worker线程在后台运行,两者互不干扰。等到worker线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被worker线程负担了,主线程(通常负责UI交互)就会很流畅,不会被阻塞或拖慢。
Worker线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮,提交表单)打断。这样有利于随时响应主线程的通信。但是这也造成了worker比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭
Web worker有以下几个使用注意点
- 同源限制
分配给worker线程运行的脚本文件,必须与主线程的脚本文件同源。
- DOM限制
Worker线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的DOM对象,也无法使用document, window, parent这些对象。但是worker线程可以使用navigator, location(只读)对象
- 通信联系
Worker线程和主线程不再同一个上下文环境,他们不能直接通信,必须通过消息完成
- 脚本限制
Worker线程不能执行alert()方法confirm()方法,但可以使用XMLHttpRequest对象发出Ajax请求
- 文件限制
Worker线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本必须来自网络
二.基本用法
2.1主线程
主线程采用new命令,调用Worker()构造函数,新建一个Worker线程
Var worker = new Worker(‘worker.js’)
Worker()构造函数的参数是一个脚本文件,该文件就是Worker线程所要执行的任务。由于Worker不能读取本地文件,所以这个脚本必须来自网络。如果下载没有成功,Worker就会默默地失败。
然后主线程调用worker.postMassage()方法向Worker发消息
Worker.postMessage(‘hello world’)
Worker.postMessage({method: ‘echo’}, args: [‘Work’])
Worker.postMessage()方法地参数,就是主线程传给Worker地数据,可以是各种数据类型包括二进制
接着主线程通过worker.onmessage指定监听函数,接收子线程发回来地消息
Worker.onmessage = functioin (event) {
Console.log(event.data)
DoSomething()
}
Function doSomething(){
//执行任务
Worker.postMessage(‘work done!’)
}
上面代码中,事件对象地data熟悉可以获取worker发来地数据,worker完成任务以后主线程就可以把它关掉
Worker.terminate()
2.2Worker线程
Worker线程内部需要有一个监听函数,监听message事件
Self.addEventListener(‘message’, function (e) {
Self.postMessage(‘you said:’ + e.data)
}, false)
上面代码中,self代表子线程自身,即子线程地全局对象,因此等同于下面两种写法
//写法一
This.addEventListerer(‘message’, function (e) {
This.postMessage(‘you said:’ + e.data)
}, false)
AddEventListener(‘message’, function(e) {
PostMessage(‘you said:’ + e.data)
},false)
除了使用addEventListener()指定监听函数,也可以使用self.onmessage指定。监听函数地参数是一个事件对象。它地data属性包含主线程发来地数据。Self.postMessage()方法用来向主线程发送消息
根据主线程发来地数据,worker线程可以调用不同地方法
Self.addEventListener(‘message’,function(e){
Var data = e.data
Switch(data.cmd) {
Case ‘start’:
Self.postMessage(‘worker started:’ + data.msg)
Break;
Case ‘stop’:
Self.postMessage(‘worker stoped:’ + data.msg)
Self.close()
Break;
Default:
Self.postMessa(‘unknown:’ + data.msg)
}
},false)
Self.close()用于再worker内部关闭自身
2.3worker加载脚本
Worker内部如果要加载其它脚本,有一个专门的方法importScript()
ImportScript(‘script.js’)
加载多个脚本
ImportScript(‘script1.js’,’script2.js’,’script3.js’)
2.4错误处理
主线程可以监听worker是否发生错误。如果发生错误worker会触发主线程的error事件
Worker.onerror(function (event) {
Console.log([
‘error:line’, event.loneno,’ in ’, event.filename, ‘ : ’,event.message
].join(‘’))
})
Worker.addEventListener(‘error’, function (event){
})
Worker内部也可以监听error事件
2.5关闭worker
使用完毕,为了节省系统资源,必须关闭worker
//主线程
Worker.terminate()
//worker线程
Self.close()
三数据通信
前面说过,主线程与worker之间的通信内容,可以是文本,也可以是对象。需要注意的是,这种通信是拷贝关系,即是传值而不是传地址。Worker对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是先将通信内容串行化,然后把串行化后的字符串发给worker,后者再将它还原
主线程与worker之间也可以交换二进制数据,比如file, blob, arrayBuffer等类型。也可以在线程之间发送。
//主线程
Var uInt8Array = new Uint8Array(new ArrayBuffer(10))
For (var I = 0l I < uInt8Array.length; ++i) {
uInt8Array[i] = I * 2 // [0,2,4,6,7,…….]
}
Worker.postMessage(uInt8Array)
//worker线程
Self.onmessage = function (e) {
Var uInt8Array = e.data
PostMessage(uInt8Array.toString())
PostMessage(uInt8Array.byteLength)
}
但是,拷贝方式发送二进制数据会造成性能问题。比如主线程向worker发送一个500MB文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦。这种转移数据的方法叫做Transferable Objects。这使得主线程可以快熟把数据交给worker,对于影像处理,声音处理,3D运算等就非常方便了,不会产生性能负担
如果要直接转移数据的控制权,就要使用下面的写法
// Transferable Objects 格式
Worker.postMessage(arrayBuffer,[arrayBuffer])
//例子
Var ab = new ArrayBuffer(1)
Worker.posrMessage(ab,[ab])
四同页面的web worker
通常情况下,worker载入的是一个单独的JavaScript脚本文件,但是也可以载入与主线程在同一个网页的代码
<!DOCTYPE html>
<body>
<script id=”worker” type=”app/worker”>
AddEventListener(‘message’, function () {
PostMessage(‘some message’)
}, false)
</script>
</body>
</html>
上面是一段嵌入网页的脚本,注意必须指定<script标签的type属性是一个浏览器不认识的值,上例子是app/worker
然后读取这一段嵌入页面的脚本用worker来处理
Var blob = new Blob([document.querySeletor(‘#worker’).textContent])
Var url = window.URL.createObjectURL(blob)
Var worker = new Worker(url)
Worker.onmessage = function (e) {
//e.data === ‘some message’
}
上面代码中,先将嵌入网页的脚本代码,专程一个二进制对象,然后为这个二进制对象生成URL.再让worker加载这个URL.这样就做到了,主线程与worker的带都再同一个网页上面
五实例:worker线程完成轮询
有时候浏览器需要轮询服务器状态,以便第一时间得知状态改变。这个工资可以放在worker里面
Function createWorker(f){
Var blob = new Blob([‘(’ + f.toString() + ‘)()’])
Var url = window.URL.createObjectURL(blob)
Var worker = new Worker(url)
Return worker
}
Var pollingWorker = createWorker (function(e){
Var cache
Function compare (new, old) {…}
SetIntval(function () {
Fetch(‘/my-api-endpoint’).then(function (res){
Var data = res.json()
If (!compare(data,cache)){
Cache = data
Self.postMessage(data)
}
})
},1000)
})
PollingWorker.onmessage = function () {
// render data
}
PollingWorker.postMessage(‘int’)
上面代码中,worker每秒钟轮询一次数据,然后跟缓存做比较。如果不一致说明服务端有了新的变化,因此就要通知主线程
六实例:worker新建worker
Worker线程内部还能再新城worker线程(目前只有firefox浏览器支持)。下面的例子是将一个计算密集的任务分配到10个worker
主线程:
Var worker = new Worker(‘worker.js’)
Worker.onmssage = function(event){
Document.getElementById(‘result’).textContent = event.data
}
Worker线程代码如下:
// worker.js
//settings
Var num_worker = 10
Var items_per_worker = 1000000
//start the workers
Var result = 0
Var pending_workers = num_workers
For (var I = 0; I < num_workers; i+=1) {
Var worker = new Worker(‘core.js’)
Worker.postMessage(I * items_worker)
Worker.postMessage((i+1) * items_per_worker)
Worker.onmessage = storeResult
}
//handle the results
Function storeResult (event) {
Result += event.data
Pending_worker -=1
If (pending_worker <= 0) {
PostMessage(result) // finished!
}
}
上面代码中,worker线程内部新建10个worker线程,并且依次向这10个worker发送消息,告知了计算的七点和终点。计算任务脚本的代码如下
//core.js
Var start
Onmessage = getStart
Function getStart(event){
Start = event.data
Onmessage = getEnd
}
Var end
Function getEnd(event){
End = event.data
Onmessage = null
Work()
}
Function work() {
Var result = 0
For (var I = start; I < end; I += 1) {
//perform some complex calculation here
Result += 1
}
PostMessage(result)
Close()
}
七 API
7.1主线程
浏览器原生提供winker()构造函数,用来供主线程生成worker线程
Var myWorker = new Worker(jsURL,options)
Worker()构造函数,可以接受两个参数。第一个参数是脚本的网址(必须遵守同源策略),该参数是必须的,且只能加载JS脚本。第二个参数是配置对象,该对象可选。它的一个作用就是指定worker的名称,用来区分多个worker线程
// 主线程
Var myWorker = new Worker(‘worker.js’, {name: ‘myWorker’})
//worker线程
Worker()构造函数返回一个worker线程对象,用来供主线程操作worker。Worker线程对象的属性和方法如下
Worker.onerror:指定error事件的监听函数
Worker.onmessa:指定messa事件的监听函数,发怂过来的数据再Event.data属性中
Worker.onmessageerror:指定messageerror事件的监听函数。发送的数据无法序列号称字符串时,会触发这个事件
Worker.postMessage():向worker线程发送消息
Worker.termitnate():立即终止worker线程
7.2worker线程
Web worker有自己的全局对象,不是主线程的window,而是一个专门为Worker定制的全局对象,因此定义再window上面的对象和方法不是全部都可以
Worker线程有一些自己的全局属性和方法
Self.nama: worker的名字,该属性只读,由构造函数指定
Self.onmessageerror:指定messageerror事件的监听函数。发送的树无法序列号成字符串时,会触发这个事件
Self.close():关闭worker线程
Self.postMessage():向产生这个worker线程发送消息
Self.importScript():加载JS脚本