HTML5 – 使用Web Worker执行后台任务(附:后台搜索素数样例)
一、Web Worker 规范
1,产生背景
- 有时我们可能需要使用 JavaScript 执行一些复杂的任务,由于 js 代码始终在前台运行,因此那些耗费时间的代码会打断用户,阻塞页面,直到任务完成。这样对用户体验会造成很大的影响。
- 为解决 JavaScript 阻塞页面的问题,过去常会使用 setInterval() 或 setTime() 把大任务分成小任务,每次只运行一个小任务。这个方法在某些场景下是可行的,但对于不能拆分而又耗时很长的任务,这个办法会增加复杂性和困扰。
2,基本介绍
- HTML5 提出了更好的解决方案:一个叫 Web Worker 的对象,它能够在后台完成工作。
- 对于那些比较费时的工作,可以创建一个新的 Web Worker 对象,把要运行的代码交给它,然后让它运行就好了。
- 在 Web Worker 工作期间,还可以通过传递文本消息这种安全且受限的方式与它通信。
Web Worker安全措施
使用
Web Worker 不必担心会发生两段代码争抢操作同一处数据的问题,因为它不允许在网页之间或
Web Worker 之间共享数据。
不过我们可以把数据从网页发送到
Web Worker(或者相反),但
JavaScript 会自动复制一份,并发送该副本。这就意味着不同的线程不能同时占用相同的内存区域。
这种简化的模型索然限制
Web Worker 的能力,但却换来了安全。
3,兼容性
(1)桌面浏览器
- Chrome:完美支持
- Firefox:完美支持
- Safari:完美支持
- IE:IE10 起开始支持(包括后面的 Edge 也支持)
- Opera:完美支持
(2)移动设备
- iOS:iOS 5.1 起开始支持
- Android:Andorid 4.4 起开始支持
4,测试当前浏览器是否支持
通过测试是否存在
window.Worker,可以判断出浏览器是否支持
Web Worker。
if(window.Worker) { alert("当前浏览器支持 Web Worker。"); }else{ alert("当前浏览器不支持 Web Worker。"); }
二、Web Worker 使用说明
1,Worker 对象
Web Worker 协议定义了一个新对象:
Worker。我们在需要后台执行任务时,可以创建一个新的
Worker,交给它一些代码,然后发送给它一些数据。 比如下面这行代码创建了一个新的
Worker 对象,让它执行
PrimeWorker.js 中的代码。
var worker = new Worker("PrimeWorker.js");
2,数据传输
网页与
Worker 之间通过消息来沟通:
- 给 Worker 发送消息要使用该对象的 postMessage() 方法,Worker 那边会通过 onmessage 事件接收到该数据的一个副本。
- 如果 Worker 需要跟网页对话,它可以调用自己的 postMessage() 方法,并带上一些数据。网页同样是在 onmessage 事件中接收这些数据。
3,处理 Worker 错误
如果后台脚本遇到问题或者因为数据无效出现错误,
Worker 会把打包的错误数据发送给网页。通过
onerror 事件告诉网页有错误发生。
worker.onerror = workerError; function workerError(error) { statusDisplay.innerHTML = error.message; }
错误对象中包含如下三个属性:
- message:错误消息
- lineno:错误所在的行号
- filename:文件的名字
4,取消后台任务
要停止
Worker 工作的方式有如下两种:
- 一种是 Worker 对象调用自己的 close() 方法.
- 另一种是创建 Worker 对象的页面调用该对象的 terminate() 方法。
5,在 Worker 中使用另一个 JS 文件中的代码
这个可以使用
importScripts() 函数实现。比如我们需要在一个
Worker 内调用
findPrimes.js 文件中的函数,只需添加如下代码将其引入即可:
importScripts("findPrimes.js");
三、一个待改造样例(未使用 Web Worker)
1,效果图
(1)在输入框中填写一个区间范围后点“
搜索”按钮,程序便会搜索出该区间内所有的素数,并显示在下方区域。 (2)当我们选择的区间很窄(1 ~
50000),任务很快就能完成。 (3)但如果范围很大时(
1 ~
5000000),会导致页面数分钟没有反应,整个页面死在哪里,无法操作。
2,样例代码
(1)主页面(
index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>hangge.com</title>
<script src="FindPrimes.js"></script>
<style>
#primeContainer {
border: solid 1px black;
padding: 3px;
height: 300px;
max-width: 500px;
overflow: scroll;
overflow-x: hidden;
font-size: x-small;
}
input {
width: 70px;
}
p {
margin-bottom: 25px;
}
</style>
<script>
//搜索按钮点击
function doSearch() {
//取得指定搜索区间的两个数据
var fromNumber = document.getElementById("from").value;
var toNumber = document.getElementById("to").value;
//执行搜索(花的时间主要在这里,该方法保存在FindPrimes.js文件中)
var primes = findPrimes(fromNumber, toNumber);
//遍历素数数组,把它们转成一个长字符串
var primeList = "";
for (var i=0; i<primes.length; i++) {
primeList += primes[i];
if (i != primes.length-1) primeList += ", ";
}
//把素数字符串插入页面中
var primeContainer = document.getElementById("primeContainer");
primeContainer.innerHTML = primeList;
//更新状态消息,告诉用户当前情况
var statusDisplay = document.getElementById("status");
if (primeList.length == 0) {
statusDisplay.innerHTML = "未找到任何结果!";
}
else {
statusDisplay.innerHTML = "搜索完毕!";
}
}
</script>
</head>
<body>
<p>
要搜索的范围:<input id="from" value="1"> 至 <input id="to" value="50000">
<button onclick="doSearch()">搜索</button>
</p>
<div id="primeContainer">
</div>
<div id="status"></div>
</body>
</html>
(2)上面代码中查找素数的任务是一个名叫
findPrimes() 的函数完成的,该函数保存在另一个
JavaScript 文件中(
FindPrimes.js)。文件内容如下:
//搜索指定区间范围的素数 function findPrimes(fromNumber, toNumber) { // Create an array containing all integers between the two specified numbers. var list = []; for (var i=fromNumber; i<=toNumber; i++) { if (i>1) list.push(i); } // Test for primes. var maxDiv = Math.round(Math.sqrt(toNumber)); var primes = []; for (var i=0; i<list.length; i++) { var failed = false; for (var j=2; j<=maxDiv; j++) { if ((list[i] != j) && (list[i] % j == 0)) { failed = true; } else if ((j==maxDiv) && (failed == false)) { primes.push(list[i]); } } } return primes; }
四、功能改造:把任务放在后台执行
1,效果图
(1)在输入框中填写一个区间范围后点“
搜索”按钮,程序便会搜索出该区间内所有的素数,并显示在下方区域。 (2)由于是在后台搜索,搜索过程中页面仍然保持响应,不会卡死。同时下方还会当前进度。 (3)增加了个“
取消”按钮,点击后会取消后台任务,停止搜索。
2,样例代码
(1)
Worker 任务代码(
PrimeWorker.js)
//onMessage事件处理 onmessage = function(event) { //网页发过来的对象保存在evnet.data属性中,获取并在该范围内搜索素数 var primes = findPrimes(event.data.from, event.data.to); //搜索完成,把结果发回网页 postMessage( {messageType: "PrimeList", data: primes} ); }; //搜索指定区间范围的素数 function findPrimes(fromNumber, toNumber) { // Create an array containing all integers between the two specified numbers. var list = []; for (var i=fromNumber; i<=toNumber; i++) { if (i>1) list.push(i); } // Test for primes. var maxDiv = Math.round(Math.sqrt(toNumber)); var primes = []; var previousProgress; for (var i=0; i<list.length; i++) { var failed = false; for (var j=2; j<=maxDiv; j++) { if ((list[i] != j) && (list[i] % j == 0)) { failed = true; } else if ((j==maxDiv) && (failed == false)) { primes.push(list[i]); } } //计算进度百分比 var progress = Math.round(i/list.length*100); //只在进度变化超过1%时才发送进度百分比信息 if (progress != previousProgress) { postMessage( {messageType: "Progress", data: progress} ); previousProgress = progress; } } return primes; }
(2)主页代码(
index.html)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>hangge.com</title> <script src="PrimeWorker.js"></script> <style> #primeContainer { border: solid 1px black; padding: 3px; height: 300px; max-width: 500px; overflow: scroll; overflow-x: hidden; font-size: x-small; } input { width: 70px; } p { margin-bottom: 25px; } </style> <script> //Web Worker对象 var worker; //搜索按钮 var searchButton; //状态显示区域 var statusDisplay; //页面加载完毕 window.onload = function() { searchButton = document.getElementById("searchButton"); statusDisplay = document.getElementById("status"); }; //搜索按钮点击 function doSearch() { //开始搜索时先禁用按钮,防止同时进行多个搜索 searchButton.disabled = true; //取得数值范围,发送给Web Worker var fromNumber = document.getElementById("from").value; var toNumber = document.getElementById("to").value; //创建新的Worker worker = new Worker("PrimeWorker.js"); //指定onMessage事件,以便从Worker那里收到消息 worker.onmessage = receivedWorkerMessage; //指定onError事件,处理Worker错误 worker.onerror = workerError; //将数值范围发送给Web Worker worker.postMessage( { from: fromNumber, to: toNumber } ); //搜索开始提示 statusDisplay.innerHTML = "web worker任务开始,搜索范围("+ fromNumber + " 到 " + toNumber + ") "; } //处理Worker发送过来的消息数据 function receivedWorkerMessage(event) { //取得发送过来的对象 var message = event.data; //判断是那种类型的消息(PrimeList:搜索完毕。Progress:上报进度) if (message.messageType == "PrimeList") { //取得素数数组列表 var primes = message.data; //遍历素数数组,把它们转成一个长字符串 var primeList = ""; for (var i=0; i<primes.length; i++) { primeList += primes[i]; if (i != primes.length-1) primeList += ", "; } //把素数字符串插入页面中 var displayList = document.getElementById("primeContainer"); displayList.innerHTML = primeList; //更新状态消息,告诉用户当前情况 if (primeList.length == 0) { statusDisplay.innerHTML = "未找到任何结果!"; } else { statusDisplay.innerHTML = "搜索完毕!"; } //启动搜索功能 searchButton.disabled = false; } else if (message.messageType == "Progress") { //报告当前进度 statusDisplay.innerHTML = "当前进度:" + message.data + "%"; } } //当Worker发生错误时在页面上显示错误信息 function workerError(error) { statusDisplay.innerHTML = error.message; } //取消按钮点击 function cancelSearch() { //停止任务 worker.terminate(); worker = null; statusDisplay.innerHTML = ""; searchButton.disabled = false; } </script> </head> <body> <p> 要搜索的范围:<input id="from" value="1"> 至 <input id="to" value="50000"> <button id="searchButton" onclick="doSearch()">搜索</button> <button onclick="cancelSearch()">取消</button> </p> <div id="primeContainer"> </div> <div id="status"></div> </body> </html>
五、高级技巧
上面的素数搜索页面只是一个演示
Web Worker 如何使用的简单样例。在实际开发中,页面通常不会这么简单。下面是
Web Worker 的一些进阶用法。
1,在多个任务中重用 Web Worker
Worker 对象完成既定任务,触发
onmessage 事件处理程序后并不会被销毁。它只会闲置在那儿,等待新的任务。如果你再给它发送新的消息,它会马上进入状态,投入新的工作
2,创建多个 Web Worker
一个页面并不限于只能创建一个
Worker 对象。比如,若要支持访客同时搜索多个区间内的素数,就需要为每个搜索单独创建一个
Worker,然后通过数组来跟踪它们。这样,每当有
Worker 返回结果,就可以把结果添加到页面中,同时注意不覆盖其他
Worker 的结果。(为了稳妥起见,还是建议大家少创建
Web Worker,它们都不是“省油的灯",一次运行太多会拖慢计算机。
3,在一个 Web Worker 中创建另一个 Web Worker
每个
Web Worker 都可以创建自己的
Web Worker,向它们发送消息,从它们那里接收消息。对于复杂的计算任务,比如计算斐波那契数这种需要递归的计算,在
Worker 内创建
Worker 便可以派上用场。
4,通过 Web Worker 下载数据
Web Worker 可以使用
XMLHttpRequest 对象取得新页面,或者向
Web 服务发送请求。取得了所需的信息后,它们可以调用
postMessage() 方法,把数据发回页面。
5,利用 Web Worker 执行周期性任务
与普通网页中的脚本一样,
Web Worker 也可以调用
setTimeout() 或
setlnterval() 函数。因此,可以通过
Web Worker 来定期检测某个网站是否有新数据。