自学内容网 自学内容网

Web Worker 使用教程

前言

        JavaScript语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没有做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核CPU的出现,单线程带来了很大的不便,无法充分发挥计算机的能力。

        Web Worker的作用,就是为JavaScript创造多线程环境,允许主线程创建Worker线程,将一些任务分配给后者运行。在主线程运行的同时,Worker线程在后台运行,两者互不干扰。等到Worker线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集或高延迟的任务的任务,被Worker线程负担了,主线程(通常负责UI交互)就会很流畅,不会被阻塞或拖慢。

        Worker线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮,提交表单)打断。这样有利于响应主线程的通信。但是,这也造成了Worker比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭

        Web Worker有以下几个使用注意点:

(1)同源策略

       分配给 Worker 线程运行的脚本文件,必须与页面文档同源。

(2)DOM限制

        Worker线程所在的全局对象,与主线程不一样,无法读取主线程所在的网页的DOM对象,也无法使用document、window、parent这些对象。但是,Worker线程可以使用navigator对象和location对象

(3)通信联系

        Worker线程和主线程不在同一个上下文环境,它们不能直接通信,必须同个消息完成。

(4)脚本限制

        Worker线程不能执行alert()方法和confirm()方法,但可以使用XMLHttpRequest 对象发出Ajax请求。

(5)文件限制

        Worker线程无法读取本地文件,即不能打开本地的文件系统(file://),它所加载的脚本,必须来自网络。

基础用法

主线程

主线程采用new命令,调用Worker()构造函数,新建一个Worker线程

const worker = new Worker('work.js')

Worker()构造函数的参数是一个脚本文件,该文件就是Worker线程所要执行的任务。由于Worker不能读取本地文件,所以这个脚本必须来自网络。如果下载没有成功(比如404错误),Worker就会默默地失败。

然后,主线程调用worker.postMesage()方法,向Worker发消息

work.postMessage('我是主线程');
work.postMessage({method:'echo',args:['work']});

worker.postMesage()方法的参数,就是主线程传给Worker的数据。它可以是各种数据类型,包括二进制数据。

接着,主线程通过worker.onmessage指定监听函数,接受子线程发回来的消息。

worker.onmessage = function (event){
    console.log('Received Message'+event.data);
    doSomething();
}

function doSomething() {
     // 执行任务
    worker.postMessage('work done!')
}

上面代码中,事件对象的data属性可以获取Worker发来的数据

Worker完成任务以后,主线程就可以把它关掉

worker.terminate();

Worker线程

Worker线程内部需要一个监听函数,监听message事件

// 写法一
self.addEventListener('message', function(e) {
    this.postMessage('You said:'+e.data);
},false);

// 写法二
addEventListener('message', function(e) {
    postMessage('You said:'+e.data);
},false)

除了使用self.addEventListener()指定监听函数,也可以使用self.onmessage指定。监听函数的参数是一个事件对象,它的data属性包含主线程发来的数据。self.postMessage()方法向主线程发送消息。

根据主线程发来的数据,Worker线程可以调用不同的方法,下面是一个例子:

self.addEventListener('message', function (e) {
    const data = e.data
    switch (data.cmd) {
        case 'start':
            self.postMessage('WORKER STARTED: ' + data.msg);
            stop;
        case 'start':
            self.postMessage('WORKER StOPED: ' + data.msg);
            break;
        case 'start':
            self.postMessage('WORKER StOPED: ' + data.msg);
            self.close(); // Terminates the worker.
            break;
        default:
            self.postMessage('Unknown command: ' + data.msg);
    }

},false)

上面代码中,self.close()用于在 Worker 内部关闭自身。

Worker加在脚本

Worker内部如果要加在其他脚本,有一个专门的方法importScripts()

importScript('script1.js');

该方法可以同时加在多个脚本

importScripts('script1.js', 'script2.js');

错误处理

主线程可以监听Worker是否发生错误,如果发生错误,Worker会触发主线程的error事件

worker.onerror = function (event) {
    console.log([
    'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
  ].join(''))
}

// 或者
worker.addEventListener('error', function (event) {
  // ...
});

Worker 内部也可以监听error事件。

关闭Worker

使用完毕,为了节省系统资源,必须关闭 Worker。

// 主线程
worker.terminate();

// Worker 线程
self.close();

数据通信

前面说过,主线程与Worker之间的通信内容,可以是文本,也可以是对象。需要注意的是,这种通信时拷贝关系,即是传值而不是传址,Worker对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是先将通信内容串行化,然后把串行化的字符串发给Worker,后者再将它还原,后者再将它还原。

主线程与Worker之间也可以交换二进制数据,比如File、Blob、ArrayBuffer等类型,也可以在线程之间发送,下面是一个例子:

// 主线程
const uInt8Array = new Uint8Array(new ArrayBuffer(10));
for(const i = 0;i<uInt8Array.length;i++){
    uInt8Array[i] = i * 2;  //[0, 2, 4, 6, 8,...]
}
worker.postMessage(uInt8Array);

// Worker线程
self.onmessage = function (e){
    const uInt8Array = e.data;
    postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString());
    postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength);   
}

但是,拷贝方式发送二进制数据,会造成性能问题。比如,主线程向Worker发送了一个500MB文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做Transferable Objects,这使得主线程可以快速把数据交给Worker,对于影像、声音处理、3D运算等就非常方便了,不会产生性能负担。

