Posted in

??面试官:Web Worker 知道吧?怎么动态的创建 Web Worker 呢?? – 掘金_AI阅读总结 — 包阅AI

包阅导读总结

1.

关键词:Web Worker、渲染线程、并行计算、长任务、并发控制

2.

总结:本文介绍了 Web Worker 技术,包括其用途优点(如并行计算、执行耗时任务等)及注意事项(无法访问 DOM 等),以 Vite+React 为例展示使用方法,如创建、引入外部函数、动态封装,并提及并发控制,防止过多创建导致资源占用。

3.

主要内容:

– Web Worker 简介

– 浏览器架构导致长任务阻塞渲染线程

– Web Worker 优点:并行计算、执行耗时任务、提高用户体验、并发处理

– Web Worker 限制:无法访问 DOM、通信开销、内存消耗

– Vite+React 使用 demo

– 展示未使用 Web Worker 时长任务阻塞渲染

– 介绍如何使用 Web Worker 解决阻塞问题

– 演示在 Web Worker 中引入外部函数

– 动态封装

– 封装 createWorker 方法,可动态创建 worker 并执行调用

– 说明参数处理及函数与参数的转换

– 并发控制

– 通过 DynamicWorker 类控制 worker 并发

– 任务池不超过核心数量,满则入队列等待

思维导图:

文章地址:https://juejin.cn/post/7385758285960478759

文章来源:juejin.cn

作者:可乐鸡翅kele

发布时间:2024/6/30 15:06

语言:中文

总字数:2402字

预计阅读时间:10分钟

评分:91分

标签:Web Worker,性能优化,Vite,React,动态创建


以下为原文内容

本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com

前言

浏览器是一个多进程多线程的架构,以 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;}

Kapture 2024-04-15 at 23.14.36.gif

可以看到是一个简单的小球跳动的动画,然后我们在组件挂载 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);  }, []);

Kapture 2024-04-15 at 23.13.42.gif

可以看到小球在长任务开始之后动画就停止了,整个页面也是处于一个不可交互的状态。这就是我们的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);  }, []);

然后来看一下执行效果:

Kapture 2024-04-17 at 21.50.30.gif

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

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;

image.png

并发控制

上面已经提到过,如果 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);};

最后

以上就是本文的全部内容,如果你觉得有意思的话,点点关注点点赞吧~