当前位置: 首页 > article >正文

无阻塞UI:通过Web Worker提升用户体验的新途径

1.Web Worker 的定义

Web Worker 是浏览器提供的 JavaScript 多线程解决方案,允许在主线程之外运行脚本,执行耗时任务而不阻塞用户界面, 适用于处理耗时任务,避免阻塞主线程,提升用户体验。通过 postMessage 和 onmessage 实现主线程与 Worker 的通信.

2.使用场景

  1. 复杂计算:如图像处理、数据加密等。

  2. 大数据处理:如排序、过滤等。

  3. 实时通信:如 WebSocket 或轮询服务器。

  4. 后台任务:如日志上传、数据同步。

3.语法

1. 创建 Web Worker
// 主线程
const worker = new Worker('worker.js');
2. 主线程与 Worker 通信
  • 主线程发送消息
    worker.postMessage('Hello Worker');
  • 主线程接收消息
    worker.onmessage = function(event) {
        console.log('Received from worker:', event.data);
    };
3. Worker 脚本
  • 接收消息
    self.onmessage = function(event) {
        console.log('Received from main thread:', event.data);
    };
  • 发送消息
    self.postMessage('Hello Main Thread');
4. 终止 Worker
worker.terminate();

4.主要API

Web Worker 除了基本的 postMessage 和 onmessage 之外,还提供了其他一些 API 和方法,用于更灵活地控制 Worker 的行为和通信。以下是一些常用的 Web Worker API 和方法:

1. Worker 构造函数 ★ 常用

用于创建一个新的 Web Worker。

const worker = new Worker('worker.js');

2. postMessage ★ 常用

用于向 Worker 发送消息。

worker.postMessage(data);


// data 可以是任意类型的数据(如字符串、对象、数组等)。

// 数据会被结构化克隆算法(Structured Clone Algorithm)序列化后传递给 Worker。

3. onmessage ★ 常用

用于监听 Worker 发送的消息。

worker.onmessage = function(event) {
    console.log('Received from worker:', event.data);
};

// event.data 是 Worker 发送的数据。

4. onerror ★ 常用

用于监听 Worker 中的错误。

worker.onerror = function(error) {
    console.error('Worker error:', error);
};

// error 是一个 ErrorEvent 对象,包含错误的详细信息。

5. terminate ★ 常用

用于立即终止 Worker。

worker.terminate(); // 终止后,Worker 会停止运行,无法再接收或发送消息。

6. importScripts

在 Worker 中动态加载外部脚本。

// worker.js
importScripts('script1.js', 'script2.js');

// 可以加载多个脚本,脚本会按顺序执行。

// 加载的脚本必须是同源的。

7. close ★ 常用

在 Worker 内部调用,用于关闭 Worker。

// worker.js
self.close();

// Worker 会立即停止运行,无法再接收或发送消息

8. MessageChannel

用于在主线程和 Worker 之间创建双向通信通道。

// 主线程
const channel = new MessageChannel();

// 发送端口给 Worker
worker.postMessage({ port: channel.port1 }, [channel.port1]);

// 监听消息
channel.port2.onmessage = function(event) {
    console.log('Received from worker:', event.data);
};

// 发送消息
channel.port2.postMessage('Hello Worker');
// worker.js
self.onmessage = function(event) {
    const port = event.ports[0];

    // 监听消息
    port.onmessage = function(event) {
        console.log('Received from main thread:', event.data);
    };

    // 发送消息
    port.postMessage('Hello Main Thread');
};

9. BroadcastChannel

用于在多个 Worker 或主线程之间广播消息。

// 主线程或 Worker
const channel = new BroadcastChannel('my-channel');

// 发送消息
channel.postMessage('Hello everyone!');

// 监听消息
channel.onmessage = function(event) {
    console.log('Received:', event.data);
};

10. SharedArrayBuffer 和 Atomics

用于在主线程和 Worker 之间共享内存。

// 主线程
const sharedBuffer = new SharedArrayBuffer(1024);
const sharedArray = new Int32Array(sharedBuffer);

// 发送共享内存给 Worker
worker.postMessage(sharedBuffer);

// 修改共享内存
Atomics.store(sharedArray, 0, 123);
// worker.js
self.onmessage = function(event) {
    const sharedBuffer = event.data;
    const sharedArray = new Int32Array(sharedBuffer);

    // 读取共享内存
    const value = Atomics.load(sharedArray, 0);
    console.log('Shared value:', value);
};

11. Worker 生命周期事件 ★ 重要