如果要使用转移数据的控制权,就要使用下面的写法:

// Transferable Objects 格式
worker.postMessage(arrayBuffer,[arrayBuffer]);

// 例子
var ab = new ArrayBuffer(1);
worker.postMessage(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来处理。

const blob = new Bolb([document.querySelect('#worker').textContent]);
const url = window.URL.createObjectURL(blob);
const worker = new Worker(url)

worker.onmessage = function (e) {
    // e.data === 'some message'
}

上面代码中,先将嵌入网页的脚本代码,转成一个二进制对象,然后为这个二进制对象生成URL,再让Worker加载这个URL。这样就做到了,主线程和Worker的代码都在一个网页上。

实例1:Worker线程完成轮询

有时,浏览器需要沦胥服务器状态,以便第一时间得知状态改变。这个工作可以放在Worker里面。

function createWorker(f) {
    const blob = new Bolo(['(' + f.toString() +')()']);
    const url = window.URL.createObjectURL(blob);
    const worker = new Worker(url);
    return worker;
}


const pollingWorker = createWorker(function (e) {
    let cache;
    function compare(new,old) {...};
    
    setInterval(function() {
        fecth('/my-api-endpoint').then(function (res) {
            const data = res.json();
            if(!compare(data,cache)){
                cache = data;
                self.postMessage(data);
            }
        })
    },1000)
});

pollingWorker.onmessage = function () {
    //render data
}

pollingWorker.postMessage('init');

上面代码中,Worker每秒钟轮询一次数据,然后跟缓存做比较。如果不一致,就说明服务端有了新的变化,因此就要通知主线程。

实例2:Worker新建Worker

Worker线程内部还能再新建Worker线程(目前只有Firefox浏览器支持)。下面的例子将是一个计算密集的任务,分配10个Worker。

主线程代码如下:

const worker = new Worker('worker.js');
worker.onmessage = function (event) {
    document.getElementById('result').textContent = event.data;
}

Worker线程代码如下:

// worker.js

// settings

const num_workers = 10;
const items_per_worker = 100000;

// start the workers

let result = 0;
let pending_workers = nun_workers;
for(let i = 0; i < num_workers; i++){
    cosnt worker = new Worker('core.js');
    worker.postMessage(i*items_per_worker);
    worker.postMessage((i+1)* items_per_worker);
    worker.onmessage = storeResult;
}

// handle the results
function storeResult(event) {
    result += event.data;
    pedding_worker -= 1;
    if (pedding_worker  <= 0)
        postMessage(result);// finished!
}

上面代码中,Worker线程内部新建了10个Worker线程,并且依次访问10个Worker发送消息,告知了计算的起点和终点。计算任务脚本的代码如下:

// core.js
let start;
onmessage = getStart;

function getStart(event) {
    start = event.data
    onmessage = getEnd;
}

let end;
function getEnd(event) {
    end = event.data;
    onmessage = null;
    work();
}

function work(
    let result = 0;
    for(let i = start; i < end; i += 1) {
        // perform some complex calculation here
        result += 1;
    }
    postMessage(result);
    close();
)

实例2实现的即使,在Worker中开了10个Worker线程去计算,给每个线程传入的开始值与结束值:[0,1000000],[1000000,2000000],[2000000,3000000]...,每个worker线程计算完毕时都会通知主线程进行页面显示。但是在storeResult方法中进行了判断,只有pending_workers <= 0即计算完毕之后才会通知主线程进行显示。

API

主线程

浏览器原生提供Worker()构造函数,用来供主线程生产Worker线程。

const myWorker = new Worker(jsUrl, options);

Worker()构造函数,可以接受两个参数。第一个参数是脚本的网址(必须遵循同源政策),该参数是必须的,且只能加载JS脚本,否则会报错。第二个参数是配置对象,该对象可选。它的作用就是指定Worker的名称,用来区分多个Worker线程。

// 主线程
const myWorker = new Worker('worker.js',{name:'myWorker'});

// Worker 线程
self.name // myWorker

Worker()构造函数返回一个Worker线程对象,用来供主线程操作Worker,Worker线程对象的属性和方法如下:

  • Worker.onerror:指定error事件的监听函数
  • Worker.onmessage:指定message事件的监听函数,发送过来的数据再Event.data中
  • Worker.onmessageerror:指定messageerror事件的监听函数,发送的数据无法序列化成字符串时,会触发这个事件。
  • Worker.postMessage():向Worker线程发送消息
  • Worker.terminate():立即终止Worker线程

Worker线程

Web Worker有自己的全局对象,不是主线程的window,而是一个专门 未Worker定制额全局对象。因此定义在window上面的对象和方法不是全部都可以使用。

Worker线程有一些自己的全局属性和方法:

  • self.name:Worker的名字。该属性只读,由构造函数指定。
  • self.onmessage:指定message事件的监听函数。
  • self.onmessageerror:指定messageerror事件的监听函数。发送数据无法序列化成字符串时,会触发这个事件。
  • self.close():关闭Worker线程
  • self.postMessage():向产生这个Worker线程发送消息
  • self.importScript():加载JS脚本

原文地址:https://blog.csdn.net/weixin_46872121/article/details/142262456

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!