JavaScript 的多线程,Worker 和 SharedWorker

随着业务的发展,前端脚本处理大量数据的情况已经变的越来越常见了。但是,当我们尝试用前端脚本处理大量数据的时候,会发现这样的计算可能占用过多资源导致页面前端UI线程的假死。在这背景下,为了让页面有更好的体验,W3C在HTML5的规范中提出了Web Worker的概念,允许浏览器通过后台线程来执行复杂的事物或者逻辑。

1. 大量计算导致的页面假死

先让我们看一下什么情况下会导致页面假死。

假死Demo传送门 (代码: 1.withoutWorkers.html

这个页面中,我们可以看到一个不停更新的时间戳,以及一个按钮。

当我们点击按钮之后,就会通过递归的方法计算第43个斐波那契数列,由于这个计算需要大量的计算资源,在点击之后,我们发现页面假死了,一段时间之后,才弹出如下的结果:

斐波那契数列结果

计算斐波那契数列仅仅是一个个例,类似这样的计算其实还是很常见的,那么我们怎么来解决这个问题呢?

2. 初识 Web Worker

让我们先看Demo:

DEMO:通过Worker计算斐波那契数列 (代码:1. withWorkers.html 2. workers.js

打开页面之后发现页面第一个Demo几乎一致,但是当我们点击按钮的时候,神奇的事情发生了。页面的UI线程并没有受到影响,一段时间之后,页面同样弹出了同样的计算结果。这就是worker的神奇之处。

2.1 worker的初始化

上述的Demo中我们使用了Workder,那么我们一起来看一下如何使用Workder。

Worker可以通过Worker()构造函数来构建,Worker接收一个参数,该参数为线程脚本的地址,但是必须遵守同源策略,比如在下面的代码中我们使用同目录下的workers.js文件创建一个Worker对象,初始化完成之后,浏览器就会用后台线程运行我们的workers.js文件中的代码。

// 初始化worker
let worker = new Worker('workers.js');

2.2 worker的消息传递

创建新的后台线程之后,我们迫切关注的是如何和后台线程进行通信,这时候就要用到postMessage方法了。

2.2.1 postMessage(Object aMessage [,sequence transferList])

在主线程中,我们可以通过 worker.postMessage 向后台线程传递数据,postMessage接受2个参数,第一个为我们要传递的消息对象,第二个参数为一个数组,用来转让对象的所有权。

button.addEventListener('click', function () {
    // 向worker传递消息
    worker.postMessage('start');
});

上述代码中,我们在点击按钮之后主线程中向子线程传递了一个内容为'strat'的字符串对象。此时如果想在子线程中处理这些传递进来的数据的话,我们需要在子线程中通过监听onmessage来捕获这些数据,onmessage接收一个MessageEvent 对象,这里我们先关注 MessageEvent 的 data 属性,data就是我们通过主线程传递进来的数据,其他的MessageEvent属性(如origin、source等)大家可以自行了解。

// workers.js
onmessage = function (messageEvent) {
    switch (messageEvent.data) {
        case 'start':
            let result = fabonacci(42);
            postMessage(result);
    }
}

上述代码中我们通过onmessage监听父线程传递进来的消息,如果消息内容是'start'就执行计算,计算完成之后我们再次通过postMessage把结果传递回去,这里我们需要注意的是,在主线程中我们需要通过指定worker来调用postMessage (worker.postMessage),而在子线程中我们则不需要指定worker,直接调用postMessage方法就可以了。

回到我们的主线程的代码,在主线程中,我们可以通过worker.onmessage监听子线程传递过来的数据。

// 监听workder返回的消息
worker.onmessage = function (messageEvent) {
    alert('计算结束,结果为' + messageEvent.data + ',用时' + (new Date() - time) + 'ms'); 
}

2.2.2 terminate()

如果我们想终止我们的worker,可以直接调用terminate方法:

var myWorker = new Worker('worker.js');
myWorker.terminate();

3. ShareWorker

worker可以帮助我们创建后台线程,用来优化我们的体验,那么ShareWorker是用来做什么的呢?顾名思义,通过shareWorker我们可以创建共享线程,即不同的页面使用同一个ShareWorkder。

我们来看页面:

ShareWorker Demo (代码 1. shareWorker.html 2. sharedworkers.js)

页面上有个按钮,每次点击计数器就会加1。我们先点若干下,然后再新开一个窗口继续点击,你会发现第二个页面的计数器是接着第一个页面的,再新开一个页面,点击之后也会发现计数器仍然是继续累加的。我们再做另外一个实验,打开多个窗口,点击几下,然后把所有的窗口都关闭,再打开一个新的窗口,这时候你会发现点击的计数重新开始了。

简单的总结就是,shareWorkder会在页面存在的生命周期内创建一个唯一的线程,并且开启多个页面也只会使用同一个进程,当所有的页面都关闭之后该线程也会随之被结束。

3.1 shareWorker的创建

类似于worker,创建shareWorker也非常简单,他的语法为:

new SharedWorker(aURL, name);

其中aURL是脚本的地址,name是子线程的名称,相同名字的线程会被共享(但必须符合同源策略)

在我们的Demo中,我们通过如下的代码创建shaerdWorker:

let worker = new SharedWorker('sharedworkers.js');

3.2 shareWorker的消息传递

shareWorker实现于SharedWorkerGlobalScope,这个和worker有一点的使用上的区别。

在shareWorker中,如果我们想要拿到Message对象的话,需要通过 shareWorker.port 获得,其中的postMessage和onmessage的用法同workder,如下:。

// 传递strat指令
worker.port.postMessage('start');
// 接收子线程的数据
worker.port.onmessage = function (val) {
    timeDom.innerHTML = val.data
}

这里需要注意的是,如果你是通过addEventListener绑定message事件的话(而不是.onmessage)这时候需要手动的调用 port 的 .start()方法。

worker.port.start();
worker.port.addEventListener('message', function(e) {
    // ... 
});

同样的在子线程中我们的使用也有些小的区别,我们可以通过对onconnect事件获取到新的shareWorker的连接,然后在connect的参数中的ports字段我们可以拿到MessageEvent,这时候我们就可以使用 .onmessage 和 .postMessage 来处理和传递我们的数据了。

// sharedworkers.js
onconnect = function (e) {
    // 通过 e.ports 拿到 port
    var port = e.ports[0];

    // port.onmessage 监听父线程的消息
    port.onmessage = function () {
        // port.postMessage 向父线程传递消息
        port.postMessage(a++)
    }
}

下面我们来完整的分析一下我们的 sharedworkders.js 的内容,在进程启动的时候,我们会通过var a = 1;创建一个变量,然后在每次的父线程发来消息的时候就会把a加上一,这样就能实现所有的页面共享同一个变量a。

3.3 shareWorker的调试

大家尝试开发sharedworkders的时候,可能会发现这样的问题,普通workder可以很方便的通过控制台来调试,但是sharedworkders是后台线程,很难去调试,我在开发的时候就遇到下面的问题:

  1. sharedWorkder 的 console 信息不会在控制台中展示,怎么去打断点?
  2. sharedWorkder 的缓存比较严重,修改了代码之后浏览器还是会运行之前的脚本,缓存该如何清除?

其实在chrome浏览器中我们可以通过chrome://inspect/来很方便的调试shareWorker。

shareWorker的调试

打开页面之后,选择 Shared workres 我们就可以看到所有的正在运行的shareWorker了,这里有2个按钮 一个是inspect另外一个是terminate,terminate很容易理解,就是结束线程。

这里我们尝试使用inspect。

shareWorker的调试2

点击之后,你会发现,啊哈~ 原来一切都在这里,这里会弹出一个和我们常用的控制台一样的窗口,这时候大家应该是如鱼得水般的愉悦了。

总结

Worker 和 SharedWorker 给web应用提供了更大的空间,开发人员可以利用它开发出更加友好、高效的应用,同时我们也可以通过SharedWorker在不同的页面中进行数据的共享和传递,给我们的开发带来了新的思路。

Write a response...
Mofei Zhu
publish
mf
2017-05-22 19:18
NB了
0
 Replay
@mf  
Replay
Mofei
2017-05-09 11:17
@剧中人  谢谢陈老师夸奖,[捂脸]

 

0
 Replay
@Mofei  
Replay
剧中人
2017-05-09 06:35
棒,又走在了时代的前列腺上!
1
 Replay
@剧中人  
Replay
falost
2017-05-07 14:46
前排支持下,棒棒哒
0
 Replay
@falost  
Replay