认识Web Workers

60 阅读10分钟

小组会议.png

前言

在一个html文件中,我们通常会看到三种模块[html·css·js],一个好的页面,一般都这样的规则,css放在head中,js放在最下面,为啥要这样做呢?其实我们都知道页面在加载过程中从上到下的过程,如果js在加载的过程中,如果报错,一个页面的加载过程就会终止掉,有可能看不到页面,影响体验。
从上面我们不能看出,页面在加载的过程中js和UI页面会共用一个线程,而js在执行时会阻塞浏览器的响应,这也就是为什么js都在页面底部应用和书写js代码,还有我们在业务代码都不会写很复杂的业务。后期的ajax的出现使得页面在等待服务器响应的这段时间内不再发生阻塞,但是这仍然没有改变代码单线程执行的本质。
下面给大家带来一个新的方式!
  • 什么是Web Workers?

Web Workers是在js单线程执行的基础上开启一个子线程,进行程序处理,而不影响主线程的执行,当子线程执行完之后再回到主线程上,在这个过程中不影响主线程的执行。简单的来说,Web Worker就是一个运行在后台的JavaScript线程,不会影响页面的响应。Web Worker的类型有专用线程dedicated web worker和共享线程shared web worker两种
专用线程随当前页面的关闭而结束;这意味着Dedicated web worker只能被创建它的页面访问。
共享线程可以被多个页面访问
  • 为什么引用Web Workers?有什么作用?

为什么引用
我们作为前端开发者应该知道浏览器中JS和UI公用一个线程,JS计算过程中,不能响应UI;如果遇到计算量比较大的任务,如操作图像像素时,会造成用户行为得不到响应。Web Worker 是为了解决 JavaScript 在浏览器环境中没有多线程的问题。支持 Web Worker 的浏览器会额外提供一个 JavaScript Runtime 供 Web Worker 使用。它的最佳使用场景是执行一些开销较大的数据处理或计算任务。
有什么作用
为了让 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。Web Workers线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。
  • Web Workers工作原理是什么?

在“主线程”中创建一个子线程 Web Workers实例,通过监听 onmessage 事件获取消息,通过 postMessage 发送消息。
在“主线程”中创建一个子线程 Web Workers实例,通过监听 onmessage 事件获取消息,通过 postMessage 发送消息。

在这里插入图片描述

  • Web Workers适用哪些应用场景?

射线追踪:
射线追踪是一项通过追踪光线的路径作为像素来生成图片的渲染技术。Ray tracing 使用 CPU 密集型计算来模仿光线的路径。思路即模仿一些诸如反射,折射,材料等的效果。所有的这些计算逻辑可以放在 Web Worker 中以避免阻塞 UI 线程。甚至更好的方法即-你可以轻易地把把图片的渲染拆分在几个 workers 中进行(即在各自的 CPU 中进行计算,意思是说利用多个 CPU 来进行计算,可以参考下 nodejs 的 api)。
加密
端到端的加密由于对保护个人和敏感数据日益严格的法律规定而变得越来越流行。加密有时候会非常地耗时,特别是如果当你需要经常加密很多数据的时候(比如,发往服务器前加密数据)。这是一个使用 Web Worker 的绝佳场景,因为它并不需要访问 DOM 或者利用其它魔法-它只是纯粹使用算法进行计算而已。一旦在 worker 进行计算,它对于用户来说是无缝地且不会影响到用户体验。
预取数据
为了优化网站或者网络应用及提升数据加载时间,你可以使用 Workers 来提前加载部分数据以备不时之需。不像其它技术,Web Workers 在这种情况下是最棒哒,因为它不会影响程序的使用体验。
渐进式网络应用
即使在网络不稳定的情况下,它们必须快速加载。这意味着数据必须本地存储于浏览器中。这时候 IndexDB 及其它类似的 API 就派上用场了。大体上说,一个客户端存储是必须的。为了不阻塞 UI 线程的渲染,这项工作必须由 Web Workers 来执行。呃,当使用 IndexDB的时候,可以不使用 workers 而使用其异步接口,但是之前它也含有同步接口(可能会再次引入 ),这时候就必须在 workers 中使用 IndexDB。
拼写检查
一个基本的拼写检测器是这样工作的-程序会读取一个包含拼写正确的单词列表的字典文件。字典会被解析成一个搜索树以加快实际的文本搜索。当检查器检查一个单词的时候,程序会在预构建搜索树中进行检索。如果在树中没有检索到,则会通过提供替代的字符为用户提供替代的拼写并检测单词是否是有效-是否是用户需要的单词。这个检索过程中的所有工作都可以交由 Web Worker 来完成,这样用户就只需输入单词和语句而不会阻塞 UI,与此同时 worker 会处理所有的搜索和服务建议。

.......

  • 如何使用Web Workers?

创建线程
var worker= new Worker(jsUrl, options)
数据发送
worker.postMessage('Hello World');
worker.postMessage(...);//各种数据类型
数据接收
1.worker.onmessage = function(event){...}
2.worker.addEventListener(....);
异常处理
worker.onerror = function(error){...}
结束线程
主页面中调用 
worker.terminate() 
或者在 workder 内部调用 
self.close()
Web Workers API之主线程
  1. Worker.onerror:指定 error 事件的监听函数。
  2. Worker.onmessage:指定 message 事件的监听函数,发送过来的数据在Event.data属性中。
  3. Worker.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
  4. Worker.postMessage():向 Worker 线程发送消息。
  5. Worker.terminate():立即终止 Worker 线程。
Web Workers API之Worker 线程
  1. self.name: Worker 的名字。该属性只读,由构造函数指定。
  2. self.onmessage:指定message事件的监听函数。
  3. self.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
  4. self.close():关闭 Worker 线程。
  5. self.postMessage():向产生这个 Worker 线程发送消息。
  6. self.importScripts():加载 JS 脚本。
