Web Worker, JavaScript Multithreading

Web Worker, JavaScript Multithreading

We all know that JavaScript is a single-threaded language. This is because js was originally designed for use in browsers, mainly for manipulating DOM elements and realizing the interaction between users and browsers. If js is multi-threaded, there may be many In order to avoid this complexity, js is designed as a relatively simple single-threaded language.

However, with the development of technology, in the era of multi-core CPU, a single thread cannot give full play to the computing power of the computer. Therefore, in HTML5, the Web Worker standard is proposed. Web Worker can create multiple threads for JavaScript scripts, so that we can assign some complex computing tasks to the worker thread to run, avoiding the blocking of the main thread, and return the result to the main thread after the worker thread completes the calculation. However, the child thread is completely controlled by the main thread and has various restrictions, including the inability to manipulate the DOM. Therefore, Web Worker does not change the essence of js single thread.

Classification of threads

Web Worker threads can actually be divided into several categories, such as

  • Dedicated Worker: Dedicated thread
  • Shared Worker: Shared thread
  • Service Workers: Service Workers

​ ...wait

This article mainly talks about Dedicated Worker, that is, a dedicated thread. A dedicated thread can only be used by the script that created it. A dedicated thread corresponds to a main thread. In general, the code run by the Web Worker thread is for the current script (page) Service, so dedicated thread is also the most commonly used kind of thread.

initial use

Create worker thread

Worker threads are created through the Worker constructor

let worker = new Worker('xxx.js')
复制代码

The first parameter of the Worker constructor is a script file, which cannot be a local file, because the Worker cannot read the local file, and an error will be reported when writing the local address directly, so the script file must come from the server.

The second optional parameter is an options object. The name value can be configured to specify the name of the worker, which can be used to distinguish multiple workers.

let worker = new Worker('xxx.js', { name: 'my_worker' })
复制代码

可以理解为当前创建worker线程的代码就是主线程,上面Worker 构造函数的参数脚本文件就是worker线程。

主线程与Worker线程通信

主线程收发数据

主线程创建 Worker 线程后,可以通过 worker.postMessage() 方法向Worker发送数据。该数据可以是各种数据类型,包括二进制数据等。

然后通过 worker.onmessage 或者 worker.addEventListener('message', function(){})的方式接收Worker线程发送过来的数据。

const worker = new Worker('xxx.js')
// 给Worker线程发送数据
worker.postMessage('你好')

// 接收来自Worker线程的数据
worker.onmessage = function (e) {
  console.log('接收到的Worker线程发过来的数据:' + e.data)
}
// 或者
worker.addEventListener('message', function (e) {
  console.log('接收到的Worker线程发过来的数据:' + e.data)
});
复制代码

Worker线程收发数据

同样,Worker线程可以通过 self.postMessage()给主线程发送数据

通过self.onmessage或者self.addEventListener('message', function(){})的方式接收

值得一提的是在Worker线程中,self 代表线程自身(主线程中self代表window),也可以用this代替self,或者干脆省略不写也是可以的,所以下面三种写法其实是一样的

// 写法一
self.addEventListener('message', function (e) {
  self.postMessage('接收到的主线程发过来的数据:' + e.data);
});

// 写法二
this.addEventListener('message', function (e) {
  this.postMessage('接收到的主线程发过来的数据:' + e.data);
});

// 写法三
addEventListener('message', function (e) {
  postMessage('接收到的主线程发过来的数据:' + e.data);
});
复制代码

数据通信例子

例如我们有段worker线程的代码如下

self.addEventListener("message", function (e) {
  console.log("接收到的主线程发过来的数据: ", e.data.user.name);
  // 修改接收到的主线程数据	
  e.data.user.name = "mike";
  self.postMessage("你好,我是worker线程");
});

复制代码

同时主线程的代码如下

let data = {
  user: {
    name: "jack",
  },
};

const worker = new Worker("http://127.0.0.1:8080/worker.js");
worker.postMessage(data);
worker.onmessage = function (e) {
  console.log("接收到的Worker线程发过来的数据: " + e.data);
  console.log(data.user.name); // name值还是jack,并没有变成 mike
};
复制代码

最终打印结果为

w4.png

可以发现上面代码 Worker线程在接收主线程发过来的数据后将其对象数据上的某个值修改,然后发送数据给主线程,主线程在接收时打印原先发生的data数据,发现name值没变,说明主线程和worker线程的这种数据通信是拷贝关系,而不是简单的传值。所以Worker线程对通信数据的修改并不会影响主线程的数据。

worker线程的限制

  • Worker 脚本文件的限制
    • Worker 线程无法读取本地文件,脚本文件需来自服务器
    • 同源策略:Worker 线程运行的脚本文件,必须与主线程的脚本文件同源
  • Worker 线程全局对象限制
    • Dom限制:如前面所说,为了避免多个线程同时操作dom带来的复杂性,Worker线程不能访问documentwindowparent对象。但是可以访问navigator对象和location对象
    • Worker线程也无法使用alert()方法和confirm()方法。但是Worker可以访问XMLHttpRequest 对象,也就是发AJAX请求,也可以获取setTimeout(), clearTimeout(), setInterval(), clearInterval()等定时操作方法
  • 数据通信限制:如上面所说,Worker线程和主线程并不在同一个上下文环境,不能直接通信。

