本文要介绍的是 HTML5 的 Web Workers 特性,它解决了 JavaScript 开发中一个重大的问题 —— 在后台运行 JavaScript 。与本系列前两篇文章介绍的特性相似,Web Wordkers 似乎也是为了 Web Apps 而设计的,可以想象,Web Apps 乃至原生 Apps 受移动设备性能的限制比在桌面环境中要大很多,尽管现在的移动设备性能已经不断的提高,这对于发展 Web Apps 的确是个很好的机遇,但是其性能表现始终受限,因此,对 Web Apps 开发来说 Web Workers 无疑是个十分实用的技术,使到 Web Apps 可以在移动设备上更好的运行。

 

一. 什么是 Web Workers ?

在介绍 Web Workers 的具体使用时,Kayo 首先正式说明一下 Web Workers 。当 HTML 页面中执行 JavaScript 时,都是单线程执行的,这时页面的状态是不可响应的,即页面的 UI 会被锁定,若执行需要的时间过长,页面甚至会出现假死状态。这样对于用户来说是个很不好的体验,他们只能等待 UI 恢复过来才能继续操作,对于开发者来说,这也很无奈。Web Workers 则使到 JavaScript 能在后台运行 ,即在后台创建相应的线程,把费时的处理交给后台,前台便能继续响应用户的操作,这样既不会影响页面的性能,又使到用户可以继续做想做的事。

可以看出,Web Workers 确实是个很棒的特性,但它仍有一些限制:

Web Workers 会完全独立于它们的执行页面,即在独立线程中工作,因此不能在 Web Workers 内访问 DOM 对象,window 对象,document 对象和 parent 对象。但 Web Workers 仍可以直接访问 navigator ,因此开发者可以访问 navigator 对象中如 appName , appVersion 和 userAgent 等一些属性。

由于这个限制的存在,我们无法直接利用 Web Workers 进行 DOM 操作,因此 Web Workers 更适合用于复杂的计算等耗费 CPU 的处理,这样可以尽量减轻 CPU 的负担,提升应用的性能。另外,需要开发者虽然无法直接在 Web Workers 内进行 DOM 操作,但可以在 Web Workers 的 message 事件的回调函数中进行 DOM 操作(关于 message 事件 Kayo 会在下面详细介绍),而把计算处理交给 Web Workers 在后台计算。

另外,也是由于 Web Workers 在独立线程中工作的原因,因此它也不能使用主线程中的变量和函数,也不能使用 alert 等会使到页面状态暂停的操作。

以上就是 Web Workers 的基本情况了,虽然仍有限制,但 Web Workers 已经有很实用的开发价值了。

 

二. 浏览器支持

Web Workers 在现代浏览器中都已经实现完整的支持,IE 则完全不支持。具体如下(以下版本号表明从该版本开始支持 Web Workers ,但并不一定完全支持 Web Workers 的完整功能,并且早期的具体表现在各浏览器之间有较大差异):

Chrome 3+ , Firefox 3.5+ , Safari 4+ 和 Opera 10.6+

 

三. 使用 Web Workers

下面开始介绍如何利用 Web Workers 在后台运行 JavaScript 脚本。

 

1. 创建 Web Workers 对象

使用 Web Workers 运行 JavaScript 必须创建一个相应的 Web Workers 对象,例如,把需要执行的 JavaScript 都放在 demo_workers.js 中,而 demo_workers.js 与主页文档 index.html 处于同级目录,可以在 index.html 中使用以下语句创建相应的 Web Workers 对象:

var w = new Worker('demo_workers.js')
 

这样创建 Web Workers 对象之后,可以使用 "w" 引用这个 Web Workers 进行其他的操作了。

为了避免无效的操作,开发者还可以在创建及使用 Web Workers 之前判断浏览器是否支持 Web Wokers 。具体的代码如下:

if( typeof(Worker)!=="undefined" ){
 
    // 创建 workers 对象
    var w = new Worker('demo_workers.js');
} else {
    alert('抱歉,你的浏览器不支持 Web Workers!');
}
 

 

2. 发送信息

在创建 Web Workers 对象后,就可以继续进一步的使用了,Web Workers 的常用操作包括发送信息和接受信息,之所以要使用专门的方法来发送和接收信息,是因为创建 Web Workers 对象后只会执行脚本,但无法直接与脚本互通信息,而有了信息的传递才是真正的利用 Web Workers,下面开始介绍这两个方法。

要发送信息,可以使用 postMessage() 方法,这里值得注意的是,有些资料介绍 postMessage() 方法是从前台发送信息到后台的方法,实际上从后台发送信息到前台也是可以的(甚至可以说是必须的),并且也是使用 postMessage() 方法,只是具体使用的情况稍有不同。

从前台发送信息到后台,通常是发送需要在 Web Workers 内部进行计算的原始数据,这需要在相应的 Web Workers 对象上调用 postMessage() 方法,例如发送一个字符串到上面创建的 "w" 的后台中,可以这样编写代码:

// 定义需要发送的信息字符串
var the_string = "This is a message.";
// 发送信息到后台
w.postMessage(the_string);
 

这样一个字符串便会发送到 Web Workers 后台了,而在 Web Workers 的后台,即上面的 "demo_workers.js" 内部,若需要发送信息(通常为 Web Workers 根据原始数据计算的结果数据),由于 Web Workers 对象已经默认是自身对象,因此无需再手动引用 Web Workers 对象。例如,我们需要发送一个结果字符串到前台,可以这样编写代码:

//发送信息到前台
postMessage(result);
 

 

3. 接收信息

既然有发送信息的方法,那么肯定也有接受信息的方法,接收信息的方法是监听 onmessage 事件,在 onmessage 事件的回调函数中有一个封装好的参数 event ,在 event 的 data 属性中包含了使用 postMessage() 发送过来的数据。这个接收方法也是可以在前台和后台中使用,与 postMessage() 相似,后台监听 onmessage 事件不需引用 Web Workers 对象。例如,针对上面发送信息的例子,接收相应的信息可以这样编写代码:

// 在后台中接收前台发送过来的信息
onmessage = function (event){
    // 从 event 中获取数据,在这里其值为 "This is a message."
    var original_data = event.data;
    // 处理数据,得到结果
    // 发送计算结果到前台
    postMessage(result);
};
 

当然,你也可以使用 addEventListener 标准监听事件方法监听 onmessage ,例如:

w.addEventListener('message', function(event){
    // 处理数据,同上
});
// 在前台接收后台发送过来的信息
w.onmessage = function (event){
    // 从 event 中获取数据,在这里为 result 的值。
    alert(event.data);
};
 
 

到这里,已经完成了一个完整的 Web Workers 的基本使用过程。

 

4. 终止

当 Web Workers 对象被创建后,即使它执行的外部脚本已经完成,它也会继续监听信息。若需要终止 Web Workers ,释放相应的资源,可以使用 terminate() 方法。例如终止 "w" ,可以这样编写代码:

w.terminate()
 

 

四. 更多方法与事件

关于 Web Workers 的方法除了上面介绍到的 postMessage() 和 terminate() 外还有以下两个方法,并且以下两个方法只能在 Web Workers 执行的脚本中使用,在上例中即只能在 demo_worker.js 文件中使用。

 

1. close

停止 Worker 的行为。

 

2. importScripts

Workers 线程会动态加载外部脚本,调用该方法则会冻结 Worker 的线程,直到动态加载脚本完毕或脚本执行完毕(具体视浏览器而定),该方法可以方便开发者在 Web Workers 的脚本文件中载入其他脚本文件(例如 JavaScript 代码很长,需要分开几个文件编写时该方法便十分有用)。由于 Web Workers 是在独立线程中工作,它无法访问 document 对象,即无法访问外部资源,因此需要用该方法载入外部脚本。另外关于 importScript 还有以下两点需要注意:

  • importScripts 支持同时加载多个脚本,只需在每个参数中填写一个 JavaScript 文件,如:importScripts('a.js', 'b.js', 'c.js');
  • 只要 HTTP 连接数足够,使用 importScripts 同时加载多个脚本时均为并行加载。但执行这些脚本是按照参数顺序进行的,如上面的例子,则按照 a.js , b.js , c.js 的顺序执行各脚本(即使 b.js 脚本比 a.js 脚本先加载完毕,也会等待 a.js 加载完毕并且执行 a.js 后才会执行 b.js )

接下来再介绍 Web Workers 事件 onerror 。

执行 demo_workers.js 时若捕获到语法错误或运行异常(相关的文件 404 ,JavaScript 错误等)会触发 onerror ,注意该事件只能在 Web Workers 前台监听,具体的错误信息会封装在事件回调函数的 event 对象中,具体如下:

worker.onerror = function(event){
 
    event.message // 具体的错误信息
    event.lineno // 在 demo_workers.js 中导致错误发生的语句的所在行号
    event.filename // 返回发生错误的文件的完整 URL
};
 

onerror 事件也支持使用 addEventListener 标准监听事件方法监听。

 

五. 其他类型的 Workers

上面介绍的是 Web Workers 的基本类型,也就是普通的 Worker ,事实上还可以在普通的 Worker 上扩展出另外两种 Worker —— Subworker 和 Shared worker 。

 

1. Subworker

Worker 支持在一个 Worker 内部创建 Worker ,称为 Subworker 。这个 Worker 必须与父 Workers 同源,它的 URL 是根据父 Worker 的 URL 确定的,而不是依据它自己的页面 URL ,这样导致了以下两个需要注意的地方:

  • Subworker 不能跨域
  • 内部 Worker 引用资源必须使用相对 URL(相对于创建父 Worker 的页面,即上例中的 index.html)。

 

