先有问题再有答案
js不是单线程嘛 为什么又说多线程?
如何理解H5的多线程?
如何理解h5的worker?
web worker 有什么特点?
有什么使用场景?
有什么限制?
多线程间是如何通讯?
背景
JavaScript 是单线程运行的,这意味着同一个时间只能做一件事情。由于用户交互、网络请求、IO操作等可能会产生阻塞,所以 JavaScript 引入了异步机制和事件循环的概念。
单线程&异步
异步可以看做是单线程面对需要大量耗时操作时的一种解决方案,它将这些耗时操作异步处理,也就是挂起这些事件,进行下一步的操作。当异步事件处理完成后,再通过回调函数的方式进行处理。所以,异步并不是在单线程之外开辟了新的线程,而是通过事件挂起和回调函数的方式使得 JavaScript 引擎不需要阻塞等待,可以先处理其他任务。
单线程&事件循环
事件循环是 JavaScript 实现异步的一种方法,它可以解决 JavaScript 单线程同步执行带来的阻塞问题。事件循环的机制是,将所有的任务分为宏任务和微任务,JavaScript 引擎先执行一个宏任务,紧接着执行所有的微任务,然后再执行下一个宏任务。这种模式不断循环,称为事件循环
单线程&多线程
JavaScript是单线程的,这是因为JavaScript设计初衷是处理简单的DOM操作,复杂和耗时的操作可能导致页面阻塞,影响用户体验。
然而,随着Web应用变得越来越复杂,JavaScript也需要处理更多的复杂且计算密集型任务
。为了解决这个问题,HTML5提出了Web Workers标准,允许JavaScript创建多个工作线程,这些工作线程在后台执行任务,不会影响主线程和用户界面的相应。
我们可以认为,JavaScript的主线程仍然是单线程的,但是通过Web Workers,JavaScript能够创建生成真正的操作系统级别的工作线程,实现多线程编程
。
web worker兼容性
线上完全可用 无兼容性问题。
使用场景
cpu密集型的任务
如果有耗时任务 在主线程中长时间占用cpu,这样的计算可能会阻塞用户界面卡顿,导致无法响应用户的操作。
测试代码如下:
const test = () => { let count = 0; for (let i = 0; i < 1000000000; i++) { count = count + i; } }; test();
运行在主线程的任务性能分析如图:
主线程耗时12s左右 这期间左侧列表是卡死 不能响应用户滚动的
运行在worker线程
性能分析如图-1
可以看到当选中主线程时 js执行仅3毫秒
性能分析如图-2
当选中worker线程时 js执行耗时13s
运行在worker中的整个过程 左侧列表是可以响应用户滚动的, 这就是多线程的魅力 开启了一个工作线程 分担了主线程的耗时任务 有效的避免了卡顿。
周期性后台任务
这部分一般不需要用户交互,但需要定时执行的任务。这类任务通常包括一些定时获取服务器数据、清理缓存、自动保存用户操作等。 例如高精准大定时器功能。
使用限制
同源限制:
Web Workers 只能加载来自同源的脚本。也就是说,你不能在你的网站上使用一个来自其他域的脚本,这是出于安全原因。
不能访问 DOM
Web Workers 不能访问页面的 DOM。这是因为 DOM 操作不能用于多线程环境。目的是防止出现两个线程对 DOM 进行修改而造成的数据不同步。
不能访问某些对象:
Web Workers 不能访问window对象、document对象、parent对象等, 即使Web Workers不能访问这些对象,它们仍然可以发起AJAX请求或使用 WebSockets 这类网络API。
使用方式
创建worker
const worker = new Worker('worker.js', { type: 'module', credentials: 'same-origin' });
Worker构造函数接收两个参数:
url: 这是一个字符串值,表示一个指向 JavaScript 文件的 URL。这个参数是必须的,它的值应该是一个相对于当前 HTML 文档的相对路径,或者是一个完整的 URL。
options: 这是一个对象,用于配置新创建的 Worker 实例。这个参数是可选的,可以包含以下属性:
type: 用于指定要创建的 Worker 的类型。支持的值有 "classic" 和 "module"。"classic" 表示 Worker 脚本是一个普通 JavaScript 文件,这是默认类型。"module" 表示 Worker 脚本是一个 JavaScript 模块。
credentials: 用于指定在加载 Worker 脚本时是否需要证书(cookies)。支持的值有 "omit"、"same-origin" 和 "include"。
Blob对象创建worker
// 创建一个 Blob 对象 const blob = new Blob([ "onmessage = function(e) { postMessage('Worker: ' + e.data); }" ]); // 创建一个 Blob URL const blobURL = window.URL.createObjectURL(blob); // 使用 Blob URL 创建一个 Worker const worker = new Worker(blobURL); worker.onmessage = function(e) { console.log(e.data); // 输出 "Worker: Hello" } worker.postMessage('Hello'); // 向 worker 发送消息
多线程间通讯
Worker 线程和主线程不能直接共享内存或变量,它们之间的通信必须通过消息传递来完成。可以使用 postMessage
方法来发送消息,并在另一边使用 onmessage
事件监听器来接收消息。
worker.onmessage = function(e) { console.log(e.data); // 输出 "Worker: Hello" } worker.postMessage('Hello'); // 向 worker 发送消息
实现一个线程池
本文提供一个通过web worker实现线程池的能力。
demo
export const workerStr = ` this.store = {}; this.addEventListener( "message", function (e) { var key = e.data.key; if(e.data.type === 'remove-item' && this.store[key]){ this.store[key] = null; return; } if(e.data.type === 'remove'){ this.store = {}; return; } if (!e.data.task) { return; } var task = new Function('return (' + e.data.task + ')(this.store)'); Promise.resolve(task()) .then((res) => { this.store[key] = { status: "succ", data: res, }; }) .catch((error) => { this.store[key] = { status: "fail", data: error, }; }) .finally(() => { console.log('test worker res', { key, ...this.store[key], }) this.postMessage({ key, ...this.store[key], }); }); }, false ); `; export default workerStr;
import { PoolList, Resolve, TaskRes } from '../type/task'; import workerStr from './worker' let blob: Blob; let url: string; let threadPool: Worker[] = []; /** * 使用webworker开启一个线程池 执行复杂运算 * real parallel * @param list 任务列表 * @param max 最大并发数 默认为2 最好和cpu数量一致 不要设置过多 * @returns 返回一个promise */ export const pool = async (list: PoolList, max = 1) => { if (!blob) { blob = new Blob([workerStr], { type: 'text/plain' }); } if (!url) { url = URL.createObjectURL(blob); } const store: { [key: string]: TaskRes } = {} let _resolve: Resolve; const p = new Promise((resolve) => { _resolve = resolve; }) let doneCount = 0; let index = 0; const runner = (curWorker: Worker) => { const curIndex = index++; if (!curWorker || !list[curIndex]) { return; } const { key, task } = list[curIndex]; curWorker.postMessage({ key, task: `${task}` }); curWorker.onmessage = function (event) { store[event.data.key] = event.data; doneCount++; if (doneCount === list.length) { _resolve(store) return; } runner(curWorker) } curWorker.onerror = function (event) { console.dir('test error', event) } } for (let i = 0; i < max; i++) { let worker = threadPool[i]; if (!worker) { worker = new Worker(url) threadPool[i] = worker; } runner(worker) } return p } /** * clear thread pool */ export const clearPool = () => { threadPool.forEach(worker => { worker.terminate() }) threadPool = []; } export default pool;