边学边译JS工作机制--7.WebWorker和5个使用场景

1,334 阅读7分钟

本系列其他译文请看JS工作机制 - 小白1991的专栏 - 掘金 (juejin.cn)

本文推荐指数: 2
WebWorker使用的场景是有限的,如果不是计算密集型任务可能很少用到。

这次来看看WebWorker:我们会总览一下不同种类的worker,它们是如何搭配的,以及在不同的场景下表现出来的优缺点。最后,提供5个挑选合适woker的场景。 我们知道,虽然JS是单线程的,但是其实可以进行异步编程。

异步编程的局限性

在EventLoop中调度延迟执行的代码,这种方式的异步编程让你的UI响应性更好,因为允许UI渲染优先执行

常见的异步编程是AJAX 请求。因为请求要花费很多时间,使用异步的话,在等待回应的时间,可以执行其他代码

// This is assuming that you're using jQuery
jQuery.ajax({
    url: 'https://api.example.com/endpoint',
    success: function(response) {
        // Code to be executed when a response arrives.
    }
});

但这依然是个问题,因为AJAX请求是被浏览器的WEB API来处理的,如果想让其他的代码异步执行怎么办?比如,请求的成功回调是一个非常重的CPU密集型代码怎么办呢?(计算机任务一般分两种,一种是IO密集型,一种是CPU密集型(什么是IO密集型和CPU密集型)

var result = performCPUIntensiveCalculation();

如果performCPUIntensiveCalculation不是一个HTTP请求,而是一个阻塞性代码(例如一个很大的for循环),这样就没办法去释放EventLoop或者解冻浏览器UI----所有的用户行为都不会被响应了。
可见异步函数只是就解决了一小部分单线程引起的问题。
有时候,你可以使用setTimeout. 来执行耗时很久的计算,从而让UI不那么卡顿。把一些复杂的运算,拆分到多个独立的setTimeout中,这样就可以在EnenvtLoop的不同位置去执行他们,从而让给UI的渲染和响应争取到时间。 看一个简单的例子,这个例子是计算一个数组中所有数字的平均值:

function average(numbers) {
    var len = numbers.length,
        sum = 0,
        i;

    if (len === 0) {
        return 0;
    } 
    
    for (i = 0; i < len; i++) {
        sum += numbers[i];
    }
   
    return sum / len;
}

如果改成异步的话可以是这样:

function averageAsync(numbers, callback) {
    var len = numbers.length,
        sum = 0;

    if (len === 0) {
        return 0;
    } 

    function calculateSumAsync(i) {
        if (i < len) {
            // Put the next function call on the event loop.
            setTimeout(function() {
                sum += numbers[i];
                calculateSumAsync(i + 1);
            }, 0);
        } else {
            // The end of the array is reached so we're invoking the callback.
            callback(sum / len);
        }
    }

    calculateSumAsync(0);
}

使用setTimeout方法,在EventLoop中添加每一步的运算。在每次计算之间,就有足够的时间,去进行其他的运算,以及浏览器的解冻。

Web Workers 会解决这些

H5给我们带来了很多惊喜,比如:

  • SSE (前一章讨论过了)
  • 地理位置
  • 应用换窜
  • 本地存储
  • 拖拽
  • Web Workers

Web Worker 是浏览器内置线程,你可以用它来执行JS代码。

JS整个范式是基于单线程环境的,WebWorker可以解决这个问题。 Web Worker允许开发者把耗时久和计算密集型的任务放置到后台运行,而不会阻塞UI。也不需要使用setTimeout 这种奇巧淫技来处理了。 看一下这个简单的demo ,理解一下使用和未使用WebWorker来做数组排序的区别。

Web Worker总览

Web Workers 允许你去处理长耗时的脚本,而不阻塞UI----他们是并行执行的。Web Workers是真正的多线程。但是JS不是单线程么?

你应该意识到JS其实并没有定义线程模型。 Web Workers 不是JS的一部分,他们是浏览器的特性,只是可以通过JS调用. 大多数浏览器历史上是单线程的,大多JS实现是在浏览器中发生的。WebWorkers 没有在Node中实现--Node具有一个“cluster” 或者 “child_process”的概念,略有不同。 注意,规范中声明了三种web worker的类型:

Dedicated Workers

Dedicated Web Worker是被主线程实例化的,值可以跟他通信。浏览器的支持情况如下:

image.png

Shared Workers

Shared workers 可以被同源的线程访问 (包括不同的tab, iframes 或者其他 shared workers).浏览器的支持情况如下:

image.png

Service Workers

Service Worker 是一个由事件驱动的 worker,它由源和路径组成。它可以控制它关联的网页,解释且修改导航,资源的请求,以及一种非常细粒度的方式来缓存资源以让你非常灵活地控制程序在某些情况下的行为(比如网络不可用)。浏览器的支持情况如下:

本篇文章,我们将会专注于 Dedicated Workers 并以 『Web Workers』或者 『Workers』来称呼它。

Web Workers 是如何运行的

Web Workers 的实现,是像.js 文件那样,在页面中包含了异步的HTTP请求。这些请求对开发者完全隐藏的。

Workers 利用线程类似的信息传递来达到并行效果。他们非常合适去保持UI的更新,响应。

Web Workers 运行在一个跟浏览器分离得线程中。因此,他们执行的代码需要被放在一个独立的文件中。这一点很重要。 看看一个基础的woker是如何创建的:

var worker = new Worker('task.js');

如果"task.js"文件是可访问的,浏览器将会启动一个新线程去异步下载。一旦下载完成,将会启动woker去执行它。

如果文件路径返回了404,worker就是静默的失败(不抛出异常)。 为了开始创建一个woker,你需要调用postMessage方法:

worker.postMessage();

Web Worker 通信

为了在Worker和创建它的页面之间通信,你需要使用postMessage 方法或者一个Broadcast Channel.

postMessage 方法

新的浏览器支持JSON 对象作为该方法第一个参数,老的浏览器只支持string 看一个简单的例子,看页面如何创建一个woker并通过传递JSON对象与之来回通信,传递字符串是一样的 看一段 HTML 页面的代码 :

<button onclick="startComputation()">Start computation</button>

<script>
  function startComputation() {
    worker.postMessage({'cmd': 'average', 'data': [1, 2, 3, 4]});
  }

  var worker = new Worker('doWork.js');

  worker.addEventListener('message', function(e) {
    console.log(e.data);
  }, false);
  
</script>

worker部分代码像这样:

self.addEventListener('message', function(e) {
  var data = e.data;
  switch (data.cmd) {
    case 'average':
      var result = calculateAverage(data); // Some function that calculates the average from the numeric array.
      self.postMessage(result);
      break;
    default:
      self.postMessage('Unknown command');
  }
}, false);

当button 点击之后,主页面会调用postMessageworker.postMessage这一行传递JSON对象给woker,并将 cmd 和 data各自的值和key添加进去。 worker 将会通过定义的message句柄来处理信息

当message到达,worker 实际执行了计算,而不会阻塞event loop。worker 检查传递的事件e ,然后就像标准JS函数那样执行。当一切结束,函数结果将会返回给主页面。

在Worker的上下文中,self 和 this都指向worker的全局。

两种办法停止一个 worker: 主页面调用 worker.terminate() 或者worker本身调用 self.close().

Broadcast Channel

 Broadcast Channel 是为了通信更常用的API。它可以让我们广播信息到同源的所有上下文。同源的所有的浏览器tab,iframes或者worker 都可以收发信息

// Connection to a broadcast channel
var bc = new BroadcastChannel('test_channel');

// Example of sending of a simple message
bc.postMessage('This is a test message.');

// Example of a simple event handler that only
// logs the message to the console
bc.onmessage = function (e) { 
  console.log(e.data); 
}

// Disconnect the channel
bc.close()

通过下图,你可以更清晰地看到Broadcast Channels是什么样子的

image.png

不过浏览器对Broadcast Channel的支持性不是很好:

image.png

传递信息的大小

有两种方式传递信息给 Web Workers:

  • 复制消息:  消息是序列化的,复制过的,发送,然后在另一端反序列化。页面和worker不会共享相同的实例,所以每次传递的时候都要创建副本。大多数浏览器是通过在任何一端自动进行 JSON 编码/解码消息值来实现这一功能。正如所预料的那样,这些对于数据的操作显著增加了消息传送的性能开销。消息越大,传送的时间越长。
  • 传递消息: 源发送者在发送之后不能再次使用,数据传递时即时的。唯一的限制是ArrayBuffer 

Web Worker的可用功能

Web Workers只能获取JS的一部分功能,因为它的多线程特性 。这里是一些功能列表:

  •  navigator 对象
  •  location 对象 (只读)
  • XMLHttpRequest
  • setTimeout()/clearTimeout() and setInterval()/clearInterval()
  • 应用缓存
  • 使用 importScripts()导入外部的脚本
  • 创建其他worker

Web Worker 局限

很遗憾WebWoker不能获得一些比较关键的JS特性

  • DOM (线程不安全)
  •  window 对象
  •  document 对象
  •  parent 对象 由此看,Worker 不能操作DOM。一旦你学会正确使用Worker,你将会像使用计算器一样开始使用它们,同时其他代码可以执行UI操作。Workers处理所有的重运算,然后传递结果到创建它的页面上,以便进行页面上的更新

异常处理

As with any JavaScript code, you’ll want to handle any errors that are thrown in your Web Workers. If an error occurs while a worker is executing, the ErrorEvent is fired. The interface contains three useful properties for figuring out what went wrong: 就像任何JS代码那样,你可以在你的Worker中处理所有的异常。如果当worker执行时发生了异常,ErrorEvent 会被触发。这个接口包含三个有用的属性来指出异常的信息:

  • filename - 发生错误的脚本名
  • lineno - 错误发生的位置
  • message - 错误信息的描述

举个栗子:

function onError(e) {
  console.log('Line: ' + e.lineno);
  console.log('In: ' + e.filename);
  console.log('Message: ' + e.message);
}

var worker = new Worker('workerWithError.js');
worker.addEventListener('error', onError, false);
worker.postMessage(); // Start worker without a message.
self.addEventListener('message', function(e) {
  postMessage(x * 2); // Intentional error. 'x' is not defined.
};

这里我们创建了一个worker然后开始监听error事件

worker (在 workerWithError.js中) 内部我们创建了一个内部异常---让X加2,但是X是未定义的。这个异常传递到初始化它的脚本,然后onError被触发了。

使用 Web Workers的绝佳场景

我们已经讨论了Worker的优势和劣势,来看看有哪些适合它的场景

  • ** 光线追踪**: 光线追踪是一种渲染技术,将跟踪的光线路径作为纹理生成图像。光线追踪是一个非常重的CPU密集型计算,需要模拟一些效果,像反射,折射,材质等等。所有这些计算逻辑需要被添加到Worker,以避免UI线程的阻塞。更棒的是,你可以在几个worker之间很随意的切割图像渲染(可以利用多个CPU)。这里是一个简单的例子 — nerget.com/rayjs-mt/ra….

  • 加密: 端到端的加密越来越流行了,因为个人信息和敏感信息的要求越来越高。加密是相当耗时的,尤其是如果加密的数据量很大(比如,发往服务器前加密数据)。这是使用worker的绝佳场景,因为它不需要访问DOM。一旦进入worker,它对用户是无感的,也不会影响用户体验。

  • 预加载数据: 为了优化你的代码,提升加载事件,你可以使用worker去提前加载和存储数据,这样你可以在需要的时候使用它们。这种场景下,Worker的表现是非常棒的。

  • 渐进式网络应用: 即使网络不稳定,这些应用也可以很快加载。也就是说数据需要被存储在浏览器本地。这里就需要IndexDB和一些类似的API了。这都是客户端存储需要的。为了不阻塞UI,这些东西都要放在Worker中执行。当使用 IndexDB的时候,可以不使用 workers 而使用其异步接口,但是之前它也含有同步接口(可能会再次引入 ),这时候就必须在 workers 中使用 IndexDB。

  • 拼写检查: 一个常见的拼写检查是这么工作的---程序读取带着正确拼写的字典文件。这个字典被转换长一个搜查树好让文本搜索更有效,当cheker提供了一个单词,程序会检查它是否在树中。如果不在,则会通过提供替代的字符为用户提供替代的拼写,并检查它是否是用户想写的。这个过程可以轻松的转给Worker来做,这样当用户输入任何单词或者句子,都不会阻塞UI,同时执行所有的搜索和拼写建议。