稀土掘金技术社区

不会webworker,你做什么性能优化??

Image

点击关注公众号,“技术干货” 及时达!

前言

本来这篇文章是打算写我之前「那个迭代」的「首屏优化」的,但是我做的那些优化,前几天上了生产,效果「并不如意」,因为生产环境不能随便动,我只能在灰度环境上测试,但是灰度测试后确实是比之前好的。那篇文章我也写了大半了,过几天发出来大家指点一下。然后我最近看了评论想着使用webworker去优化一下我那坨列表,然后我之前也没用过,最近也研究了一下,所以这篇文章就给大家分享一下webworker怎么用。其实挺简单的,但是遇到了一些坑!希望大家使用的时候能注意一下。

什么是web worker?

❝

mdn的链接 https://developer.mozilla.org/zh-CN/docs/Web/API/Worker/Worker

❞

webworker是html5的一个api,它的作用就是新开一个线程去做一些操作,因为这个线程并不会阻塞主线程,所以可以去提高网页的性能。其实可以把他「当成一个函数」,传参进行计算得到你想要经过一些代码处理的东西。

Demo

这里就以计算「计算斐波那契数列」为例子去给大家演示。

「主线程脚本(index.html)」

在主线程中 「new」一个worker对象,传参的是一个js脚本路径,「不能传其他类型的文本」,也必须是「同源」的。然后通过message事件和postmessage进行通信即可。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF - 8">
  <title>demo</title>
</head>

<body>
  <button onclick="startCalculation()">斐波那契数列</button>
  <p id="result"></p>
  <script>
    function startCalculation() {
      // 创建一个Web Worker对象,指定worker.js作为工作线程的脚本
      const worker = new Worker('worker.js');
      worker.postMessage(20)
      // 监听工作线程发送的消息
      worker.onmessage = function (event) {
        document.getElementById('result').innerHTML = '斐波那契数列的第20项是:' + event.data;
          // 通知关闭线程
          worker.postMessage('close')
      };
    }
  
</script>
</body>

</html>

「工作线程(work.js)」

这里主要的点是「self」,在工作线程中的self就相当于「主线程的window」,也就是window中很多东西在self中也能使用。但是工作线程是没有去「获取dom的权限」的,所以他不能也不建议去操作dom。然后就是通过message和postMessage去通信。

// 计算斐波那契数列的函数
function feibo(n) {
  if (n === 0 || n === 1) {
    return n;
  }
  return feibo(n - 1) + feibo(n - 2);
}

// 计算斐波那契数列的第10项并发送结果给主线程
self.onmessage = function (e) {
  console.log(e.data);
  if(e.data == 'close') {
      // 关闭线程
      self.close()
      return 
  }
  const data = feibo(e.data);
  self.postMessage(data);
};

方法属性

  1. self

self是对工作线程自身全局对象的引用。类似于浏览器主线程中的window对象,要注意的self不能访问 DOM 相关内容

  1. postmessge和message

当主线程使用worker.postMessage发送消息时,工作线程中的message事件处理函数就会被触发。当工作线程使用worker.postMessage发送消息时,主线程中的message事件处理函数就会被触发。所以就是用来通信的

  1. importScripts

用于在工作线程中加载外部脚本。可以同时加载多个脚本,并且脚本会按照它们在importScripts函数参数中的顺序「依次加载」,不过我感觉这个方法应该很少用。

  1. terminate(在「主」线程中使用)
worker.terminate();
  1. close (在「工作」线程中使用)
self.colse();

是吧。其实webworker就这么点东西,就是相当一个「装机师傅」,你把主板,cpu,显卡,内存,风扇...给他,师傅就可以还给你一台主机。然后我在用的时候就发现会有一些「坑」,就会导致出现bug。

坑1:postMessage