Web Workers使用中注意事项
  1. 同源限制: 分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。

  2. DOM 限制 Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象

  3. 通信联系 Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。

  4. 脚本限制 Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。

  5. 文件限制 Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。

  • Web Workers Demo

测试 demo1: 该实例测试线程会不会阻塞
点击“开始”按钮启动web worker,可以看到web worker开始工作,且在web worker正常工作时,我们仍然可以在input输入框中输入信息,这表示页面并没有因为web worker的运行而被阻塞。

demo1.html

<!DOCTYPE html>
<html>

<body>
  <p>数值计算: <output id="result"></output></p>
  <button onclick="startWorker()">开启</button>
  <button onclick="stopWorker()">关闭</button>
  <input type="text" value="" />
  <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
  <script>
    var worker;
    function startWorker() {
      if (typeof (Worker) !== "undefined") {
        if (typeof (worker) === "undefined") {
          worker = new Worker("./script/demo_one.js");
        }
        worker.onmessage = function (event) {
          document.getElementById("result").innerHTML = event.data;
        };
      } else {
        document.getElementById("result").innerHTML = "Sorry, your browser does not support Web Workers...";
      }
    }
    function stopWorker() {
      worker.terminate();
      worker = undefined
    }
  </script>
</body>

</html>

demo_one.js

function timedCount() {
  for (var i = 0; i < 10000000000; i++) {
    if (i % 100000 === 0) {
      postMessage(i);
    }
  }
}
timedCount();
测试 demo2: 该实例使用工作线程做后台数值(算法)计算
工作线程最简单的应用就是用来做后台计算,而这种计算并不会中断前台用户的操作。下面我们提供了一个工作线程的代码片段,用来执行一个相对来说比较复杂的任务:计算两个非常大的数字的最小公倍数和最大公约数。
在这个例子中,我们在主页面中创建一个后台工作线程,并且向这个工作线程分配任务(即传递两个特别大的数字),当工作线程执行完这个任务时,便向主页面程序返回计算结果,而在这个过程中,主页面不需要等待这个耗时的操作,可以继续进行其它的行为或任务。

demo2.html

<!DOCTYPE HTML>
<html>

<head>
  <title>
    使用工作线程做后台数值(算法)计算
  </title>
</head>

<body>
  <div>
    最小公共倍数和最大公约数是:
    <p id="computation_results">请等待, 计算中 … </p>
  </div>
  <script>
    var worker = new Worker("./script/demo_two.js");
    worker.postMessage('{"first":347734080,"second":3423744400}');
    worker.onmessage = function (event) {
      document.getElementById('computation_results').textContent = event.data;
    }; 
  </script>
</body>

</html>

demo_two.js

onmessage = function (event) {
  debugger
  var first = JSON.parse(event.data).first;
  var second = JSON.parse(event.data).second;
  calculate(first, second);
};

function calculate(first, second) {
  //do the calculation work 
  var common_divisor = divisor(first, second);
  var common_multiple = multiple(first, second);
  postMessage("Work done! " +
    "The least common multiple is " + common_divisor
    + " and the greatest common divisor is " + 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;
}
测试 demo3: 该实例使用工作线程做后台数值(算法)计算解析
我们使用 Worker()构造函数创建一个新的工作线程,它会返回一个代表此线程本身的线程对象。接下来我们使用这个线程对象与后台脚本进行通信。线程对象有两个主要事件处理器:postMessage 和 onmessage 。postMessage 用来向后台脚本发送消息,onmessage 用以接收从后台脚本中传递过来的消息。
在后台工作线程代码片段中,我们定一个两个 JavaScript 函数,一个是 function divisor:用以计算最大公约数,一个是 function multiple:用以计算最小公倍数。同时工作线程的 onmessage 事件处理器用以接收从主页面中传递过来的数值,然后把这两个数值传递到 function calculate 用以计算。当计算完成后,调用事件处理器 postMessage,把计算结果发送到主页面。

demo3.html

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title> 使用共享线程处理多用户并发连接</title>
</head>

<body onload=''>
  <output id='response_from_worker'>
    使用共享线程处理多用户并发连接
  </output>
  send instructions to shared worker:
  <input type="text" autofocus oninput="postMessageToSharedWorker(this);return false;">
  </input>

  <script>
    var worker = new SharedWorker('./script/demo_three.js');
    var log = document.getElementById('response_from_worker');
    worker.port.addEventListener('message', function (e) {
      debugger
      log.textContent = e.data;
    }, false);
    worker.port.start();
    worker.port.postMessage('ping from user web page..');

    function postMessageToSharedWorker(input) {
      debugger
      var instructions = { instruction: input.value };
      worker.port.postMessage(instructions);
    } 
  </script>

</body>

</html>

demo_three.js

var connect_number = 0;

onconnect = function (e) {
  connect_number = connect_number + 1;
  var port = e.ports[0];
  port.postMessage('A new connection! The current connection number is '
    + connect_number);
  port.onmessage = function (e) {
    var instruction = e.data.instruction;
    var results = execute_instruction(instruction);
    port.postMessage('Request: ' + instruction + ' Response ' + results
      + ' from shared worker...');
  };
};

function execute_instruction(instruction) {
  var result_value;
  return result_value
}

第三方框架引用: 熟悉Angular的朋友应该都清楚,Angular1最被大家诟病就是它的脏检查机制,当scope的数据量过多时会严重影响性能。而Angular2正是借助WebWorker来把繁重的计算工作移入辅助线程,让界面线程不受影响。

Web Workers的更多方法和用法请查找MDN