首页/文章/javascript

Web Worker知道吧?怎么动态的创建Web Worker呢

2024-10-08
17206 分钟
...

前言

浏览器是一个多进程多线程的架构,以 Chrome 为例,每一个 tab 页都是一个单独的渲染进程。在这个渲染进程下,有 JS 执行线程和渲染线程。

JS 执行线程与渲染线程是互斥的,这就导致了我们在执行一些长任务的时候,会阻塞渲染线程,具体的表现可能有:

  1. 动画卡顿
  2. 点击按钮/输入框输入没反应等等

HTML5 中,浏览器提出了 Web Worker 这种技术,它允许在主线程之外运行脚本,这样就可以在后台执行一些耗时的任务,而不会阻塞 JS 执行线程,从而提高了 Web 应用的性能和响应性。

web worker简介

Web Worker 的一些主要用途和优点包括:

  1. 并行计算Web Worker 提供了一个在后台线程中执行 JavaScript 代码的环境,可以在这个环境中进行并行计算,提高页面性能和响应速度。
  2. 执行耗时任务:对于需要较长时间来完成的任务,比如大量数据处理、复杂算法运算、图片处理等,可以将这些任务放在 Web Worker 中执行,避免阻塞渲染线程。
  3. 提高用户体验:通过将一些耗时的操作放在 Web Worker 中执行,可以提高页面的响应速度和用户体验,使用户感受到页面更加流畅。
  4. 并发处理:使用多个 Web Worker 实例可以实现更高级的并发处理,从而更有效地利用多核 CPU

尽管 Web Worker 提供了很多优点,但也需要注意以下几点:

  • 无法访问 DOMWeb Worker 运行在独立的线程中,无法直接访问 DOM 和一些浏览器 API,因此主要用于处理纯粹的计算任务和网络请求。
  • 通信开销:由于 Web Worker 与主线程是隔离的,它们之间的通信需要通过消息传递,因此可能会存在一定的通信开销。
  • 内存消耗:每个 Web Worker 都会占用一定的内存,如果过多地创建 Web Worker,可能会导致内存消耗过大。

Vite+React 使用 demo

下面以 Vite+React 为例,介绍一下如何使用 Web Worker ,并展示 Web Worker 不阻塞渲染的特性。

首先我们来做两件事情,第一件事情是写一个简单的动画:

@keyframes ball-animation {
  0% {
    top: 200px;
  }
  50% {
    top: 100px;
  }
  100% {
    top: 200px;
  }
}
.ball {
  width: 100px;
  height: 100px;
  top: 200px;
  left: 200px;
  background-color: red;
  position: absolute;
  border-radius: 50%;
  animation: ball-animation infinite 1s;
}

可以看到是一个简单的小球跳动的动画,然后我们在组件挂载 3S 之后,执行一段 JS 长任务,这里以从 0 累计到 4000000000 为例:

  useEffect(() => {
    setTimeout(() => {
      console.log("长任务开始");
      console.time("calculate");
      let count = 0;
      for (let i = 0; i < 4000000000; i++) {
        count++;
      }
      console.log("长任务结束");
      console.timeEnd("calculate");
    }, 3000);
  }, []);

可以看到小球在长任务开始之后动画就停止了,整个页面也是处于一个不可交互的状态。这就是我们的JS执行线程阻塞了渲染线程。

然后我们尝试使用 worker 去进行这个计算,看看是什么效果。

首先在 public 目录下新建一个 calculate.worker.js 文件,填入以下的内容:

self.onmessage = function (e) {
  const data = e.data;
  console.log("长任务开始");
  console.time("calculate");
  let count = 0;
  for (let i = 0; i < data; i++) {
    count++;
  }
  console.log("长任务结束");
  console.timeEnd("calculate");
  self.postMessage("处理完成");
};

