Web Workers是什么。
在浏览器端提供并发执行函数能力的API接口。即 Worker 构造函数。
作用:
- 在浏览器端并行执行函数,可以并发执行同步代码。
- 使同步代码和微任务不再阻塞渲染进程。
测试环境效果演示:
测试1并发执行的函数
const simpleFunc = () => {
const now = Date.now()
while (Date.now() - now < 3000) {}
}
测试2并发执行的函数
const lzwDecode = (minCodeSize: number, data: string | Uint8Array) => {
/**
* minCodeSize 取值范围 5~8
* codeSize 6~12
*/
// minCodeSize 编码长度9-12位 minCodeSize值是8~11,表示 0~511至0~4095
// TODO: Now that the GIF parser is a bit different, maybe this should get an array of bytes instead of a String?
let pos = 0 // Maybe this streaming thing should be merged with the Stream?
const readCode = (size: number) => {
let code = 0
for (let i = 0; i < size; i++) {
const val =
data instanceof Uint8Array ? data[pos >> 3] : data.charCodeAt(pos >> 3)
if (val & (1 << (pos & 7))) {
code |= 1 << i
}
pos++
}
return code
}
const output: number[] = []
const clearCode = 1 << minCodeSize // 清除编译表标记,值是原始数据长度加1,就是256/1024/4096
const eoiCode = clearCode + 1 // 编译结束标记,值是清除码+1
let codeSize = minCodeSize + 1
let dict: number[][] = [] // 编译表
const clear = () => {
codeSize = minCodeSize + 1
dict = []
for (let i = 0; i < clearCode; i++) {
dict[i] = [i]
}
dict[clearCode] = []
dict[eoiCode] = null as any
}
let code: number = 0
let prev: number = code
// eslint-disable-next-line no-constant-condition
while (true) {
prev = code
code = readCode(codeSize)
if (code === clearCode) {
clear()
continue
} else if (code === eoiCode) {
break
} else if (code > dict.length) {
throw new Error('Invalid LZW code.')
} else if (code === dict.length) {
dict.push(dict[prev].concat(dict[prev][0]))
} else if (prev !== clearCode) {
dict.push(dict[prev].concat(dict[code][0]))
}
Array.prototype.push.apply(output, dict[code])
if (dict.length === 1 << codeSize && codeSize < 12) {
// If we're at the prev code and codeSize is 12, the next code will be a clearCode, and it'll be 12 bits long.
codeSize++
}
if (pos >= data.length * 8) {
break
}
}
return output
}
const simpleFunc = lzwDecode
web worker实现并发执行代码的原理:
创建 worker 实例会生成一个操作系统级别的线程。利用处理器多核心执行代码实现并发。
- 在浏览器调度下,worker线程会利用2个或多个处理器内核执行任务。在开发工具的性能面板可以查看支持的并发数量。
web worker可以跑满多核处理器吗有人试验过用 4 个 worker 跑满4核心4线程的 i5 2500k
worker 线程的特点和限制
-
worker 线程拥有自己的脚本和独立的运行时上下文。
- 实例化 worker 对象需要传入 js 脚本,可以是一个 js 文件的 url,也可以是一个脚本的字符串。
- worker 线程内的变量和外界是隔绝的。
- worker 线程和外界传递变量的方式是拷贝而不是共享。
-
worker 线程的限制:
- 不能直接操作 DOM 节点,也不能使用window对象的默认方法和属性。
- 因为数据传递是拷贝而不是共享,所以不能传实例对象,this会找不到父类,最好是传JSON。
worker接口的 API 简介: 1兼容性检测、2实例化、3线程通信。
- worker 特性检测:检测浏览器是否存在 Worker 接口和支持并发数量。
worker 特性检测
("Worker" in window); // boolean 检测当前浏览器是否支持 Worker 接口
(navigator.hardwareConcurrency); // number 检测当前浏览器最大支持并发数量,在 Chrome 内返回的是处理器的物理线程数量,比如我的处理是6核12线程的9750h,就返回12
- 用Worker 构造函数实例化 worker 对象。构造函数的大部分的选项兼容性不佳。这里只介绍嵌入式 worker 。
实例化嵌入式worker对象
const messageHandlerString = encodeURIComponent(`
onmessage = (event) => {
console.log(event.data)
}
`) // 创建用于 worker 线程的脚本字符串
const worker = new Worker(
`data:application/javascript,${messageHandlerString}`
) // 实例化一个嵌入式 worker
线程通信API: postMessage 和 message 和消息事件类型 主线程向 worker 线程发消息和接收消息
// 发消息
worker.postMessage({ a: 1, b: 2});
// 接收 worker 线程的消息
worker.onmessage = (event: MessageEvent<{ a: number, b: number }>) => {
const { a, b} = event.data;
}
worker线程向主线程发消息和接受消息
// 发消息
self.postMessage({ a: 1, b: 2});
// 接收主线程的消息
onmessage = (event) => {
console.log(event.data)
}
消息事件对象上除了有传递的数据,还有域名和端口等信息。
关闭线程和错误处理 主线程关闭 worker 线程和错误处理
worker.terminate(); // void 关闭 worker 线程
worker.onerror = (event) => {}; // 错误处理
worker 线程关闭自己
self.close(); // void
嵌入式web worker是什么和嵌入式web worker相比于标准web worker的优劣势
嵌入式web worker 不是像 生成一个专用 worker 那样通过传入一个脚本的 URI 来进行实例化。
生成一个专用_worker
var myWorker = new Worker('worker.js');
生成一个嵌入式_worker
const worker = new Worker(data:application/javascript,${'console.log("hello")'})
- 嵌入式web worker可以在运行时确定哪些函数需要并发执行。原因是实例化worker需要传入脚本文件的路径,而嵌入式worker没有具体的文件,传入的是字符串。
- 嵌入式web worker没有打包的问题,标准web worker需要打包时生成worker的脚本js文件,作为npm包内的代码使用时也不用担心生成worker的脚本文件在node_modules里的问题。
- 嵌入式web worker很容易实例化多个worker。而标准web worker有一个限制是一个worker只能实例化一次,用同一个脚本文件实例化多个worker时,会报引入错误。
3. 兼容性区别:嵌入式web worker不支持safari,标准web worker可以。
封装了个useWorker高阶函数,可以很容易的把一个一般函数转化成易于使用的worker版本。
从这个示例能看出一个现象:
前2次运行时,实际运行时长明显长于设置的运行时间,原因是因为web worker实例化需要时间,实例化的消耗时间因电脑配置而异。
从第三次运行时,实际运行时长也比设置时长要多几毫秒,这几毫秒就是worker线程和主线程通信的消耗时间。
还有一个在日志上没有体现的现象是我的示例函数写的内容是直接阻塞线程一定时长,如果这个函数放到主线程运行,会导致页面卡住,但由于是在worker中运行,所以页面不会卡住。
选择自己封装这个脚本的原因:
- 我希望可以用ts
- 我希望可以动态的直接把一个函数转化成易于使用的worker版本,参数和返回值就像原先的函数一样。这样的话将既有代码改为使用worker执行时,修改的代码会少很多。
- 我不希望用远程调用静态js文件,我希望给项目代码内部的函数甚至是组件内部的函数赋予并发执行能力。
相比于workerpool的特点:
-
主要是选择worker的策略不同。
workerpool在主线程有个任务队列,然后在worker线程的处理是当函数完全执行完时,才通知主线程执行下一个函数。
而我觉得不需要等待这么久,只要函数开始的会阻塞线程的那部分同步代码和微任务代码执行完成后,就可以开始执行下一个函数了。
// execute the function var result = method.apply(method, request.params); // 执行函数 if (isPromise(result)) { // promise returned, resolve this and then return result .then(function (result) { // 函数完全执行完后处理 worker.send({ // 这个 send 实际是 postMessage.bind(worker) id: request.id, result: result, error: null }); currentRequestId = null; }) .catch(function (err) { worker.send({ id: request.id, result: null, error: convertError(err) }); currentRequestId = null; }); } -
这个库提供了一个用的 Promise 是自己模拟 Promise API 实现的对象,它的 resolve 是一个同步函数,而不是微任务。
-
workerpool还不支持ts
存在的尚未解决的问题
1.目前不能用 ... 运算符。数组结构和对象结构可以用。
目前数组插值要用 Array.prototype.push.apply 代替 ... 运算符。对象插值要用 Object.assign 代替 ... 运算符。