前言

最近的部门项目和参赛项目都打算在Web前端实现一个计算量很大的模型,但由于Web渲染的特性,如果其中有一个计算量很大的函数,就会阻塞事件队列,导致界面像卡死了一样无法操作,这会严重影响使用体验,所以就在寻找多线程的解决方案,本文会介绍一些个人的踩坑经历和对部分多线程解决方案的理解。


在此先感谢知乎@湖心月 大佬,他写的Comlink的文章给了我很大的启发,并在我使用Comlink踩坑的时候指明了解决问题的方向,真的非常的感谢!
文中使用到的资料会进行标注,文末会放上参考资料的列表。

一、Web Worker

Web Worker是我所接触到的Web多线程解决方案里最早的一个,也是后来其他解决方案的原典。
下面是Web Worker的部分用法示例,
主线程:

//主线程采用new命令,调用Worker()构造函数,新建一个 Worker 线程
var worker = new Worker('work.js');
//然后,主线程调用worker.postMessage()方法,向 Worker 发消息
worker.postMessage('Hello World');
worker.postMessage({method: 'echo', args: ['Work']});
//接着,主线程通过worker.onmessage指定监听函数,接收子线程发回来的消息
worker.onmessage = function (event) {
  console.log('Received message ' + event.data);
  doSomething();
}
function doSomething() {
  // 执行任务
  worker.postMessage('Work done!');
}

//Worker 完成任务以后,主线程就可以把它关掉
worker.terminate();

Worker线程:

//Worker 线程内部需要有一个监听函数,监听message事件
self.addEventListener('message', function (e) {
  self.postMessage('You said: ' + e.data);
}, false);

//上面代码中,self代表子线程自身,即子线程的全局对象。因此,等同于下面两种写法
// 写法一
this.addEventListener('message', function (e) {
  this.postMessage('You said: ' + e.data);
}, false);
// 写法二
addEventListener('message', function (e) {
  postMessage('You said: ' + e.data);
}, false);

//根据主线程发来的数据,Worker 线程可以调用不同的方法
self.addEventListener('message', function (e) {
  var data = e.data;
  switch (data.cmd) {
    case 'start':
      self.postMessage('WORKER STARTED: ' + data.msg);
      break;
    case 'stop':
      self.postMessage('WORKER STOPPED: ' + data.msg);
      self.close(); // Terminates the worker.
      break;
    default:
      self.postMessage('Unknown command: ' + data.msg);
  };
}, false);

Web Worker虽然看上去比较简洁明了,但实际使用中对于我这样从Vue开始接触前端开发的萌新来说,还是比较抽象的,而且使用中也有不少限制,如下文所示[1]:

(1)同源限制

分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。

(2)DOM 限制

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

(3)通信联系

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

(4)脚本限制

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

(5)文件限制

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

不过Web Worker由于提出的时间早,所以兼容性很好,如果想兼容低版本浏览器的话,Worker不失为一个良好的解决方案。

二、Comlink

Worker的踩坑导致我一度对多线程方案丧失了信心,但在模型即将完成之际,又重燃了实现多线程的想法,于是碰到了让我大受启发的@湖心月 大佬写的关于Comlink的文章。
Comlink是Google的Surma所设计的更现代化的API,更符合现在开发人员的使用习惯。@湖心月 文中对Worker的优缺点有着更深层次的理解和描述,并介绍了Comlink的用例。
以下是Comlink的官方给出的一个示例,一个简单的计数器。

// main.js
import * as Comlink from "https://unpkg.com/comlink?module";const worker = new Worker("worker.js");
// This `state` variable actually lives in the worker!
const state = await Comlink.wrap(worker);
await state.inc();
console.log(await state.currentCount);// worker.js
import * as Comlink from "https://unpkg.com/comlink?module";const state = {
  currentCount: 0,inc() {
    this.currentCount++;
  }
}
​
Comlink.expose(state);

示例就是如此简洁,看完很快就能上手。

@湖心月 大佬对Comlink的部分描述

Comlink精妙的地方,我个人认为在于将数据传递的操作变成了一个异步的操作,这样我们就能很好的利用ES6所提供的async/await语法糖,将数据的传递与接收逻辑写得非常简洁优雅。开发者不需要再去考虑事件订阅所带来的各种复杂度。

他的文章中还有在Vue或Vue+Vuex环境下的例子,感兴趣的可以去看看,链接在文末。

三、Comlink-loader

Comlink-loader是尝试在Vue-cli构建的项目下引入Comlink失败而找到的办法(来自于@湖心月 大佬的提示)
下面就直接介绍comlink-loader的使用方法

安装

npm install -D comlink-loader

使用

代码如下(示例):

//TestFun.js
export class MyClass{
    async inc(count) {
        for (let i = 0; i<10000; i++){
            for(let j = 0; j<30000; j++){
                count += i+j
            }
        }
        return count;
    }
}

//main
import MyWorker from 'comlink-loader!../../TestFun'
    export default {
        data() {
            return {
                counter: 0,
                continuedCount: 0
            }
        },
        methods: {
            countP1(){
                this.counter++
            },
            async continuedCountP(){
                const count = this.continuedCount;
                const inst = new MyWorker();
                const obj = await new inst.MyClass();
                this.continuedCount = await obj.inc(count)
            }
        },
    }

‘comlink-loader!../…/TestFun’中的’…/…/TestFun’是TestFun.js与main.vue的相对位置,TestFun不需要加后缀。为了防止有像我一样的小白看不懂,特地说明一下。
此处介绍的是comlink-loader的默认使用方式,comlink-loader还有另一种Singleton Mode(单例模式),可到git仓库中查看。


总结

本文仅仅简单介绍了comlink-loader的使用,而关于Worker的包还有vue-worker、worker-loader等内容,感兴趣的可以自行去了解。
目前仅实现了简单的例子,日后把模型给应用上之后,也许会来更新一些使用中碰到的坑。

参考资料

[1]Web Worker 使用教程@阮一峰
http://www.ruanyifeng.com/blog/2018/07/web-worker.html
[2]如何无痛的为你的前端项目引入多线程@湖心月
https://zhuanlan.zhihu.com/p/146374834
[3]vue-worker:在vue中方便使用web worker
https://www.tangshuang.net/3657.html
[4]https://github.com/GoogleChromeLabs/comlink-loader

Logo

前往低代码交流专区

更多推荐