WebWorker 其实没那么难

前言

很多同学在学习一些技术时,并没有理解这些技术所能解决的问题,没有这些技术会导致怎样的问题。结果在学了之后,只学了一个皮毛,学了有哪些 API,在项目中又难以去运用。本篇文章将从问题出发,探讨 WebWorker 是怎么来应用的。

概念

首先,我们必须知道的是 JavaScript 是单线程模型,即使通过事件循环、Web Workers、多进程/多线程浏览器架构以及异步API,让 JavaScript能够以一种看起来像是并行的方式执行代码,但其本质依然是单线程模型。

为了更加深刻的理解单线程模型,我们以如下代码为例:

javascript 复制代码
<script>

function fib(n) {
  if (n < 2) {
    return n;
  }
  return fib(n - 1) + fib(n - 2);
}

fib(45);

const el = document.createElement('div');
el.innerHTML = 'Hello Web Worker';
document.documentElement.appendChild(el);
</script>

当我们刷新网页时,浏览器经历了很长的时间后,才停止旋转。这是因为页面中有JavaScript代码在执行且耗时过长,阻塞UI更新

为了解决这个问题,出现了 Web Worker,为创造了多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的时候,Worker 线程也在运行

javascript 复制代码
<script>
var worker = new Worker('fib_worker.js');

const el = document.createElement('div');
el.innerHTML = 'Hello Web Worker';
document.documentElement.appendChild(el);
</script>
javascript 复制代码
// fib_worker.js
function fib(n) {
  if (n < 2) {
    return n;
  }
  return fib(n - 1) + fib(n - 2);
}

const result = fib(45);

console.log('result is ', result);

可以看到,当刷新浏览器时,浏览器渲染按钮立即结束,且过了很长时间控制台输出 fib(45) 的结果,可以看到 worker 线程并不会阻塞主线程。通过这个例子,想必你对 WebWorker 基本的使用有了一定的认识,后面我们将举出更多的例子去理解 WebWorker。

基本用法

Worker 创建

javascript 复制代码
// worker.js 文件对应的是所要执行的任务

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

Worker 通信

主线程与 Worker 之间通信采用的是消息传递机制,具体如下:

javascript 复制代码
// 主线程

// postMessage 发送消息
worker.postMessage('Main Hello World');

// 监听消息
work.onmessage = function (event) {
  console.log('Main Received message ' + event.data);
}

// work 内部出错时   
work.onerror = function (error) { }
javascript 复制代码
// worker 线程

self.postMessage('Worker Hello World');

self.onmessage = function (event) {
  console.log('Worker Received message ' + event.data);
}

self 是一个全局对象,它代表了 Worker 线程自身的环境。在 Worker 文件中,self 取代了在普通浏览器环境中全局可用的 window 对象。

Worker 销毁

worker 执行完任务后,通过销毁可以释放资源。

javascript 复制代码
// 主线程

worker.terminate()
javascript 复制代码
// worker 线程

self.close()
  • worker.terminate() 是在主线程中调用的方法,用于立即终止 Worker 线程。当你调用 terminate() 时,Worker 线程会被强制停止,它不会有机会完成当前正在执行的任务或进行清理工作。这是主线程对 Worker 线程的一种单向控制。

  • self.close() 是在 Worker 线程内部调用的方法,用于优雅地关闭 Worker 线程。当你从 Worker 线程内部调用 close() 时,Worker 线程会完成当前正在执行的任务,然后关闭自己。这种方式允许 Worker 线程在关闭前进行必要的清理工作。

WebWorker 实战

页面上存在三个输入框,用户可以在任一输入框中输入数字后,输出对应的斐波那契值。

这里我们将分别给出非 WebWorker 实现、以及 WebWorker 实现两种方式加深对 WebWorker 的理解。

非 WebWorker Demo

首先,我们举一个不采用 Web worker 实现的示例,可以看到如果输出一个较大的值,整个网页会被阻塞住,我们没办法对网页进行任何操作

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>

    <div id="box1">
        <input type="number" value="0" />
        <button onclick="clickHandler('box1')">计算</button>
        <span></span>
    </div>

    <div id="box2">
        <input type="number" value="0"/>
        <button onclick="clickHandler('box2')">计算</button>
        <span></span>
    </div>

    <div id="box3">
        <input type="number" value="0"/>
        <button onclick="clickHandler('box3')">计算</button>
        <span></span>
    </div>

    <script>

        function clickHandler(boxId) {
          console.log('clickHandler', boxId);
          const boxEl = document.getElementById(boxId);
          const inputEl = boxEl.getElementsByTagName('input')[0];
          const number = inputEl.value;
          const result = fib(number);
          const spanEl = boxEl.getElementsByTagName('span')[0];
          spanEl.innerHTML = `${number} fib result is ${result}`;
        }

        function fib(n) {
          if (n < 2) {
            return n;
          }
          return fib(n - 1) + fib(n - 2);
        }

    </script>