Worker 支持以下生命周期事件:

  • onmessage:接收消息。

  • onerror:捕获错误。

  • onmessageerror:当接收到的消息无法反序列化时触发。

    worker.onmessageerror = function(event) {
        console.error('Message error:', event);
    };

12. Worker 全局作用域

在 Worker 中,self 指向 Worker 的全局作用域。常用的属性和方法包括:

  • self.postMessage:发送消息。

  • self.onmessage:监听消息。

  • self.close:关闭 Worker。

  • self.importScripts:加载外部脚本。

完整示例:使用 MessageChannel 实现双向通信

以下是一个完整示例,展示如何使用 MessageChannel 在主线程和 Worker 之间实现双向通信。

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Worker - MessageChannel 示例</title>
</head>
<body>
    <h1>Web Worker - MessageChannel 示例</h1>
    <button id="sendMessage">发送消息</button>
    <div id="output"></div>

    <script>
        const worker = new Worker('worker.js');
        const channel = new MessageChannel();

        // 发送端口给 Worker
        worker.postMessage({ port: channel.port1 }, [channel.port1]);

        // 监听 Worker 的消息
        channel.port2.onmessage = function(event) {
            document.getElementById('output').innerHTML = `收到 Worker 的消息: ${event.data}`;
        };

        // 发送消息给 Worker
        document.getElementById('sendMessage').addEventListener('click', function() {
            channel.port2.postMessage('Hello Worker');
        });
    </script>
</body>
</html>

worker.js

self.onmessage = function(event) {
    const port = event.ports[0];

    // 监听主线程的消息
    port.onmessage = function(event) {
        console.log('收到主线程的消息:', event.data);
        port.postMessage('Hello Main Thread');
    };
};

Web Worker 提供了丰富的 API 和方法,包括:

  • 基本通信postMessage 和 onmessage

  • 错误处理onerror 和 onmessageerror

  • 双向通信MessageChannel

  • 广播通信BroadcastChannel

  • 共享内存SharedArrayBuffer 和 Atomics

5.web Work语法示例

主线程

const worker = new Worker('worker.js');

worker.postMessage('Start');

worker.onmessage = function(event) {
    console.log('From Worker:', event.data);
};

Worker 脚本 (worker.js)

self.onmessage = function(event) {
    console.log('From Main:', event.data);
    self.postMessage('Working...');
};

6.注意事项

  1. 无 DOM 访问:Worker 不能直接操作 DOM。

  2. 同源限制Worker 脚本必须与主线程同源。★ 重要

  3. 有限 API:Worker 只能使用部分浏览器 API,如 XMLHttpRequestfetch 等

7.复杂计算阻塞主线程示例 ★ 反例

  • 主线程阻塞

    • JavaScript 是单线程的,复杂计算任务会占用主线程,导致页面无法响应用户交互。

    • 即使是一些简单的操作(如点击按钮、播放动画)也会被阻塞。

  • 用户体验差

    • 页面卡顿会让用户感到不流畅,甚至误以为页面崩溃。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>复杂计算阻塞主线程示例</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }
        #output {
            margin-top: 20px;
            padding: 10px;
            border: 1px solid #ccc;
            background-color: #f9f9f9;
        }
        button {
            padding: 10px 20px;
            font-size: 16px;
            cursor: pointer;
            margin-right: 10px;
        }
        #animation {
            width: 100px;
            height: 100px;
            background-color: red;
            margin-top: 20px;
            animation: rotate 2s linear infinite;
        }
        @keyframes rotate {
            from { transform: rotate(0deg); }
            to { transform: rotate(360deg); }
        }
    </style>
</head>
<body>
<h1>复杂计算阻塞主线程示例</h1>
<p>点击“开始计算”按钮,复杂计算任务将在主线程中运行,阻塞页面交互。</p>
<button id="startCalculation">开始计算</button>
<button id="interactiveButton">测试交互</button>
<div id="output"></div>
<div id="animation"></div>

<script>
    // 复杂计算任务:计算斐波那契数列(递归实现,性能较差)
    function fibonacci(n) {
        if (n <= 1) return n;
        return fibonacci(n - 1) + fibonacci(n - 2);
    }

    // 启动计算
    document.getElementById('startCalculation').addEventListener('click', function() {
        document.getElementById('output').innerHTML = "计算中...";
        const startTime = Date.now();
        const result = fibonacci(45); // 计算斐波那契数列的第 45 项
        const endTime = Date.now();
        document.getElementById('output').innerHTML = `计算完成,结果是: ${result},耗时: ${endTime - startTime} 毫秒`;
    });

    // 测试页面交互
    document.getElementById('interactiveButton').addEventListener('click', function() {
        alert('页面交互正常!');
    });
