给 JavaScript 插上多线程的翅膀 —— Web Worker 的 Promise 化实践

栏目: IT技术 · 发布时间: 4年前

内容简介:众所周知,JavaScript 这门语言的一大特点就是单线程,即同一时间只能同步处理一件事情,这也是这门语言衍生出的 nodeJS 被各后端大佬诟病的很重要的一点。然而,JavaScript 在设计之初,其实是故意被设计成单线程语言的,这是由于它当时的主要用途决定的。JavaScript 最初的设计初衷是完成页面与用户的交互,操作 DOM 或者 BOM 元素,此时如果一味地追求效率使用多线程的话,会带来资源抢占,数据同步等等问题,因此必须规定,同一时间只有一个线程能直接操作页面元素,以保证系统的稳定性以及安

Web Worker 介绍

众所周知,JavaScript 这门语言的一大特点就是单线程,即同一时间只能同步处理一件事情,这也是这门语言衍生出的 nodeJS 被各后端大佬诟病的很重要的一点。

然而,JavaScript 在设计之初,其实是故意被设计成单线程语言的,这是由于它当时的主要用途决定的。

JavaScript 最初的设计初衷是完成页面与用户的交互,操作 DOM 或者 BOM 元素,此时如果一味地追求效率使用多线程的话,会带来资源抢占,数据同步等等问题,因此必须规定,同一时间只有一个线程能直接操作页面元素,以保证系统的稳定性以及安全性。

尽管如此,但是 JavaScript 并不是只能线性处理任务。JS 拥有消息队列和事件循环机制,通过异步处理消息的能力来实现并发。在高 I/O 型并发事务处理的过程中,由于不需要手动生成与销毁线程以及占用额外管理线程的空间,性能表现及为优异。因此,nodeJS 作为 JavaScript 在服务端的探索者,在处理高并发网络请求的优势极为明显。

尽管 JavaScript 通过异步机制完美解决了高 I/O 性能的问题,但 JavaScript 单线程执行的本质还是没有变的。因此缺点就显而易见了,那就是处理 CPU 密集型的事务时没有办法充分调动现代多核心多线程机器的运算资源。

在现代大型前端项目中,随着代码的复杂程度越来越高,本地的计算型事务也在变得繁重,而运行在单线程下 JS 项目必定会忙于处理计算而无暇顾及用户接下来的频繁操作,造成卡顿等不太好的用户体验,更严重的情况是,当计算型事务过多时还有可能因为资源被占满带来网页无响应的卡死现象。因此,Web 项目的本地多线程运算能力势在必行,由此,Web Worker 应运而生了。

Web Worker 是 HTML5 中推出的标准,官方是这样定义它的:

Web Workers makes it possible to run a script operation in a background thread separate from the main execution thread of a web application.

它允许 JavaScript 脚本创建多个线程,从而充分利用 CPU 的多核计算能力,不会阻塞主线程 (一般指 UI 渲染线程) 的运行。

Web Worker 虽然是 HTML5 标准,但其实早在 2009 年 W3C 就已经提出了草案,因此它的兼容性良好,基本覆盖了所有主流浏览器。

给 JavaScript 插上多线程的翅膀 —— Web Worker 的 Promise 化实践

Web Worker 的局限

需要注意的是,Web Worker 本质上并没有突破 JavaScript 的单线程的性质。

事实上,Web Worker 脚本中的代码并不能直接操作 DOM 节点,并且不能使用绝大多数 BOM API。它的全局环境是 DedicatedWorkerGlobalScope 而并不是 Window。运行 Worker 的实际上是一个沙箱,跑的是与主线程完全独立 JavaScript 文件。

Worker 做的这些限制,实际上也是为了避免文章开头说过的抢占问题。它更多的使用场景是作为主线程的附属,完成高 CPU 计算型的数据处理,再通过线程间通信将执行结果传回给主线程。在整个过程中,主线程仍然能正常地相应用户操作,从而很好地避免页面的卡顿现象。

Web Worker 的使用

新建

目前 Web Worker 的浏览器支持已经较为完善,基本上直接传入 Worker 脚本的 URI 并实例化即可使用。

/* main.js */
 
const worker = new Worker("./worker.js")

通信

Worker 与主线程之间的通信只需要各有两个 API:onmessage/addEventListener 与 postMessage 即可完成收发消息的交互。

/* main.js */
const worker = new Worker("./worker.js");
 
// 主线程发送消息
worker.postMessage({ data: 'mainthread send data' });
 
// 主线程接收消息
worker.onmessage = (e) => {
    const { data } = e;
    if (!data) return;
    console.log(data);
}
/* worker.js */
// worker线程接收消息
self.addEventListener('message', (e) => {
    const { data } = e;
    if (!data) return;
    // worker线程发送消息
    self.postMessage({data: 'worker received data'})
});

注:Worker 中,this.xx, self.xx 与直接使用 xx,其作用域都指向 worker 的全局变量 DedicatedWorkerGlobalScope ,可以互换。

销毁

Worker 的销毁方式有两种,既能在内部主动销毁,也能够被主线程通知销毁。

/* main.js */
worker.terminate();
/* worker.js */
self.close();

进阶:让通信方式 Promise 化

根据上一节,我们已经能够简单地使用 Worker 的 API 来获取浏览器多线程计算的能力,但是它离工程化的应用还缺少了一些易用性,比如我们多数时候需要使用到的异步相应。接下来我们就来做这件事情。