然后我们在主线程中来调用这个 worker

  useEffect(() => {
    setTimeout(() => {
      const worker = new Worker("/calculate.worker.js");
      worker.postMessage(4000000000);
      worker.onmessage = (e) => {
        console.log("收到worker的消息", e.data);
      };
    }, 1000);
  }, []);

然后来看一下执行效果:

可以看到,已经不再阻塞页面的渲染,整个过程十分丝滑。

Worker引入外部函数

那在我们实际的开发过程中,经常是需要引入一些外部第三方库去搭配使用的。在 worker 中,引入第三方库需要使用 importScripts 这个方法。这样的话就需要我们打成一个 umd 的包来引入。

使用示例如下:

importScripts(
  "https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.core.min.js"
);
self.onmessage = function (e) {
  const data = e.data;
  const res = self._.max(data)
  self.postMessage(res)
};

这里先引入 lodashcdn 包,然后用来计算最大值。

主线程发送数据如下:

 const worker = new Worker("/calculate.worker.js");
    worker.postMessage([1, 2, 3, 4, 5]);
    worker.onmessage = (e) => {
      console.log("收到worker的消息", e.data);
};

动态封装

下面我们来封装一个 createWorker 方法,可以动态创建 worker 并执行调用。

完整代码如下,先看代码,再来解释:

export const createWorker = ({ executor, params }) => {
  return new Promise((resolve, reject) => {
    const funcStr = executor.toString();
    const blob = new Blob([
      `onmessage = function(e) {
  const func = eval('(' + e.data.funcStr + ')');
  function convertStringsToFunctions(obj) {
    function dfs(obj) {
      for (const key in obj) {
        if (typeof obj[key] === 'string') {
          try {
            obj[key] = eval('(' + obj[key] + ')');
          } catch (error) {
          }
        } else if (typeof obj[key] === 'object' && obj[key] !== null) {
          dfs(obj[key]);
        }
      }
    }
    dfs(obj);
    return obj;
  }
  const data = convertStringsToFunctions(e.data.funcData);
  Promise.resolve(func(data)).then(res=>{
    postMessage({
      type: 'success',
      data: res
    })
  }).catch(err=>{
    postMessage({
      type: 'error',
      data: err
    })
  })
}`,
    ]);

    const url = URL.createObjectURL(blob);
    const worker = new Worker(url);

    worker.onmessage = function (e) {
      worker.terminate();
      URL.revokeObjectURL(url);
      console.log(e.data.type);
      if (e.data.type === "success") {
        resolve(e.data.data);
      } else {
        reject(e.data.data);
      }
    };

    function convertFunctionsToStrings(params) {
      function dfs(obj) {
        for (const key in obj) {
          if (typeof obj[key] === "function") {
            obj[key] = obj[key].toString(); 
          } else if (typeof obj[key] === "object" && obj[key] !== null) {
            dfs(obj[key]);
          }
        }
      }
      dfs(params);
      return params;
    }
    const data = convertFunctionsToStrings(cloneDeep(params));
    worker.postMessage({ funcStr, funcData: data });
  });
};
  1. createWorker 接收两个参数,一个是执行函数,一个是执行函数所需的参数
  2. 我们需要把执行函数放到 worker 执行,使用 postMessage 传递, postMessage 是不支持传输函数的,所以需要把函数转成字符串。在 worker 接收到之后再使用 eval 来调用。由于可能会有深层次的对象函数嵌套,所以这里需要递归。
  3. 同理, params 中的函数也需要转成字符串再交给 worker
  4. 为了兼容异步函数,这里统一把执行函数都先转成一个 Promise
  5. 执行函数执行完毕后,通知主线程并返回结果

使用示例

使用示例如下:

import { useEffect, useState } from "react";
import * as lodash from "lodash";
import { createWorker } from "./worker";
function App() {
  useEffect(() => {
    const run = async () => {
      const res = await createWorker({
        executor: ({ isEmpty, list }) => {
          return new Promise((resolve) => {
            const res = list.map(isEmpty);
            resolve(res);
          });
        },
        params: {
          isEmpty: (value) => !!value,
          list: [0, undefined, null, ""],
        },
      });
      console.log("res", res);
    };
    run();
  }, []);
}