</script>
</body>
</html>

实际效果展示,差点给我电脑干崩啦 

8.Web Worker 不阻塞交互示例 ★ 案例

  1. 复杂计算任务在 Web Worker 中运行,不会阻塞主线程。

  2. 在计算过程中,用户可以正常点击按钮、观看动画,页面交互流畅。

  3. 计算完成后,结果会显示在页面上。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Worker 示例 - 不阻塞交互</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }
        #output {
            margin-top: 20px;
            padding: 10px;
            border: 1px solid #ccc;
            background-color: #f9f9f9;
        }
        button {
            padding: 10px 20px;
            font-size: 16px;
            cursor: pointer;
            margin-right: 10px;
        }
        #animation {
            width: 100px;
            height: 100px;
            background-color: red;
            margin-top: 20px;
            animation: rotate 2s linear infinite;
        }
        @keyframes rotate {
            from { transform: rotate(0deg); }
            to { transform: rotate(360deg); }
        }
    </style>
</head>
<body>
<h1>Web Worker 示例 - 不阻塞交互</h1>
<p>点击“开始计算”按钮,Web Worker 会在后台执行复杂计算,同时页面交互(如按钮点击和动画)不会受影响。</p>
<button id="startWorker">开始计算</button>
<button id="interactiveButton">测试交互</button>
<div id="output"></div>
<div id="animation"></div>

<script>
    // 主线程代码
    let worker;

    // 启动 Worker
    document.getElementById('startWorker').addEventListener('click', function() {
        if (typeof(Worker) !== "undefined") {
            if (!worker) {
                worker = new Worker('worker.js'); // 创建 Web Worker
            }

            // 监听 Worker 发送的消息
            worker.onmessage = function(event) {
                document.getElementById('output').innerHTML = "计算完成,结果是: " + event.data;
            };

            // 向 Worker 发送消息,开始计算
            worker.postMessage('开始计算');
            document.getElementById('output').innerHTML = "计算中...";
        } else {
            document.getElementById('output').innerHTML = "抱歉,你的浏览器不支持 Web Worker。";
        }
    });

    // 测试页面交互
    document.getElementById('interactiveButton').addEventListener('click', function() {
        alert('页面交互正常!');
    });
</script>

<!-- Web Worker 脚本 -->
<script id="workerScript" type="javascript/worker">
    // Worker 脚本
    self.onmessage = function(event) {
        if (event.data === '开始计算') {
            // 模拟复杂计算(计算从 0 到 10 亿的和)
            let result = 0;
            for (let i = 0; i < 1000000000; i++) {
                result += i;
            }
            self.postMessage(result); // 将结果发送回主线程
        }
    };
</script>

<script>
    // 将 Worker 脚本转换为 Blob URL
    const blob = new Blob([document.getElementById('workerScript').textContent], { type: 'text/javascript' });
    const workerUrl = URL.createObjectURL(blob);

    // 替换 worker.js 的路径为 Blob URL
    window.Worker = class extends Worker {
        constructor() {
            super(workerUrl);
        }
    };
</script>
</body>
</html>

实际效果展示,非常的流畅 

 

9.拓展:为什么要用 Blob URL?★ 了解

  1. 无需外部文件

    • 通常情况下,Web Worker 需要从一个单独的 JavaScript 文件(如 worker.js)加载脚本。

    • 使用 Blob URL 可以将 Worker 脚本直接嵌入到 HTML 文件中,避免创建额外的文件。

  2. 方便演示和分享

    • 在示例代码或教程中,使用 Blob URL 可以让代码更简洁,用户只需复制一个 HTML 文件即可运行完整的功能。

    • 不需要用户额外下载或创建 worker.js 文件。

  3. 动态生成脚本

    • Blob URL 允许动态生成 Worker 脚本内容。例如,可以根据用户输入或其他条件动态修改 Worker 的逻辑。

  4. 同源策略

    • Web Worker 的脚本文件必须与主页面同源(即相同的协议、域名和端口)。使用 Blob URL 可以避免跨域问题,因为 Blob URL 被视为同源。