2. Shared Worker

开发者也可以创建一个被多个页面或连接使用的 Worker ,称为 Shared Worker(共享 Worker ),可以用于页面之间的共享通讯,它的工作方式与普通 Worker 稍有不同,目前主流浏览器对其支持都很不完善,因此这里不作详细介绍。

 

六. 其他注意事项

 

1. 可用的常用方法

在文章第一部分中,有介绍到由于 Web Workers 是在独立线程上工作,因此产生了无法直接访问 window 对象等一些限制,但这不影响 JavaScript 的大多数使用技巧,包括数据的类型和大多数方法(包括在 window 对象上的 setTimeout() 和 setInterval() 方法)都不受影响。

 

2. postMessage 中可用的数据类型

虽然 postMessage 中使用的数据类型并没有严格限制,但考虑到 postMessage 用于线程间的(即 Web Workers 中的线程与主页面的线程,或是多个有关联的 Web Workers 之间的线程)交互,因此建议使用 JavaScript 本地数据类型,如 Array , String , Date , Math ,同时也支持 JSON ,而不建议使用较为复杂的自定义类型。

 

3. location 对象的访问

Web Workers 可以访问 location 对象,但只可以以只读方式访问。开发者可以从 location 对象中获取 hostname 和 port 的值。

 

七. 完整实例

这里 Kayo 会使用例子进一步说明 Web Workers 的具体使用。Web Workers 的特长是进行复杂计算等耗费 CPU 的运算,而产生这种情况的通常原因要不就是算法费时,要不就是数据量大,或是两个原因同时都有。为了把代码集中于 Web Workers 的 API 调用部分,在例子中 Kayo 使用的方法是利用不复杂但效率低的递归算法配合大数据值的数据进行测试。

在下面的例子中,可以让用户输入两个整数,并计算它们的最大公约数和最小公倍数,计算方法采用递归算法,计算的部分写在独立的脚本文件中并交给 Web Workers 计算,建议用户输入数值较大的数据进行测试。当然,这个例子的主要意义只是演示 Web Workers 的使用方法,由于现在的桌面环境乃至移动设备环境都有了很大的提升,因此像例子中的算法即使使用大数值的数据进行测试也未必能体现 Web Workers 的优势,但在实际的 Web Apps 开发中,却很可能会出现比这个算法复杂得多的情况,例如从数据库中获取大量数据并运算,涉及物理模拟的运算,或是多个复杂算法需要并行运算,这样的情况都 很可能会需要较多的时间进行,导致页面 UI 出现一段时间不能响应用户操作,甚至在较低配置的移动设备上会发生崩溃,这时 Web Workers 便是很好的工具了。

完整 Demo (需要使用本文第二部分“浏览器支持”中列出的浏览器进行测试)。

下面列出例子中主要的代码。

index.html 部分

HTML 代码片段

<div id="wrap">
    <p id="tips">
        输入两个数并计算它们的最大公约数和最小公倍数:
        <output id="result"></output>
    </p>
    <p class="input-data">整数1: <input id="first"></input></p>
    <p class="input-data">整数2: <input id="second"></input></p>
    <button id="start-worker" onclick="startWorker()">开始 Worker</button>
 
    <div id="footer">
        <p>Demo by <a href="http://kayosite.com" target="_blank">Kayo</a></p>
    </div>
</div>
 

JavaScript 代码

// 获取用户输入然后发送到 workers 并在后台进行计算
function startWorker(){
    if(typeof(Worker)!=="undefined"){
 
        var first = document.getElementById('first').value;
        var second = document.getElementById('second').value;
        var nums = {'first': first, 'second': second};
 
        // 创建 workers 对象
        var w = new Worker('demo_workers.js');
        w.postMessage(nums);
 
        // 接受 workers 从后台返回的数据
        w.onmessage = function (event){
            document.getElementById('result').value = event.data;
        };
    } else {
        alert('抱歉,你的浏览器不支持 Web Workers!');
    }
};
 

demo_worker.js 代码

// 接收前台的数据并进行计算
onmessage = function (event){
    var first = event.data.first;
    var second = event.data.second;
    var common_divisor = divisor(first, second);
    var common_multiple = multiple(first, second);
    // 发送计算结果到前台
    postMessage("计算完毕! " + "最大公约数为 "+ common_divisor
    +" 以及最小公倍数为 " + common_multiple);
};
 
// 计算最大公约数
function divisor(a, b) {
    if (a % b == 0) {
        return b;
    } else {
        return divisor(b, a % b);
    }
};
 
// 计算最小公倍数
function multiple(a,  b){
    var multiple = 0;
    multiple = a * b / divisor(a, b);
    return multiple;
};
 

原文由 Kayo Lee 发表,原文链接:http://kayosite.com/web-app-by-jquery-mobile-and-html5-offline-web-workers.html