</body>

</html>

WebWorker Demo

当我们采用 web worker 实现时,可以看到即使输入很大的值,也不会阻塞其他的输入。

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="box1">
    <input type="number" value="0" />
    <button onclick="clickHandler('box1')">计算</button>
    <span></span>
  </div>

  <div id="box2">
    <input type="number" value="0"/>
    <button onclick="clickHandler('box2')">计算</button>
    <span></span>
  </div>

  <div id="box3">
    <input type="number" value="0"/>
    <button onclick="clickHandler('box3')">计算</button>
    <span></span>
  </div>
  
  <script>
    const workerMap = {};

    function clickHandler(boxId) {
      let worker = workerMap[boxId];
      if (!worker) {
        worker = new Worker('fib_worker.js');
        workerMap[boxId] = worker;

        worker.onmessage = (event) => {
          const boxEl = document.getElementById(boxId);
          const inputEl = boxEl.getElementsByTagName('input')[0];
          const number = inputEl.value;
          const spanEl = boxEl.getElementsByTagName('span')[0];
          spanEl.innerHTML = `${number} fib result is ${event.data}`;
        }
      }

      const boxEl = document.getElementById(boxId);
      const inputEl = boxEl.getElementsByTagName('input')[0];
      const number = inputEl.value;
      worker.postMessage(number);
    }
  </script>
</body>
</html>
javascript 复制代码
function fib(n) {
  if (n < 2) {
    return n;
  }
  return fib(n - 1) + fib(n - 2);
}

self.onmessage = (event) => {
  const number = event.data;
  const result = fib(number);
  self.postMessage(result);
}

注意事项

WebWorker 的使用需要遵循如下一些注意事项:

1、同源策略:分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。

2、文件限制:Worker 脚本必须来源于网络文件,不能直接运行内敛 JavaScript 或从本地文件系统加载脚本。

3、作用域隔离:每个 Worker 都有自己的全局执行环境,不能在 Worker 中访问主线程中的JavaScript 对象。

4、结构化克隆:传递给Worker的数据通过结构化克隆算法进行复制。这意味着可以传递大多数JavaScript数据结构,但是不能传递DOM元素或函数。

5、DOM 限制:Worker 中无法读取 DOM 对比,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象。

应用场景

通过上面的体验,想必你知道了 WebWorker 基本的应用场景,即耗时长会阻塞主线程。以下是一些应用场景和代码示例:

复杂数学计算

在需要执行复杂计算的情况下,可以使用 Web Worker 来避免阻塞主线程。

javascript 复制代码
// main.js

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

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

worker.postMessage({ type: 'calculate', data: [1, 2, 3, 4, 5] });
javascript 复制代码
// calculator.js

self.onmessage = function(event) {
  if (event.data.type === 'calculate') {
    const result = event.data.data.reduce((sum, num) => sum + num, 0);
    self.postMessage(result);
  }
};

图像处理

在图像处理应用中,可以在 Worker 中执行图像分析或转换,以避免影响用户体验。

javascript 复制代码
// main.js

const worker = new Worker('image-processor.js');
const image = document.getElementById('myImage');

worker.onmessage = function(event) {
  const processedImage = event.data;
  document.body.appendChild(processedImage);
};

worker.postMessage(image);
javascript 复制代码
// image-processor.js

self.onmessage = function(event) {
const image = event.data;

// 假设有一个图像处理函数 processImage
const processedImage = processImage(image);
self.postMessage(processedImage);
};

数据解析

当需要解析大型 JSON 或 XML 数据时,可以使用 Worker 来处理,以避免阻塞主线程。

javascript 复制代码
// main.js

const worker = new Worker('data-parser.js');

const data = { /* 大型数据对象 */ };

worker.onmessage = function(event) {
  const parsedData = event.data;
  console.log('Parsed data:', parsedData);
};

worker.postMessage(data);
javascript 复制代码
// data-parser.js

self.onmessage = function(event) {
  const data = event.data;
  // 假设有一个解析函数 parseData
  const parsedData = parseData(data);
  self.postMessage(parsedData);
};

