理解Web Workers

一.Web Workers是什么

Web Workers是一种支持任务在后台线程运行的机制,没错,Web Worker提供了行为受限的多线程

虽然浏览器中js的执行环境仍然是单线程的,但多线程在浏览器中早就存在了,比如异步XHR就是一个行为受限的线程,它独立于主线程,通过内置的消息机制来通信,不存在一般多线程的并发问题

Web Workers提供一个独立于主线程的context,在这个context里可以使用受限的API(禁止操作DOM,可能无法访问Cache等等)。多线程最大的好处就是不会阻塞UI,可以用多线程来完成时耗巨大的任务(比如音频/视频解码,以及在此基础上的数据处理),甚至如果API允许的话,利用Web Workers就可以自行实现XHR

P.S.是否允许Web Worker访问Cache,目前(2015/11/29)还有争议,Chrome43和FF支持,IE和Safari支持性未知

一点废话:Google的Gears插件提出了Worker Pool API,它就是Web Workers的“原型”,最初希望能够增强浏览器的功能,比如支持离线浏览(离线访问缓存页面,重新上线后提交离线操作),但目前(2015/11/29)已经被弃用了

Google Gears API

The Google Gears API is no longer available. Thank you for your interest.

二.Web Workers有什么用

后台线程,最大的作用就是do stuff。支持后台线程意味着浏览器能够承担更多更重的任务了,可以把一部分原本由服务端承担的运算(比如排序,编码/解码,模版生成)交给客户端,以减轻服务端压力,而不会影响用户体验(等待服务端返回结果和等待本地另一个线程返回结果一样,都是等待,而且后者可能更快)

笔者认为Web Workers有以下适用场景:

  • 音频/视频解码

    如果尝试过audioContext.decodeAudioData之类的操作就会发现,我们迫切需要一个能“干重活”的后台线程

  • 图片预处理

    比如头像上传前的裁剪,甚至添加水印、拼合、添马赛克,如果在客户端能够完成,就能避免大量的临时文件传输

  • 排序等数据处理算法

    减轻服务器压力

  • 客户端模版

    比如markdown,或者服务端返回JSON,客户端拿到后交给后台线程解析并应用模版HTML生成页面,这些操作都由客户端完成的话,需要传输的东西就更少了

三.Web Workers怎么用

会说话的示例代码如下:

//---主页面
if (window.Worker) {
    var worker = new Worker('worker.js');
    var data = {a: 1, b: [1, 2, 3], c: 'string'};
    worker.postMessage(data);
    worker.onmessage = function(e) {
        console.log('main thread received data');
        console.log(e.data);

        // 接到消息立即停止worker,onerror将不会触发
        // worker.terminate();
        // terminate之后收不到后续消息,但post不报错
        // worker.postMessage(1);
    }
    worker.onerror = function(err) {
        console.log('main thread received err');
        console.log(err.message);
        // 阻止报错
        err.preventDefault();
    }
}
//---end 主页面

//---worker.js
// 可以引入其它依赖
// importScripts('lib.js');
// importScripts('a.js', 'b.js');
onmessage = function(e) {console.log(self); // 看看global变量身上有些什么
    var data = e.data;
    console.log('worker received data');
    console.log(data);

    var res = data;
    res.resolved = true;

    postMessage(res);

    setTimeout(function() {
        throw new Error('error occurs');

        // close,立即停止,相当于主线程中的worker.terminate()
        // close();
    }, 100);
};
//---end worker.js