本地调试方案

脚本文件必须来自网络,那么我们在本地调试的时候怎么调试呢?

其实方案有很多,这里给出几个常用的比较简单的方案供大家参考。

利用Blob

我们可以通过Blob()方式,首先

我们可以把Worker线程代码写出字符串的形式,再通过 new Blob() 和 window.URL.createObjectURL() 的方式来将其转化为可以生效的 worker 脚本文件

const workerCode = `
self.addEventListener("message", function (e) {
  console.log("接收到的主线程发过来的数据: " + e.data);
  self.postMessage("你好,我是worker线程");
});`;
const workerBlod = new Blob([workerCode]);
const worker = new Worker(window.URL.createObjectURL(workerBlod));
worker.postMessage("你好,我是主线程");
worker.onmessage = function (e) {
  console.log("接收到的Worker线程发过来的数据: " + e.data);
};
复制代码

如果不想通过这种字符串的形式,也可以用一个 script 标签将Worker线程代码包裹起来,并将其type类型设置为js无法识别的自定义类型,那么它就会被认为是一个数据块元素。

示例代码:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <title></title>
  </head>
  <script id="myWorker" type="javascript/myWorkType">
    self.addEventListener("message", function (e) {
      console.log("接收到的主线程发过来的数据: " + e.data);
      self.postMessage("你好,我是worker线程");
    });
  </script>
  <body>
    <div id="app"></div>
  </body>
  <script defer src="./main.js"></script>
</html>
复制代码

然后通过document的方式获取其代码

const workerScript = document.getElementById('myWorker').textContent
const workerBlod = new Blob([workerScript]);
const worker = new Worker(window.URL.createObjectURL(workerBlod));
复制代码

使用http-server

通过安装http-server

npm i -g http-server
复制代码

然后在相关代码的文件夹下运行命令

http-server
复制代码

w2.png

这样,便可以创建worker

const worker = new Worker("http://127.0.0.1:8080/worker.js");
复制代码

Worker的错误处理

在主线程中通过 worker.onerror 或者 worker.addEventListener('onerror', function(){})的方式来监听Worker是否发生错误。

例如,

Worker线程代码:

self.addEventListener("message", function (e) {
  e.data.forEach((item) => {
    console.log(item);
  });
});
复制代码

主线程代码:

const worker = new Worker("http://127.0.0.1:8080/worker.js");
worker.postMessage(undefined);
worker.onerror = function (e) {
  console.log(
    "Worker报错: " +
      "\n" +
      `错误发生的行号: ${e.lineno};` +
      "\n" +
      `错误发生的文件名: ${e.filename};` +
      "\n" +
      `错误消息: ${e.message}`
  );
};
复制代码

上面代码执行后worker线程中由于 undefined 上面没有 foreach 方法,于是报错如下

w3.png 在Worker线程中也可以监听错误,不过只能拿到错误的消息数据

self.onerror = function (error) {
  // 这里 error 相当于上面代码中的 e.message
  console.log("错误消息:", error);
};
复制代码

关闭Worker

Worker一旦创建成功就会始终运行,所以Worker 也比较耗费资源,当Worker 使用完毕时,我们可以手动停止Worker,通过以下代码

worker.terminate();
复制代码

也可以在Worker线程中关闭

self.close();
复制代码

Worker中加载其他脚本

Worker内部如果需要加载其他的脚本,可以通过importScripts()来加载

举个简单的例子

下面代码是被加载的文件

// otherScript.js文件
function add(a, b) {
  return a + b;
}
复制代码

Worker线程的代码如下

importScripts("./otherScript.js");
console.log(add(1, 2));
复制代码

在主线程运行创建出上面的Worker线程后,控制台可以如期打印出 3 (1+2=3),也就是Worker线程成功引入了 otherScript.js 文件的代码。

同时引入多个文件

importScripts() 也可以同时引入多个文件

importScripts("./first.js", "./second.js");
复制代码

importScripts的阻塞性

importScripts 是同步的执行代码的,并且有一定的阻塞性,我们看下面两个实验。

首先,有以下的test.js文件代码

// test.js文件
let testArr = [];
for (let i = 0; i < 100000; i++) {
  testArr.push(i);
}
复制代码

Worker 线程代码

console.time("importScripts time");

importScripts("./test.js");

console.timeEnd("importScripts time");
复制代码

主线程创建上面Worker 的线程运行后可以在控制台发现

w6.png

时间大概是 8 毫秒。

而相对的,我们的我们直接把那段代码写到Worker线程中

Worker 线程代码

console.time("importScripts time");

let testArr = [];
for (let i = 0; i < 100000; i++) {
 testArr.push(i);
}

console.timeEnd("importScripts time");
复制代码

打印结果为

image-20220410215139350.png

时间大概是 3 毫秒。

所以,其实importScripts并不是很实用。