export default App;

并发控制

上面已经提到过,如果 worker 创建太多也会导致资源占用太多,可能会消耗大量的内存。

所以这里需要做一个并发的控制,可以使用 window.navigator.hardwareConcurrency 来获取 cpu 的数量。

可以实现下面的一个 DynamicWorker 类,同样的先把代码贴出来,再来解释。

export class DynamicWorker {
  static getInstance(params = {}) {
    const { newInstance = false } = params;
    if (newInstance) {
      return new DynamicWorker();
    }
    if (!DynamicWorker.instance) {
      DynamicWorker.instance = new DynamicWorker();
    }

    return DynamicWorker.instance;
  }
  core = window.navigator.hardwareConcurrency || 4; 
  runningCount = 0;
  queue = [];
  createWorker = ({ executor, params }) => {
    const createTask = () => {
      return new Promise((resolve, reject) => {
        const funcStr = executor.toString();
        const blob = new Blob([
          `onmessage = function(e) {
      const func = eval('(' + e.data.funcStr + ')');
      function convertStringsToFunctions(obj) {
        function dfs(obj) {
          for (const key in obj) {
            if (typeof obj[key] === 'string') {
              try {
                obj[key] = eval('(' + obj[key] + ')');
              } catch (error) {
              }
            } else if (typeof obj[key] === 'object' && obj[key] !== null) {
              dfs(obj[key]);
            }
          }
        }
        dfs(obj);
        return obj;
      }
      const data = convertStringsToFunctions(e.data.funcData);
      Promise.resolve(func(data)).then(res=>{
        postMessage({
          type: 'success',
          data: res
        })
      }).catch(err=>{
        postMessage({
          type: 'error',
          data: err
        })
      })
    }`,
        ]);

        const url = URL.createObjectURL(blob);
        const worker = new Worker(url);

        worker.onmessage = function (e) {
          worker.terminate();
          URL.revokeObjectURL(url);
          if (e.data.type === "success") {
            resolve(e.data.data);
          } else {
            reject(e.data.data);
          }
        };

        function convertFunctionsToStrings(params) {
          function dfs(obj) {
            for (const key in obj) {
              if (typeof obj[key] === "function") {
                obj[key] = obj[key].toString(); 
              } else if (typeof obj[key] === "object" && obj[key] !== null) {
                dfs(obj[key]);
              }
            }
          }
          dfs(params);
          return params;
        }
        const data = convertFunctionsToStrings(cloneDeep(params));
        worker.postMessage({ funcStr, funcData: data });
      });
    };

    if (this.runningCount < this.core) {
      this.runningCount = this.runningCount + 1;
      return createTask();
    } else {
      return new Promise((resolve, reject) => {
        this.queue.push(() => {
          this.runningCount = this.runningCount + 1;
          createTask().then(resolve).catch(reject);
        });
      });
    }
  };
  next = () => {
    if (this.queue.length > 0) {
      const task = this.queue.shift();
      task();
    }
  };
}
  1. DynamicWorker 默认是单例模式,当然也可以创建多实例。
  2. 任务池不超过核心数量
  3. 如果当前任务池已满,则把任务推到队列里面,等待空闲时候再执行。

使用起来的方式也大同小异:

const run = async () => {
  const dynamicWorker = DynamicWorker.getInstance();
  const res = await dynamicWorker.createWorker({
    executor: ({ isEmpty, list }) => {
      return new Promise((resolve) => {
        const res = list.map(isEmpty);
        resolve(res);
      });
    },
    params: {
      isEmpty: (value) => !!value,
      list: [0, undefined, null, ""],
    },
  });
  console.log("res", res);
};

最后

