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:完美支持
  • IEIE10 起开始支持(包括后面的 Edge 也支持)
  • Opera:完美支持

(2)移动设备

  • iOSiOS 5.1 起开始支持
  • AndroidAndorid 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 来定期检测某个网站是否有新数据。

THE END