注意

  1. worker的global context并不是window,而是selfself也提供一系列接口,包括self.JSONself.Mathself.console等等,最直观的区别是document对象没了,但locationnavigator还在

  2. Worker的原型是DedicatedWorkerGlobalScope,与SharedWGS不同,Worker是专用的,只有创建Worker的脚本才能访问自己创建的worker

  3. worker里不允许操作DOM,但支持WebSocket、IndexedDB、XHR等等,各浏览器支持性有差异,具体请查看Functions and classes available to Web Workers – Web APIs | MDN

    注意:XHR虽然可以用,但XHR对象的responseXMLchannel属性永远返回null

  4. 主线程和worker线程收发消息方式一致(postMessage发,onmessage/onerror收,数据从MessageEvent的data属性取),线程之间传递的是值copy,而不是共享引用

  5. IE10和FF支持在worker里new Worker,只要这些worker和父页面是同源的

  6. 支持性问题,建议做特性检测

    if (window.Worker) {
        // new Worker...
    }
    
  7. importScripts可以引入其它js文件,外部文件中的全局变量将被粘在self上,worker里可以直接引用。importScripts是同步的,下载并执行完毕后执行下一行

四.Shared Worker

上面我们关注的Worker属于dedicated worker(专用worker),只有创建worker对象的context有worker的访问权。而Shared Worker支持跨context的worker共享,比如window与iframe,iframe与iframe

由于它们具有共享的属性,可以保持一个应用程序在不同窗口内的相同状态,并且不同窗口的页面通过同一共享worker脚本保持和报告状态

用localStorage也可以实现状态同步,zxx前辈的例子:Page Visibility(网页可见性) API与登录同步引导页实例页面。有兴趣的话可以查看原文:Page Visibility(页面可见性) API介绍、微拓展

笔者能想到Shared Worker的唯一的适用场景可能就是实现应用状态同步机制了,虽然cookie、localStorage都能够存储状态,但Shared Worker能够实现一套复杂的状态控制机制。简言之,cookie和localStorage里只能放数据,Shared Worker里除了能放数据,还能执行代码

此外,Shared Worker好像没什么用了,毕竟能new Worker已经很不错了,锦上添花的东西作用不会让人诧异

示例代码如下:

//---主页面
if (window.SharedWorker) {
    var sworker = new SharedWorker('sharedworker.js');

    // 1.onmessage隐式调用start
    // sworker.port.onmessage = function(e) {
    //     console.log('main thread received data:');
    //     console.log(e.data);
    // }
    // 2.或者addEventListener再显式调用start
    sworker.port.addEventListener('message', function(e) {
        console.log('main thread received data:');
        console.log(e.data);
    });
    sworker.port.start();

    sworker.port.postMessage([1, 2, 3]);
}
//---end 主页面

//---sharedworker.js
var count = 0;

onconnect = function(e) {
    // 记录连接数
    count++;

    var port = e.ports[0];

    // 1.onmessage隐式调用start
    port.onmessage = function(e) {
        //!!! console.log失效了
        console.log('will not occur in console');

        port.postMessage({receivedData: e.data, count: count});

        //!!! 错误不会抛回给sharedworker创建者,静默失败
        //!!! 如果sharedworker本身有语法错误,也会静默失败,而且在这之前的postMessage也将无效
        // a*; // 制造一个语法错误

        setTimeout(function() {
            throw new Error('error occurs');
        }, 100);
    };
    // 2.或者addEventListener再显式调用start
    // port.addEventListener('message', function(e) {
    //     //!!! console.log失效了
    //     console.log('will not occur in console');

    //     port.postMessage('sharedworker received data: ' + e.data);
    // });
    // port.start();
};
//---end sharedworker.js

在线DEMO:testSharedWorker DEMO

测试方法:打开上面的链接,看console发现count为1,不管怎么刷新刷新都是1,再开一个标签页打开该页面,发现count变成2了,然后刷新刷新刷新,发现count变成5了,回到之前的标签页,刷新,count变成6了…

五.线程安全问题

线程安全方面,不存在并发可能引发的各种问题,因为Worker线程的行为受到了限制(worker无法访问非线程安全的组件和DOM)

六.其它Worker

除了上面讨论的Web Workers,还有这些Worker:

  • ServiceWorkers(类似于代理服务器,提供离线服务)

  • ChromeWorker(开发浏览器插件中可用的Worker)

  • Audio Workers(支持音频处理,暂无任何浏览器支持,2015/11/29)

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code