在实际开发中,我们肯定是用模块化开发,不可能把Worker的所有代码都写在一个文件上,那么在 importScripts 不实用的情况下,我们可以使用打包工具将Worker的代码打包成一个文件,例如在webpack的项目中,我们可以使用 webworkify-webpack 插件。

webworkify-webpack

笔者之前做过一个在线教育的直播平台,在对师生聊天历史记录的数据处理时,就用到了webworkify-webpack这个插件创建Worker线程。

安装webworkify-webpack

在webpack项目下安装

npm i webworkify-webpack
复制代码

webworkify-webpack的使用

首先,Worker线程代码需要在包裹在函数中,并用module.exports导出,该函数的参数就是该Worker线程的self。如下示例

module.exports = function (self) {
    self.postMessage('发送给主线程的数据')
  	self.addEventListener('message', function (ev) {
		console.log('接收到主线程发过来的数据', ev)
  	})
}
复制代码

而主线程中,创建对应Worker如下示例

import work from 'webworkify-webpack'
let worker = work(require.resolve('./xxx.js'))
worker.postMessage('发送给Worker线程的数据')
worker.addEventListener('message', (ev) => {
    console.log('接收到Worker线程发过来的数据', ev)
})
复制代码

上面代码中,work的参数可以用require.resolve来返回Worker线程文件的绝对路径,require.resolve还可以检查拼接好之后的路径是否存在。

webworkify-webpack原理

以下是webworkify-webpack源码的部分截取,可以发现其原理和上面的通过Blob()方式创建Worker线程一样都是通过 Blob 的方式

module.exports = function (moduleId, options) {
  options = options || {}
  var sources = {
    main: __webpack_modules__
  }

  var requiredModules = options.all ? { main: Object.keys(sources.main) } : getRequiredModules(sources, moduleId)

  var src = ''

  Object.keys(requiredModules).filter(function (m) { return m !== 'main' }).forEach(function (module) {
    var entryModule = 0
    while (requiredModules[module][entryModule]) {
      entryModule++
    }
    requiredModules[module].push(entryModule)
    sources[module][entryModule] = '(function(module, exports, __webpack_require__) { module.exports = __webpack_require__; })'
    src = src + 'var ' + module + ' = (' + webpackBootstrapFunc.toString().replace('ENTRY_MODULE', JSON.stringify(entryModule)) + ')({' + requiredModules[module].map(function (id) { return '' + JSON.stringify(id) + ': ' + sources[module][id].toString() }).join(',') + '});\n'
  })

  src = src + 'new ((' + webpackBootstrapFunc.toString().replace('ENTRY_MODULE', JSON.stringify(moduleId)) + ')({' + requiredModules.main.map(function (id) { return '' + JSON.stringify(id) + ': ' + sources.main[id].toString() }).join(',') + '}))(self);'

  var blob = new window.Blob([src], { type: 'text/javascript' })
  if (options.bare) { return blob }

  var URL = window.URL || window.webkitURL || window.mozURL || window.msURL

  var workerUrl = URL.createObjectURL(blob)
  var worker = new window.Worker(workerUrl)
  worker.objectURL = workerUrl

  return worker
}
复制代码

webworkify-webpack实例

接下来,我们结合Vue写一个计时器示例:

首先,创建Worker线程文件

// webWorker.js
module.exports = function (self) {
  const strategy = {
    timing: (data) => {
      let startClassTime = +new Date()
      if (data) {
        startClassTime = data
      }
      computeTime()
      // 时间格式化, 个位数前面加‘0’
      function timeFormat(num) {
        if (num < 10) {
          return '0' + num
        } else {
          return num + ''
        }
      }
      // 时间合并返回
      function computeTime() {
        let now = +new Date()
        let dif = now - startClassTime
        let hour = Math.floor(dif / 1000 / 3600)
        let minute = Math.floor((dif / 1000 / 60) % 60)
        let seconds = Math.round((dif / 1000) % 60)
        hour = timeFormat(hour)
        minute = timeFormat(minute)
        seconds = timeFormat(seconds)
        let time = hour + ' : ' + minute + ' : ' + seconds
        self.postMessage({
          type: 'timing',
          data: time,
        })
      }
      setInterval(() => {
        computeTime()
      }, 1000)
    },
  }
  self.addEventListener('message', function (ev) {
    const { type, data } = ev.data || {}
    strategy[type](data)
  })
}
复制代码

主线程(组件)代码如下

<template>
  <div>
    {{ myTime ? myTime : '00 : 00 : 00' }}
  </div>
</template>

<script>
import work from 'webworkify-webpack'

export default {
  data() {
    return {
      myTime: '00 : 00 : 00',
    }
  },
  created() {
    let worker = work(require.resolve('../../webworker.js'))
    worker.postMessage({
      type: 'timing',
      data: 0,
    })
    worker.addEventListener('message', (ev) => {
      console.log(this.myTime)
      if (ev.data.type === 'timing') {
        this.myTime = ev.data.data
      }
    })
  },
}
</script>
复制代码

运行后效果如下

w9.gif

Guess you like

Origin juejin.im/post/7085011669583134734