10.拓展:Blob URL 的工作原理 ★ 了解

  1. Blob

    • Blob 是一个表示二进制数据的对象。我们可以将 JavaScript 代码作为字符串传递给 Blob,生成一个脚本文件。

  2. URL.createObjectURL

    • URL.createObjectURL 方法可以将 Blob 对象转换为一个临时 URL。这个 URL 可以作为 Web Worker 的脚本路径。

  3. 动态加载

    • 通过将 Worker 脚本嵌入 HTML 中的 <script> 标签,然后提取其内容并转换为 Blob URL,最终动态加载 Worker 脚本。

Blob URL代码解析 ★ 了解

以下是代码中与 Blob URL 相关的部分:

<!-- Web Worker 脚本 -->
<script id="workerScript" type="javascript/worker">
    // Worker 脚本
    self.onmessage = function(event) {
        if (event.data === '开始计算') {
            // 模拟复杂计算(计算从 0 到 10 亿的和)
            let result = 0;
            for (let i = 0; i < 1000000000; i++) {
                result += i;
            }
            self.postMessage(result); // 将结果发送回主线程
        }
    };
</script>

<script>
    // 将 Worker 脚本转换为 Blob URL
    const blob = new Blob([document.getElementById('workerScript').textContent], { type: 'text/javascript' });
    const workerUrl = URL.createObjectURL(blob);

    // 替换 worker.js 的路径为 Blob URL
    window.Worker = class extends Worker {
        constructor() {
            super(workerUrl);
        }
    };
</script>
  1. 嵌入 Worker 脚本

    • 使用 <script id="workerScript" type="javascript/worker"> 将 Worker 脚本嵌入 HTML 中。

    • type="javascript/worker" 是为了避免浏览器直接执行这段脚本。

  2. 提取脚本内容

    • 通过 document.getElementById('workerScript').textContent 获取嵌入的脚本内容。

  3. 生成 Blob URL

    • 将脚本内容传递给 Blob 构造函数,生成一个 Blob 对象。

    • 使用 URL.createObjectURL(blob) 将 Blob 对象转换为一个临时 URL。

  4. 动态加载 Worker

    • 通过重写 window.Worker,将 Blob URL 作为 Worker 的脚本路径。


优点

  • 简化代码结构:无需额外文件,所有代码都在一个 HTML 文件中。

  • 方便测试和演示:用户只需复制一个文件即可运行。

  • 动态生成脚本:可以根据需要动态修改 Worker 的逻辑。


缺点

  • 临时 URL:Blob URL 是临时的,页面刷新后会失效。

  • 不适合大型项目:对于大型项目,建议将 Worker 脚本放在单独的文件中,便于维护和调试。

11.最佳实战写法  ★ 非常重要

在实际工作中,我们通常会使用单独的 worker.js 文件来编写 Web Worker 的逻辑,而不是使用 Blob URL。这样可以更好地组织代码,便于维护和调试

以下是一个完整的示例,展示如何使用单独的 worker.js 文件,并在其中执行复杂的计算任务(如矩阵乘法),然后将结果返回给主线程。同时,页面中会有一些交互(如按钮点击和动画),以展示 Web Worker 如何避免阻塞主线程

/project
  ├── index.html
  ├── worker.js

 1. index.html 文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Worker 示例 - 实际工作场景</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }
        #output {
            margin-top: 20px;
            padding: 10px;
            border: 1px solid #ccc;
            background-color: #f9f9f9;
        }
        button {
            padding: 10px 20px;
            font-size: 16px;
            cursor: pointer;
            margin-right: 10px;
        }
        #animation {
            width: 100px;
            height: 100px;
            background-color: red;
            margin-top: 20px;
            animation: rotate 2s linear infinite;
        }
        @keyframes rotate {
            from { transform: rotate(0deg); }
            to { transform: rotate(360deg); }
        }
    </style>
</head>
<body>
    <h1>Web Worker 示例 - 实际工作场景</h1>
    <p>点击“开始计算”按钮,复杂计算任务将在 Web Worker 中运行,页面交互不受影响。</p>
    <button id="startWorker">开始计算</button>
    <button id="interactiveButton">测试交互</button>
    <div id="output"></div>
    <div id="animation"></div>

    <script>
        // 主线程代码
        let worker;

        // 启动 Worker
        document.getElementById('startWorker').addEventListener('click', function() {
            if (typeof(Worker) !== "undefined") {
                if (!worker) {
                    worker = new Worker('worker.js'); // 创建 Web Worker
                }

                // 监听 Worker 发送的消息
                worker.onmessage = function(event) {
                    const { result, time } = event.data;
                    document.getElementById('output').innerHTML = `计算完成,结果是: ${result},耗时: ${time} 毫秒`;
                };

                // 向 Worker 发送消息,开始计算
                worker.postMessage({ action: 'start' });
                document.getElementById('output').innerHTML = "计算中...";
            } else {
                document.getElementById('output').innerHTML = "抱歉,你的浏览器不支持 Web Worker。";
            }
        });

        // 测试页面交互
        document.getElementById('interactiveButton').addEventListener('click', function() {
            alert('页面交互正常!');
        });

        // 终止 Worker(可选)
        window.addEventListener('beforeunload', function() {
            if (worker) {
                worker.terminate(); // 关闭 Worker
            }
        });
    </script>
