Skip to content

HTML5 Web Worker 的使用

Web Workers 是 HTML5 提供的一个 javascript 多线程解决方案,我们可以将一些大计算量的代码交由 web Worker 运行而不冻结用户界面。

一:如何使用 Worker

Web Worker 的基本原理就是在当前 javascript 的主线程中,使用 Worker 类加载一个 javascript 文件来开辟一个新的线程,起到互不阻塞执行的效果,并且提供主线程和新线程之间数据交换的接口:postMessage,onmessage。

那么如何使用呢,我们看一个例子:

javascript
//worker.js
onmessage = function (evt) {
  var d = evt.data;
  // 通过 evt.data 获得发送来的数据
  postMessage(d);
  // 将获取到的数据发送会主线程
};
//worker.js
onmessage = function (evt) {
  var d = evt.data;
  // 通过 evt.data 获得发送来的数据
  postMessage(d);
  // 将获取到的数据发送会主线程
};

HTML 页面:test.html

html
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <script type="text/javascript">
      //WEB 页主线程
      var worker = new Worker("worker.js");
      // 创建一个 Worker 对象并向它传递将在新线程中执行的脚本的 URL
      worker.postMessage("hello world");
      // 向 worker 发送数据
      worker.onmessage = function (evt) {
        // 接收 worker 传过来的数据函数
        console.log(evt.data);
        // 输出 worker 发送来的数据
      };
    </script>
  </head>
  <body></body>
</html>
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <script type="text/javascript">
      //WEB 页主线程
      var worker = new Worker("worker.js");
      // 创建一个 Worker 对象并向它传递将在新线程中执行的脚本的 URL
      worker.postMessage("hello world");
      // 向 worker 发送数据
      worker.onmessage = function (evt) {
        // 接收 worker 传过来的数据函数
        console.log(evt.data);
        // 输出 worker 发送来的数据
      };
    </script>
  </head>
  <body></body>
</html>

用 Chrome 浏览器打开 test.html 后,控制台输出 ”hello world” 表示程序执行成功。

通过这个例子我们可以看出使用 web worker 主要分为以下几部分

WEB 主线程

  1. 通过 worker = new Worker(url) 加载一个 JS 文件来创建一个 worker,同时返回一个 worker 实例。
  2. 通过 worker.postMessage(data) 方法来向 worker 发送数据。
  3. 绑定 worker.onmessage 方法来接收 worker 发送过来的数据。
  4. 可以使用 worker.terminate() 来终止一个 worker 的执行。

worker 新线程

  1. 通过 postMessage(data) 方法来向主线程发送数据。
  2. 绑定 onmessage 方法来接收主线程发送过来的数据。

二:Worker 能做什么

知道了如何使用 web worker ,那么它到底有什么用,可以帮我们解决那些问题呢。我们来看一个 fibonacci 数列的例子。

大家知道在数学上,fibonacci 数列被以递归的方法定义:F0=0,F1=1,Fn=F(n-1)+F(n-2)(n>=2,n∈N*),而 javascript 的常用实现为:

javascript
var fibonacci = function (n) {
  return n < 2 ? n : arguments.callee(n - 1) + arguments.callee(n - 2);
};
//fibonacci(36)
var fibonacci = function (n) {
  return n < 2 ? n : arguments.callee(n - 1) + arguments.callee(n - 2);
};
//fibonacci(36)

在 chrome 中用该方法进行 39 的 fibonacci 数列执行时间为 19097 毫秒 ,而要计算 40 的时候浏览器直接提示脚本忙了。

由于 javascript 是单线程执行的,在求数列的过程中浏览器不能执行其它 javascript 脚本,UI 渲染线程也会被挂起,从而导致浏览器进入僵死状态。使用 web worker 将数列的计算过程放入一个新线程里去执行将避免这种情况的出现。具体看例子:

javascript
//fibonacci.js
var fibonacci = function (n) {
  return n < 2 ? n : arguments.callee(n - 1) + arguments.callee(n - 2);
};
onmessage = function (event) {
  var n = parseInt(event.data, 10);
  postMessage(fibonacci(n));
};
//fibonacci.js
var fibonacci = function (n) {
  return n < 2 ? n : arguments.callee(n - 1) + arguments.callee(n - 2);
};
onmessage = function (event) {
  var n = parseInt(event.data, 10);
  postMessage(fibonacci(n));
};

