CSDN话题挑战赛第2期 参赛话题:前端技术分享
说在前面
对于需要花费大量时间才能处理的任务,javascript 引擎通常会有两种现象:
- 执行当前任务花费大量的时间,使得无法执行任何其他操作,导致浏览卡顿
- 如果此时回调队列被阻塞的任务过多时,大多数浏览器都会抛出一个提示信息,征求是否要关闭网页
那么,我们如何在不阻塞UI并使浏览器正常响应的情况下执行繁重的代码呢?
引言
javascript 是单线程编程语言,这使得我们开发过程中不必关注因多线程导致的复杂场景(如,死锁)。
单线程意味着某一时刻只能做一件事情! javascript 引擎,以最常见的 v8 举例,内置了 事件循环 Event Loop + 回调队列 Callback Queue 机制,以及通过宏任务 macrotask + 微任务 microtask 来分配执行优先级,来确保高效运行。
因此,解决上述问题,通常有两种方案:
- 异步回调(asynchronous callbacks):依赖第三方服务
- 开启多线程(web worker):本文重点,浏览器提供了相应 web api
关于「JavaScript的工作原理」「Event loop及macrotask & microtask」相关内容,可阅读下述文章:
Web Workers
worker 的一个优势在于能够执行处理器密集型的运算而不会阻塞 UI 线程。
web workers 浏览器整体兼容性很好,为我们大面积使用奠定了基础~~~ 在一个 worker 中最主要的是不能直接影响父页面,包括操作父页面的节点以及使用页面中的对象。只能间接地实现,通过 DedicatedWorkerGlobalScope.postMessage 回传消息给主脚本,然后从主脚本那里执行操作或变化。
worker 的优势明显,但在通信上的处理极其繁琐,导致大家使用的频次并不高。
Comlink 解决了通信的问题,其借助 Proxy 可以忽略所有繁琐的通信细节(无需考虑事件订阅所带来的复杂性),极大降低了 Worker 的维护成本。-- RPC方式
RPC 全称是 Remote Procedure Call,即远程过程调用。目的是:让我们调用远程方法像调用本地方法一样,无需了解底层网络技术的协议等。
案例
地址:https://github.com/381510688/practice/tree/master/web-api-test
传统写法
const worker = new Worker('worker.js')
comput.addEventListener('click', function () {
worker.postMessage({
num1: num1.value,
num2: num2.value
})
})
worker.onmessage = function (msgEvent) {
res.innerHTML = msgEvent.data
}
onmessage = function (msgEvent) {
let {num1, num2} = msgEvent.data
postMessage(Number(num1) + Number(num2))
}
尝试保留 add 方法
const worker = new Worker('worker.js')
comput.addEventListener('click', function () {
worker.postMessage('ok')
})
worker.onmessage = function (msgEvent) {
res.innerHTML = msgEvent.data.add(num1.value, num2.value)
}
function add (num1, num2) {
return Number(num1) + Number(num2)
}
onmessage = function (msgEvent) {
postMessage({add: add})
}
UncaughtDOMException: Failed to execute 'postMessage' on 'DedicatedWorkerGlobalScope': function add (num1, num2) {}
Worker.postMessage()
Worker 接口的 postMessage() 方法向worker的内部作用域发送一个消息。接受单个参数(要发送给worker的数据)。数据可以是由结构化克隆算法处理的任何值或JavaScript对象,其包括循环引用。
结构化克隆所不能做到的:
Error 以及 Function 对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出 DATA_CLONE_ERR 的异常。- 企图去克隆 DOM 节点同样会抛出
DATA_CLONE_ERR 异常。 - 对象的某些特定参数也不会被保留
RegExp 对象的 lastIndex 字段不会被保留- 属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write,因为这是默认的情况下。
- 原形链上的属性也不会被追踪以及复制。
comlink 示例
const worker = new Worker("worker.js");
const cw = Comlink.wrap(worker);
comput.addEventListener('click', async function () {
let result = await cw(num1.value, num2.value)
res.innerHTML = result
})
importScripts("https://unpkg.com/comlink/dist/umd/comlink.js");
function add (num1, num2) {
return Number(num1) + Number(num2)
}
Comlink.expose(add);
本质上依然是 MessagePort 消息通讯,不过封装了我们所头疼的“操作判断”,并以一种更优雅的方式(Proxy + Promise)来处理。 Comlink 采用的 RPC 代理方式,并不是传递上下文环境(因为这是非常危险的,而且函数传递时会导致 Uncaught (in promise) DOMException: Failed to execute 'postMessage' on 'Worker': xxx could not be cloned. 报错)。
RPC:Remote Procedure Call,远程过程调用,指调用不同于当前上下文环境的方法,通常可以是不同的线程、域、网络主机,通过提供的接口进行调用。
index.html
import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs"
const worker = new Worker("worker.js")
const cw = Comlink.wrap(worker)
const cpt = await new cw()
comput.addEventListener('click', async function () {
let result = await cpt.add(num1.value, num2.value)
dom.innerHTML = result
})
worker.js
importScripts("https://unpkg.com/comlink/dist/umd/comlink.js")
class Comput {
constructor () {}
add (num1, num2) {
return Number(num1) + Number(num2);
}
sub (num1, num2) {
return Number(num1) - Number(num2);
}
}
Comlink.expose(Comput)
importScripts() 将一个或多个脚本同步导入到工作者的作用域中。隶属于:WorkerGlobalScope 接口。
注意:new Worker('worker.js') scriptURL will be fetched and executed in the background, creating a new global environment for which worker represents the communication channel. – https://html.spec.whatwg.org/multipage/workers.html#dom-worker-dev
总结
Comlink(RPC方式)使我们可以更多的关注业务内容,忽略调用(网络)细则。 客户端应用程序调用本地存根(stub),而不是调用实际代码;服务端应用程序接受参数,通过服务器存根(stub)检索实际代码进行运行。
链接
- JS性能基准:https://github.com/zxch3n/js-performance
- 本机测试:https://zxch3n.github.io/js-performance/
|