不知道大家知不知道,postMessage是一种「异步通信」。什么叫异步通信呢。就是我给你通知了,但是我不会去等你执行代码,我继续执行我的代码。大家可以用上面的demo试一下。我一开始以为是和我们vue的组件自定义事件通信那样,是「同步通信」的。其实应该很多人都不知道这个是同步通信的,这个很重要的,工作中不注意这个时机,就会导致bug,我记得我在面试的时候也被问到过。我下面写了个vue3的自定义通信的小demo,然后在自定义事件中写了for循环阻塞5秒钟,大家可以复制跑一下看看。

parent.vue

<template>
  <Child @chageFn="chageFn"></Child>
</template>

<script setup>
import Child from "./child.vue";
const chageFn = () => {
  const start = Date.now();
  const waitTime = 5000; // 5秒,单位是毫秒
  for (let i = 0; Date.now() - start < waitTime; i++) {

  }
  console.log("5秒时间已过,继续执行后续代码");
};

</script>
<style lang="scss" scoped></style>

child.vue

<template>
  <button @click="clickFn">点击我</button>
</template>

<script setup>
import { defineEmits } from "vue";
const emit = defineEmits(["chageFn"]);
const clickFn = () => {
  emit("chageFn");
  console.log("1");
};
</script>
<style lang="scss" scoped></style>

当我们点击的时候就可以发现,是「在五秒后打印1」,并不会立即打印1,但如果是postMessage通信是会立即打印1的。然后这个同步通信并不是说一定会等自定义函数执行完才会走后面的代码,如果这个自定义函数中是异步代码,是会先打印1的,就是正常的「事件循环」机制,大家也可以试试。

坑2:序列化和反序列化

postMessage在进行通信时会对数据进行「序列化」的,在message事件接收数据是会「反序列」的,啥意思呢,就是类似于JSON.parse和JSON.stringify,这两个api就是将js的数据类型转化成JSON格式的数据,但是postMessage并不是转化成JSON格式,它是一种「结构化克隆算法」,具体我也不清楚。他们的共同点都是不能转化函数,JSON.stringify是会将函数变成undefind,postMessage是报错。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Web Worker Function and Circular Reference Example</title>
</head>

<body>
  <script>
    const fn = ()=>{
        console.log(1)
    }
    console.log(JSON.stringify(fn)) // undefind 

    const worker = new Worker('worker.js')
    worker.postMessage(fn) // 报错

</script>
</body>

</html>

坑3:如何正确的去通信以及正确的去关闭工作线程

当我们使用webworker的时候不可能说每使用一次就new一次webwoker,而是每次都是使用这一个线程进行处理。然后可能会导致什么问题呢?还是那个装机师傅的例子,「有两个人同时去找这个师傅装机,装完了师傅不知道这两个机子分别是谁的」。那师傅肯定没有这么笨,他肯定会贴个标签说这个是他的,那个是她的。所以,当我们通信的时候就需要去规定通信的格式。比如下面这样,当主线程给工作线程通信的时候,传一个id字段实现唯一性,当工作线程回复的时候也带上这个字段,大家具体情况具体分析。

const params = {
    id:"1",
    params:{

    }
}

const response = {
    id:"1",
    data:{

                    }
}

关闭工作线程是有两种方式的,一种是「主线程去关闭」,另一种是「工作线程自己关闭」。如果协调不好的话就可能会导致工作线程的代码并没有执行完成就关闭了,然后就会引出很多问题。不过这个我倒是没遇到过hhh,其实我觉得可以去统一一下关闭的地方。比如,「统一由主线程去关闭」,当工作线程想关闭的时候,去通过postmessage去通知主线程去关闭。

总结

我觉得webworker其实大部分前端应该是很少用到的,反正我是第一次用。不过也并不是很难,稍微看一下就能学会,就能使用在工作上了。这应该也只会在「性能优化」的时候去使用,毕竟在大多数情况下封装函数就行了,谁会想着新开一个线程去处理,麻烦死了。只能说,技多不压身,学不死就往死里学!

点击关注公众号,“技术干货” 及时达!