HTML 页面:fibonacci.html

html
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>web worker fibonacci</title>
    <script type="text/javascript">
      onload = function () {
        var worker = new Worker("fibonacci.js");
        worker.addEventListener(
          "message",
          function (event) {
            var timer2 = new Date().valueOf();
            console.log("结果:" + event.data, "时间:" + timer2, "用时:" + (timer2 - timer));
          },
          false
        );
        var timer = new Date().valueOf();
        console.log("开始计算:40", "时间:" + timer);
        setTimeout(function () {
          console.log("定时器函数在计算数列时执行了", "时间:" + new Date().valueOf());
        }, 1000);
        worker.postMessage(40);
        console.log("我在计算数列的时候执行了", "时间:" + new Date().valueOf());
      };
    </script>
  </head>
  <body></body>
</html>
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>web worker fibonacci</title>
    <script type="text/javascript">
      onload = function () {
        var worker = new Worker("fibonacci.js");
        worker.addEventListener(
          "message",
          function (event) {
            var timer2 = new Date().valueOf();
            console.log("结果:" + event.data, "时间:" + timer2, "用时:" + (timer2 - timer));
          },
          false
        );
        var timer = new Date().valueOf();
        console.log("开始计算:40", "时间:" + timer);
        setTimeout(function () {
          console.log("定时器函数在计算数列时执行了", "时间:" + new Date().valueOf());
        }, 1000);
        worker.postMessage(40);
        console.log("我在计算数列的时候执行了", "时间:" + new Date().valueOf());
      };
    </script>
  </head>
  <body></body>
</html>

在 Chrome 中打开 fibonacci.html,控制台得到如下输出:

开始计算:40 时间:1316508212705

我在计算数列的时候执行了 时间:1316508212734

定时器函数在计算数列时执行了 时间:1316508213735

结果:102334155 时间:1316508262820 用时:50115

这个例子说明在 worker 中执行的 fibonacci 数列的计算并不会影响到主线程的代码执行,完全在自己独立的线程中计算,只是在计算完成之后将结果发回主线程。

利用 web worker 我们可以在前端执行一些复杂的大量运算而不会影响页面的展示,并且不会弹出恶心的脚本正忙提示。

下面这个例子使用了 web worker 来计算场景中的像素,场景打开时是一片一片进行绘制的,一个 worker 只计算一块像素值。

http://nerget.com/rayjs-mt/rayjs.html

三:Worker 的其他尝试

我们已经知道 Worker 通过接收一个 URL 来创建一个 worker,那么我们是否可以利用 web worker 来做一些类似 jsonp 的请求呢,大家知道 jsonp 是通过插入 script 标签来加载 json 数据的,而 script 元素在加载和执行过程中都是阻塞式的,如果能利用 web worker 实现异步加载将会非常不错。

下面这个例子将通过 web worker、jsonp、ajax 三种不同的方式来加载一个 169.42KB 大小的 JSON 数据

javascript
// /aj/webWorker/core.js
function $E(id) {
  return document.getElementById(id);
}
onload = function () {
  // 通过 web worker 加载
  $E("workerLoad").onclick = function () {
    var url = "http://js.wcdn.cn/aj/mblog/face2";
    var d = new Date().valueOf();
    var worker = new Worker(url);
    worker.onmessage = function (obj) {
      console.log("web worker: " + (new Date().valueOf() - d));
    };
  };

  // 通过 jsonp 加载
  $E("jsonpLoad").onclick = function () {
    var url = "http://js.wcdn.cn/aj/mblog/face1";
    var d = new Date().valueOf();
    STK.core.io.scriptLoader({
      method: "post",
      url: url,
      onComplete: function () {
        console.log("jsonp: " + (new Date().valueOf() - d));
      },
    });
  };

  // 通过 ajax 加载
  $E("ajaxLoad").onclick = function () {
    var url = "http://js.wcdn.cn/aj/mblog/face";
    var d = new Date().valueOf();
    STK.core.io.ajax({
      url: url,
      onComplete: function (json) {
        console.log("ajax: " + (new Date().valueOf() - d));
      },
    });
  };
};
// /aj/webWorker/core.js
function $E(id) {
  return document.getElementById(id);
}
onload = function () {
  // 通过 web worker 加载
  $E("workerLoad").onclick = function () {
    var url = "http://js.wcdn.cn/aj/mblog/face2";
    var d = new Date().valueOf();
    var worker = new Worker(url);
    worker.onmessage = function (obj) {
      console.log("web worker: " + (new Date().valueOf() - d));
    };
  };

  // 通过 jsonp 加载
  $E("jsonpLoad").onclick = function () {
    var url = "http://js.wcdn.cn/aj/mblog/face1";
    var d = new Date().valueOf();
    STK.core.io.scriptLoader({
      method: "post",
      url: url,
      onComplete: function () {
        console.log("jsonp: " + (new Date().valueOf() - d));
      },
    });
  };

  // 通过 ajax 加载
  $E("ajaxLoad").onclick = function () {
    var url = "http://js.wcdn.cn/aj/mblog/face";
    var d = new Date().valueOf();
    STK.core.io.ajax({
      url: url,
      onComplete: function (json) {
        console.log("ajax: " + (new Date().valueOf() - d));
      },
    });
  };
};