首先我们需要一个异步回调集合 actionHandlerMap,用于存放等待 Worker 响应的 Promise resolve 方法,其 key 值可以用通信中的某一 id 指定(保证其唯一性即可)。接着我们需要封装一下原生的 postMessage 与 onmessage 方法。

我们在原生的 postMessage 发送的信息中加入 id,并将当前的 Promise 的 resolve 方法放入 actionHandlerMap,等待 Worker 返回结果后触发。

对于 onmessage 的监听,在接收到 Worker 发送过来的响应之后,匹配响应的 Promise 并执行 .then() 方法,完成后删除集合中的 Promise resolve 函数。

/* main.js */
let fakeId = 0;
class MainThreadController {
    constructor(options) {
        this.worker = new Worker(options.workerUrl, { name: options.workerName });
 
        // 等待异步回调集合
        this.actionHandlerMap = {};
 
        this.worker.onmessage = this.onmessage.bind(this);
    }
 
    onmessage(e) {
        const { id, response } = e.data;
        if(!this.actionHandlerMap[id]) return;
 
        // 执行相应的 Promise resolve
        this.actionHandlerMap[id].call(this, response);
        delete this.actionHandlerMap[id];
    }
 
    postMessage(action) {
        // 实际使用中,可以指定或生成一个业务 id 作为 key 值
        const id = fakeId++;
        return new Promise((resolve, reject) => {
            const message = {
                id,
                ...action,
            };
            this.worker.postMessage(message);
            this.actionHandlerMap[id] = (response) => {
                resolve(response);
            };
        });
    }
}
 
const mainThreadController = new MainThreadController({ workerUrl: './worker.js', workerName: 'test-worker' });
mainThreadController
    .postMessage({
        actionType: 'asyncCalc',
        payload: { msg: 'send messages to worker', params: 1 },
    })
    .then((response) => console.log('message received from worker: ', response.msg));

对于 worker 部分的处理就简单得多,计算处理完毕后,在响应回复中带上请求的 id 即可。

/* worker.js */
class WorkerThreadController {
    constructor() {
        this.worker = self;
 
        // 等待异步回调集合
        this.actionHandlerMap = {};
 
        this.worker.onmessage = this.onmessage.bind(this);
    }
 
    async onmessage(e) {
        const { id, actionType, payload } = e.data;
        switch (actionType) {
            case 'print':
                console.log(payload.msg);
                self.postMessage({ id, response: { msg: 'msg has been print.' } });
                break;
 
            case 'asyncCalc':
                // 构造一个异步处理情形
                const result = await new Promise((resolve) => setTimeout(() => resolve(payload.params * 2), 1000));
                self.postMessage({ id, response: { msg: `the caculated answer is ${result}.` } });
                break;
 
            default:
                break;
        }
    }
}
 
const workerThreadController = new WorkerThreadController();

当然,worker 这边的改造还能够更进一步。我们发现,当 Worker 需要接收的计算种类增多,使用 switch 方式包裹的 onmessage 函数就会变得冗长,使用字符串判断也不够可靠,我们可以用策略模式简单地封装一下 Worker 中的逻辑。

/* worker.js */
// 可以单独抽成一个文件,然后 import 进来
const api = {
    print(payload) {
        console.log(payload.msg);
        return { msg: 'msg has been print.' };
    },
    async asyncCalc(payload) {
        const result = await new Promise((resolve) => setTimeout(() => resolve(payload.params * 2), 1000));
        return { msg: `the caculated answer is ${result}.` };
    },
};
 
class WorkerThreadController {
    constructor() {
        this.worker = self;
 
        // 等待异步回调集合
        this.actionHandlerMap = {};
 
        this.worker.onmessage = this.onmessage.bind(this);
    }
 
    async onmessage(e) {
        const { id, actionType, payload } = e.data;
        const result = await api[actionType].call(this, payload);
        self.postMessage({ id, payload: result });
    }
}
 
const workerThreadController = new WorkerThreadController();

至此,一个简单好用的 Promise Worker 就建立完成了。

当然,为了增加框架的鲁棒性,我们还应该加入类似于错误处理,报错及监控数据上报等等能力。由于不属于本文探讨的范围,这里就先按住不表,有兴趣的读者可以参看 AlloyTeam 最新开源的 alloy-worker 项目,其中对上述存在的问题进行了全面的补足,是一个较为完善的高可用的 Worker 通信框架。

总结

本文对 Web Worker 进行了简要的介绍,包括其能力以及局限性,让读者对 Worker 的使用场景有一个全面的了解。提出了一种封装 Worker 原生 API 使之能被 Promise 化调用的解决方案,并在最后推荐了团队内正在使用的功能完善的成熟解决方案,希望能帮助到近期有兴趣进行 Worker 改造的前端开发者们。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Linux二进制分析

Linux二进制分析

[美]瑞安 奥尼尔 / 棣琦 / 人民邮电出版社 / 2017-12-1 / CNY 59.00

二进制分析属于信息安全业界逆向工程中的一种技术,通过利用可执行的机器代码(二进制)来分析应用程序的控制结构和运行方式,有助于信息安全从业人员更好地分析各种漏洞、病毒以及恶意软件,从而找到相应的解决方案。 本书是一本剖析Linux ELF工作机制的图书,共分为9章,其内容涵盖了Linux环境和相关工具、ELF二进制格式、Linux进程追踪、ELF病毒技术、Linux二进制保护、Linux中的E......一起来看看 《Linux二进制分析》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具