数据同步

在需要与服务器进行大量数据交换的应用中,可以使用 Worker 来处理数据同步,以避免影响用户体验。

javascript 复制代码
// main.js

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

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

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

worker.postMessage({ type: 'fetchData', url: 'https://api.example.com/data' });
javascript 复制代码
// worker.js

self.onmessage = function(event) {
  if (event.data.type === 'fetchData') {
    fetch(event.data.url)
    .then(response => response.json())
    .then(data => {
        self.postMessage(data);
    })
    .catch(error => {
        self.postMessage({ error: 'An error occurred while fetching the data.' });
    });
  }
};

Vue 中使用 Web Worker

在 Vue 中使用 Web Worker 略有不同,因为 Webpack、 Vite 等构建工具会对静态资源做处理,对于 Web Worker 而言,需要告知构建工具如何处理 .js 文件的 Worker。

这里以 Vite 为例,分别支持以下两种方式创建 Web Worker。

使用查询后缀导入

语法:

javascript 复制代码
import MyWorker from './worker?worker'

const worker = new MyWorker()
javascript 复制代码
<script setup>
import FibWorker from './workers/fib_worker?worker'

const workerMap = {};
function clickHandler(boxId) {
    console.log('boxId', boxId);
    let worker = workerMap[boxId];
    if (!worker) {
        worker = new FibWorker();
        workerMap[boxId] = worker;
        worker.onmessage = (event) => {
            console.log('result', event.data);
            const boxEl = document.getElementById(boxId);
            const inputEl = boxEl.getElementsByTagName('input')[0];
            const number = inputEl.value;
            const spanEl = boxEl.getElementsByTagName('span')[0];
            spanEl.innerHTML = `${number} fib result is ${event.data}`;
        }
    }

    const boxEl = document.getElementById(boxId);
    const inputEl = boxEl.getElementsByTagName('input')[0];
    const number = inputEl.value;
    worker.postMessage(number);
}

</script>

使用构造函数导入

语法:

javascript 复制代码
const worker = new Worker(new URL('./worker.js', import.meta.url))
javascript 复制代码
import MyWorker from './worker?worker'

const worker = new MyWorker()
javascript 复制代码
<script setup>

const workerMap = {};
function clickHandler(boxId) {
    console.log('boxId', boxId);
    let worker = workerMap[boxId];
    if (!worker) {
        worker = new Worker(new URL('./workers/fib_worker.js', import.meta.url))
        workerMap[boxId] = worker;
        worker.onmessage = (event) => {
            console.log('result', event.data);
            const boxEl = document.getElementById(boxId);
            const inputEl = boxEl.getElementsByTagName('input')[0];
            const number = inputEl.value;
            const spanEl = boxEl.getElementsByTagName('span')[0];
            spanEl.innerHTML = `${number} fib result is ${event.data}`;
        }
    }

    const boxEl = document.getElementById(boxId);
    const inputEl = boxEl.getElementsByTagName('input')[0];
    const number = inputEl.value;
    worker.postMessage(number);
}

</script>

结尾

通过以上内容,想必你对 WebWorker 所能解决的问题有了深刻认识,根据其所能解决的问题就可以在项目中得以应用啦。如果你对于以上内容以及其他前端技能存在困惑,欢迎在评论区留言或添加我的个人微信交流。

相关推荐
前端青山5 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
从兄6 小时前
vue 使用docx-preview 预览替换文档内的特定变量
javascript·vue.js·ecmascript
清灵xmf7 小时前
在 Vue 中实现与优化轮询技术
前端·javascript·vue·轮询
薛一半8 小时前
PC端查看历史消息,鼠标向上滚动加载数据时页面停留在上次查看的位置
前端·javascript·vue.js
过期的H2O29 小时前
【H2O2|全栈】JS进阶知识(四)Ajax
开发语言·javascript·ajax
MarcoPage9 小时前
第十九课 Vue组件中的方法
前端·javascript·vue.js
你好龙卷风!!!9 小时前
vue3 怎么判断数据列是否包某一列名
前端·javascript·vue.js
shenweihong11 小时前
javascript实现md5算法(支持微信小程序),可分多次计算
javascript·算法·微信小程序
巧克力小猫猿11 小时前
基于ant组件库挑选框组件-封装滚动刷新的分页挑选框
前端·javascript·vue.js
嚣张农民11 小时前
一文简单看懂Promise实现原理
前端·javascript·面试