HTML 页面:/aj/webWorker/worker.html

html
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Worker example: load data</title>
<script src="http://js.t.sinajs.cn/STK/js/gaea.1.14.js" type="text/javascript"></script>
<script type="text/javascript" src="http://js.wcdn.cn/aj/webWorker/core.js"></script>
</head>
<body>
    <input type="button" id="workerLoad" value="web worker 加载"></input>
    <input type="button" id="jsonpLoad" value="jsonp 加载"></input>
    <input type="button" id="ajaxLoad" value="ajax 加载"></input>
</body>
</html>
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Worker example: load data</title>
<script src="http://js.t.sinajs.cn/STK/js/gaea.1.14.js" type="text/javascript"></script>
<script type="text/javascript" src="http://js.wcdn.cn/aj/webWorker/core.js"></script>
</head>
<body>
    <input type="button" id="workerLoad" value="web worker 加载"></input>
    <input type="button" id="jsonpLoad" value="jsonp 加载"></input>
    <input type="button" id="ajaxLoad" value="ajax 加载"></input>
</body>
</html>

设置 HOST

127.0.0.1 js.wcdn.cn

通过 http://js.wcdn.cn/aj/webWorker/worker.html 访问页面然后分别通过三种方式加载数据,得到控制台输出:

bash
web worker: 174
jsonp: 25
ajax: 38
web worker: 174
jsonp: 25
ajax: 38

多试几次发现通过 jsonp 和 ajax 加载数据的时间相差不大,而 web worker 的加载时间一直处于高位,所以用 web worker 来加载数据还是比较慢的,即便是大数据量情况下也没任何优势,可能是 Worker 初始化新起线程比较耗时间。除了在加载过程中是无阻塞的之外没有任何优势。

那么 web worker 是否能支持跨域 js 加载呢,这次我们通过 http://127.0.0.1/aj/webWorker/worker.html 来访问页面,当点击 ”web worker 加载” 加载按钮时 Chrome 下无任何反映,FF6 下提示错误。由此我们可以知道 web worker 是不支持跨域加载 JS 的,这对于将静态文件部署到单独的静态服务器的网站来说是个坏消息。

所以 web worker 只能用来加载同域下的 json 数据,而这方面 ajax 已经可以做到了,而且效率更高更通用。还是让 Worker 做它自己擅长的事吧。

四:总结

web worker 看起来很美好,但处处是魔鬼。

我们可以做什么:

  1. 可以加载一个 JS 进行大量的复杂计算而不挂起主进程,并通过 postMessageonmessage 进行通信
  2. 可以在 worker 中通过 importScripts(url) 加载另外的脚本文件
  3. 可以使用 setTimeout(), clearTimeout(), setInterval(), and clearInterval()
  4. 可以使用 XMLHttpRequest 来发送请求
  5. 可以访问 navigator 的部分属性

有那些局限性:

  1. 不能跨域加载 JS
  2. worker 内代码不能访问 DOM
  3. 各个浏览器对 Worker 的实现不大一致,例如 FF 里允许 worker 中创建新的 worker, 而 Chrome 中就不行
  4. 不是每个浏览器都支持这个新特性

最后编辑时间:

Version 4.0 (framework-1.0.0-rc.20)