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

相关推荐
forwardMyLife12 分钟前
element-plus的面包屑组件el-breadcrumb
javascript·vue.js·ecmascript
mez_Blog2 小时前
个人小结(2.0)
前端·javascript·vue.js·学习·typescript
珊珊而川2 小时前
【浏览器面试真题】sessionStorage和localStorage
前端·javascript·面试
森叶2 小时前
Electron 安装包 asar 解压定位问题实战
前端·javascript·electron
深情废杨杨2 小时前
前端vue-插值表达式和v-html的区别
前端·javascript·vue.js
GHUIJS2 小时前
【vue3】vue3.3新特性真香
前端·javascript·vue.js
markzzw2 小时前
我在 Thoughtworks 被裁前后的经历
前端·javascript·面试
众生回避2 小时前
鸿蒙ms参考
前端·javascript·vue.js
笃励3 小时前
Angular面试题五
javascript·ecmascript·angular.js
GHUIJS3 小时前
【vue3】vue3.5
前端·javascript·vue.js