当前位置: > > > HTML5 - 使用Web Worker执行后台任务(附:后台搜索素数样例)

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 来定期检测某个网站是否有新数据。
评论(0)