vite环境可以用如下方式使用worker,可以使用相对路径导入任何位置的js作为worker,并且js内部也可以像普通js文件一样使用import导入任何包new Worker(new URL('./exampleWorker.js', import.meta.url), { type: 'module' });

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
javascript2025-09-18
JavaScript 的多线程能力:Worker
如果你写过一些计算量稍大的 JavaScript 代码,比如图像处理、大量数据排序或者复杂的算法,你几乎肯定遇到过浏览器“卡死”的现象。点击页面没反应,动画也停了,就像整个世界都静止了。 这就是主线程被阻塞的典型后果。因为主线程既要负责执行 JavaScript,又要负责渲染页面、响应用户操作,一旦它被繁重的计算任务占满,就无暇顾及其他,用户体验便直线下降。 这个问题的根源,正是“主线程是单线程的”。那么,如何解决呢? 答案很简单:把这...
学习面试
javascript2025-09-10
手把手教你实现一个EventEmitter
你是不是也遇到过这种情况:代码里各种事件监听和触发,回调函数套了一层又一层,最后自己都理不清哪个事件先触发、哪个后触发了? 别担心,今天我就带你从零开始实现一个自己的事件触发器(EventEmitter),让你彻底掌握事件驱动的精髓,代码清晰度直接提升200%! 为什么要懂EventEmitter? 先说说为什么我们要关心这个。Node.js的核心就是事件驱动,比如文件读写、网络请求,都是通过事件来处理的。不会EventEmitter,...
学习面试
javascript2025-08-09
Web Worker 多线程魔法
你是否曾遇到过网页因为复杂计算而卡顿的情况?今天,让我们一起探索Web Worker的神奇世界,看看如何用多线程技术解决性能瓶颈,并实现流畅的图片压缩功能! 为什么需要Web Worker? 想象一下,你在一个繁忙的餐厅里,只有一位服务员(主线程)负责点餐、上菜、结账所有工作。当客人突然暴增时,这位服务员就会手忙脚乱,其他客人只能干等着… 这就是JavaScript的现状—— 单线程 !所有任务都在一个线程中排队执行,一旦遇到复杂计算(...
学习面试
javascript2025-08-07
🌐AST抽象语法树
AST 是前端开发中一个非常重要的概念,它就像是代码的“编译器”,能够帮助我们理解和操作代码。本文将深入探讨 AST 的原理和应用,带你从零开始了解 AST。 一、什么是 AST? AST(抽象语法树)是一种树形结构,用于表示程序代码的语法结构。它将代码分解为一系列的节点,每个节点代表代码中的一个元素,如变量、函数、表达式等。AST 是编译器和解释器的基础,也是代码分析和转换的关键工具。 1\. AST 的结构 AST 的结构通常包括以...
学习面试
javascript2025-08-05
打字机效果是如何实现的
分析效果 使用 AI 对话的时候,文字是一个字一个字蹦出来的?它不是等所有内容生成完毕后一次性展示,而是以一种流畅、实时的“打字机”效果呈现,这种体验的背后,隐藏着一种高效的Web技术。它就是 服务器推送事件(Server Sent Events,简称 SSE) 什么是 SSE 从根本上说,SSE 是一种允许服务器 单向 、 持续 地向客户端(浏览器)推送数据的Web技术。 可以把它想象成一个 电台广播 : 服务器 是广播电台,持续不断...
学习面试
javascript2025-08-04
Promise 从原理到实现
前言 是 异步编程的核心概念之一,它为我们提供了一种更优雅的方式来处理异步操作。 本文将全面剖析 的工作原理,并逐步实现一个符合 规范的 。 正文 一、Promise 基础概念 代表一个异步操作的最终完成(或失败)及其结果值。它有 种状态: 1. pending : ,既不是成功,也不是失败状态 2. fulfilled :意味着操作 完成 3. rejected :意味着操作 状态一旦改变就不能再变,只能从 变为 ,或从 变为 。 二...
学习面试

评论

请登录后发表评论

去登录
加载评论中...

目录