</body>
</html>

2. worker.js 文件

// Worker 脚本
function matrixMultiplication(size) {
    // 创建两个随机矩阵
    const matrixA = Array.from({ length: size }, () =>
        Array.from({ length: size }, () => Math.random() * 100)
    );
    const matrixB = Array.from({ length: size }, () =>
        Array.from({ length: size }, () => Math.random() * 100)
    );

    // 结果矩阵
    const result = Array.from({ length: size }, () =>
        Array.from({ length: size }, () => 0)
    );

    // 矩阵乘法
    for (let i = 0; i < size; i++) {
        for (let j = 0; j < size; j++) {
            for (let k = 0; k < size; k++) {
                result[i][j] += matrixA[i][k] * matrixB[k][j];
            }
        }
    }

    return result;
}

self.onmessage = function(event) {
    if (event.data.action === 'start') {
        const startTime = Date.now();
        const result = matrixMultiplication(200); // 计算 200x200 矩阵乘法
        const endTime = Date.now();
        self.postMessage({ result: '矩阵乘法完成', time: endTime - startTime }); // 将结果发送回主线程
    }
};

效果展示

代码说明

  1. 复杂计算任务

    • 在 worker.js 中,模拟了一个复杂的计算任务:矩阵乘法

    • 创建两个 200x200 的随机矩阵,并计算它们的乘积。这个任务非常耗时,可以明显感受到性能差异。

  2. 主线程与 Worker 通信

    • 主线程通过 worker.postMessage 向 Worker 发送消息,启动计算任务。

    • Worker 通过 self.postMessage 将计算结果返回给主线程。

  3. 页面交互

    • 点击“测试交互”按钮时,会弹出一个提示框。

    • 页面中还有一个红色方块的旋转动画,用于直观展示页面是否卡顿。

  4. 终止 Worker

    • 在页面关闭时,通过 worker.terminate() 终止 Worker,释放资源。


运行效果

  1. 打开浏览器,运行 index.html

  2. 点击“开始计算”按钮,Web Worker 会在后台执行矩阵乘法任务。

  3. 在计算过程中:

    • 点击“测试交互”按钮,页面会立即弹出提示框,证明页面交互未被阻塞。

    • 红色方块的旋转动画会一直流畅运行,证明主线程未被阻塞。

  4. 计算完成后,结果会显示在页面上。


总结

  • 实际工作场景:使用单独的 worker.js 文件编写 Web Worker 逻辑,便于维护和调试。

  • 复杂计算任务:通过矩阵乘法模拟复杂计算任务,展示 Web Worker 的性能优势。

  • 返回数据给主线程:Worker 可以通过 postMessage 将计算结果返回给主线程。


http://www.kler.cn/a/597592.html

相关文章:

  • 基于LabVIEW的Windows平台高速闭环控制
  • windows+ragflow+deepseek实战之一excel表查询
  • 第19章:StatefulSet:有状态应用部署最佳实践
  • dify案例分享-儿童故事绘本语音播报视频工作流
  • Lustre 语言的 Rust 生成相关的工作
  • 高考志愿填报管理系统基于Spring Boot SSM
  • 流程图软件推荐,好用的流程图工具分享
  • 西门子200smart之modbus_TCP(做主站与第三方设备)通讯
  • 常考计算机操作系统面试习题(四)
  • 蓝桥杯备考-----》前缀和+哈希表之连续自然数和
  • kotlin 内联函数 inline
  • Java集合框架深度剖析:从数据结构到实战应用
  • RestTemplate和RPC区别
  • Mysql深分页的解决方案
  • 怎么看股指期货多空单数量?
  • Linux 下 Git 使用简明指南
  • 004-SpringCloud Alibaba-OSS
  • 《基于自适应正负样本对比学习的特征提取框架》-核心公式提炼简洁版 2022年neural networks
  • 基于Python的个性化试题推荐系统
  • 【数据结构】kmp算法介绍+模板代码