Web Worker - 让前端实现多线程

打印 上一主题 下一主题

主题 994|帖子 994|积分 2982

什么是web worker?

javascript是单线程模型。全部任务只能在同一个线程上面完成,前面的任务没有做完,背面的就只能等候,这样当我们执行一些时间较长的js运算时候呢就会阻塞背面执行的代码。
那么如何办理呢?我们可以使用 web worker
WebWorker 现实上是运行在欣赏器后台的一个单独的线程,因此可以执行一些耗时的操作而不会阻塞主线程。WebWorker 通过与主线程之间通报消息实现通讯,这种通讯是双向的。
worker初始化

  1. const worker = new Worker(jsUrl, options);
复制代码
options 参数(非必填)
参数名描述范例nameworker线程的名称,可以在工作者线程中通过 self.name 获取到字符串标识stringtype表示加载脚本的方式,可以是 ‘classic’ 或者’module’。'classic’将脚本作为平凡脚本来执行,'module’将脚本作为模块来执行。‘classic’|’module’credentials当type为’module’时,指定如何获取与传输凭据数据(cookie)相干的Web Worker脚本,与fetch的 credentials 属性同等。在type为’classic’时默认为’omit’。‘omit’|’same-origin’|’include’ 1.1 假如是平凡项目,直接把初始化文件放在一个文件夹下,可以直接创建 Worker。
  1. const worker = new Worker('worker.js');
复制代码
1.2 在 Webpack 项目中,我们必要添加各种 loader 支持新技能,创建 Worker 必要使用worker-loader:
  1. // webpack 4.0
  2. import Worker from 'worker-loader!./worker';
  3. const worker = new Worker();
复制代码
但Webpack 5.0之后,我们不必要 worker-loader了,于是我们可以这么创建:
  1. const worker = new Worker(new URL('./worker.js', import.meta.url));
复制代码
此处的 new URL(),可以约等于 nodejs 中的 path.resolve(baserul + ‘./worker.js’)。
另有一个简单的办理方案:把 worker 脚本放到 public 文件夹下,这样打包产物就和 worker 脚本在同一个文件夹下,可以正常初始化 Worker。
除了使用脚本文件创建 Worker 之外,我们还可以使用 行内js 来创建工作者线程。通过 Blob 对象 URL 我们可以更快的初始化工作者线程,由于没有网络延迟。(推荐)
  1. // 创建代码字符串
  2. const workerScriptStr = `
  3.     self.onmessage = (e) => {
  4.         console.log(e.data);
  5.         postMessage('get message from main thread');
  6.     }
  7. `;
  8. // 基于脚本字符串生成Blob对象
  9. const workerBlob = new Blob([workerScriptStr]);
  10. // 基于Blob实例创建对象URL
  11. const workerBlobUrl = URL.createObjectURL(workerBlob);
  12. // 基于对象URL创建专用工作者线程
  13. const worker = new Worker(workerBlobUrl);
  14. worker.postMessage('main thread send message');
  15. // main thread send message
复制代码
上面的例子是把步调分解开,一步步的创建 Worker,可以写一块:
  1. const worker = new Worker(URL.createObjectURL(new Blob([`self.onmessage =
  2. ({data}) => console.log(data);`])));
  3. worker.postMessage('main thread send message');
  4. // main thread send message
复制代码
ES Module

在初始化 Worker 时,假如不传第二个配置参数,默认执行脚本的方式为 ‘classic’,此时在脚本里仅可以通过 Worker 的全局对象 WorkerGlobalScope 提供的 importScripts 方法引用在线脚本。
假如使用 import 关键字引入,会报错 Cannot use import statement outside a module 不允许在 module 外使用 import。
  1. // main.jsconst worker = new Worker('worker.js');
  2. // worker.js// import { sum } from 'lodash'; // Error: Cannot use import statement outside a moduleimportScripts('https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js');_.sum([1, 2]);...
复制代码
但假如在创建时指定了 type 为 ‘module’:
  1. // main.js
  2. const worker = new Worker('worker.js', { type: 'module' });
  3. // worker.js
  4. import { sum } from 'lodash';
  5. sum([1, 2]);
  6. ...
复制代码
则不会报错,从而可以愉快的使用按需导入能力了。
由于 Web Worker 是一个独立的线程,所以理论上,你可以在Web Worker 里再启用一个 Web Worker 子线程,在有多个CPU焦点的时候,使用多个子线程可以实现并行盘算,这里就不展开了。
worker通讯

与工作者线程通讯都是通过 postMessage 方法发送消息,通过 onmessage 变乱处置处罚函数来继承消息。数据传输的方式是通过 结构化克隆算法 克隆数据,通报数据副本。
欣赏器支持另一种性能更好的对象传输方式 可转移对象(Transferable objects) ,通过可转移对象,资源的全部权会从一个上下文直接转移到另一个上下文,而并不会颠末克隆。传输后,原始对象将不可用;它将不再指向转移后的资源,而且任何实验读取或者写入的操作都将抛出异常。

举例:
  1. // main.js
  2. const worker = new Worker(new URL('worker.js', import.meta.url), { type: 'module' });
  3. worker.onmessage = (e) => {
  4.     // 接收来自 worker 的消息
  5.     setInfo(e.data);
  6. }
  7. // 发送消息给 worker
  8. worker.postMessage('message from main thread');
  9. // 可转移对象
  10. // 创建一个 8MB 的文件并填充
  11. const uInt8Array = new Uint8Array(1024 * 1024 * 8).map((v, i) => i);
  12. console.log(uInt8Array.byteLength); // 8388608
  13. // 将底层 buffer 传递给 worker
  14. worker.postMessage(uInt8Array, [uInt8Array.buffer]);
  15. console.log(uInt8Array.byteLength); // 0
  16. // worker.js
  17. import { sum } from 'lodash';
  18. // 如果是 classic 模式,则需要通过 improtscripts 来引入网络脚本
  19. // importScripts('https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js');
  20. // 接收来自主线程的消息
  21. onmessage = (e) => {
  22.     console.log(e.data);
  23.     const temp = Array.from(e.data).map((e) => +e);
  24.     // 将计算结果发送给主线程
  25.     postMessage(sum(temp));
  26. };
复制代码
备注:像 Int32Array 和 Uint8Array 等范例化数组(TypedArray)是可序列化的(Serializable object),但是不能转移。然而,它们的底层缓冲区是一个 ArrayBuffer,它是一个可转移对象。我们可以在数据参数中发送 uInt8Array.buffer,但是不能在传输数组中发送 uInt8Array。
除了 postMessage 方法发送消息之外,另有另外一种方式,可以发送消息。
BroadcastChannel

BroadcastChannel 从字面意思上理解是广播频道,他可以让同源页面的欣赏器上下文来订阅它。
它允许 同源的 不同欣赏器窗口、tab页、frame 或者 iframe 下的不同文档之间相互通讯。通过触发 message 变乱,消息可以广播到全部监听了该频道的 BroadcastChannel 对象。
此特性在 Web Worker 中可用,由于初始化 Worker 的脚本和主线程是同源的,在 Web Worker 中广播的消息,主线程可以监听到,反之亦然。
试一下:
  1. // 初始化具名频道
  2. const channel = new BroadcastChannel('bm channel');
  3. // 广播消息,发送的消息自己接收不到,其他源可以接收到
  4. channel.postMessage('全场两元,通通两元');
  5. // 接收其他源发送的消息
  6. channel.onmessage = (e) => {
  7.     console.log('get message from other broadcast', e.data);
  8. };
复制代码
实验一下:恣意打开两个相同的页面,把上面的代码分别粘贴到欣赏器的console调试内里,在一个页面调用一下 channel 的 postMessage 方法,在另一个页面看一下,发现消息可以打印出来。
标题

如何使用web worker

1.在主线程中创建 worker
  1. const worker = new Worker('worker.js');
复制代码
2.主线程调用worker.postMessage()方法,向 Worker 发消息。
  1. worker.postMessage('Hello World');
复制代码
它可以是各种数据范例,包括二进制数据。
3.主线程通过worker.onmessage指定监听函数,吸收子线程发返来的消息
  1. worker.onmessage = function (event) {
  2.   console.log('Received message ' + event.data);
  3.   doSomething();
  4. }
  5. function doSomething() {
  6.   // 执行任务
  7.   worker.postMessage('Work done!');
  8. }
  9. // 这里的worker.onmessage 也可以换成self.addEventListener ,self代表子线程自身,即子线程的全局对象  等同于
  10. self.addEventListener('message', function (e) {
  11.   self.postMessage('You said: ' + e.data);
  12. }, false);
复制代码
4.Worker 完成任务以后,主线程就可以把它关掉。
  1. worker.terminate();
复制代码
5.Worker 内部假如要加载其他脚本,有一个专门的方法importScripts()。
  1. importScripts('script1.js', 'script2.js');
复制代码
6.主线程可以监听 Worker 是否发生错误。假如发生错误,Worker 会触发主线程的error变乱。
  1. worker.onerror(function (event) {
  2.   console.log([
  3.     'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
  4.   ].join(''));
  5. });
  6. // 或者
  7. worker.addEventListener('error', function (event) {
  8.   // ...
  9. });
复制代码
web worker简单应用

主线程
  1.     const worker = new Worker('./worker.js');
  2.     worker.addEventListener('message', function ({data}) {
  3.       switch (data.type) {
  4.         case 'prime':
  5.           document.getElementById('prime').textContent = `${data.n}之内的所有质数是:${data.result.join(",")}`;
  6.           break;
  7.         case 'fibonacci':
  8.           document.getElementById('fibonacci').textContent = `${data.n}之内的所有斐波那契数列之和是:${data.result}`;
  9.         break;
  10.         case 'reverseNumber':
  11.           document.getElementById('reverseNumber').textContent = `${data.n}之内的所有回文数是:${data.result}`;
  12.         break;
  13.         default:
  14.           break;
  15.       }
  16.     }, false);
  17.     worker.postMessage({type: 'prime', n: 300000});
  18.     worker.postMessage({type: 'fibonacci', n: 40});
  19.     worker.postMessage({type: 'reverseNumber', n: 400000});
复制代码
worker.js
  1. self.addEventListener('message', function ({ data }) {
  2.   switch (data.type) {
  3.     case 'prime': // 质数
  4.       self.postMessage({ type: 'prime', n: data.n, result: countPrime(data.n) });
  5.       break;
  6.     case 'fibonacci': //斐波那契数列
  7.       self.postMessage({ type: 'fibonacci', n: data.n, result: fibonacci(data.n) });
  8.       break;
  9.     case 'reverseNumber': // 回文数
  10.       self.postMessage({ type: 'reverseNumber', n: data.n, result: countReverseNumber(data.n) });
  11.     default:
  12.       break;
  13.   }
  14. }, false);
  15. // 计算n以内的所有质数
  16. function countPrime(num) {
  17.   let n = 1;
  18.   let nums = [];
  19.   search:
  20.   while (n < num) {
  21.     // 开始搜寻下一个质数
  22.     n += 1;
  23.     for (let i = 2; i <= Math.sqrt(n); i++) {
  24.       // 如果除以n的余数为0,开始判断下一个数字。
  25.       if (n % i == 0) {
  26.         nums.push(n)
  27.         continue search;
  28.       }
  29.     }
  30.   }
  31.   return nums;
  32. }
  33. // 计算斐波那契数列之和
  34. function fibonacci(n) {
  35.   if (n == 1 || n == 2) return 1;
  36.   return n >= 3 ? fibonacci(n - 1) + fibonacci(n - 2) : null;
  37. }
  38. // 计算
  39. function countReverseNumber(n) {
  40.   return Array.from(new Array(n), (v,i) => (i+1)).filter(v => {
  41.     let nv = v.toString().split('').reverse().join('')
  42.     return nv == v && v > 10
  43.   })
  44. }
复制代码
相比于异步,worker的优点在哪?

当我们执行时间复杂度很大的逻辑时,背面的逻辑为了等候前面的循环竣事必要很长的时候,很大概会造成页面卡顿,页面会有“闪动”。
当我们把这“一坨”逻辑放到异步中去执行时,欣赏器会先执行同步任务,后去执行异步操作,从而不去堵塞我们的页面,这貌似办理了刚才的问题。但是这并非最佳办理方案,它仅仅是改变了函数的执行次序,假如有多个异步任务执行呢?别忘了js是单线程,背面的异步任务还是会受到阻塞的。等候前面的异步任务执行完成,才会执行背面的异步任务。
worker的限制有哪些?

1.同源限制
线程执行的脚本文件(即 上述代码的 worker.js)必须和主线程的文件同源,从其他源加载 Worker 脚本文件会报错。
2.DOM 限制
Worker 线程地点的全局对象,与主线程不一样,无法读取主线程地点网页的 DOM 对象,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象。
3.通讯联系
Worker 线程和主线程不在同一个上下文环境,它们不能直接通讯,必须通过消息完成。
4.脚本限制
Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。
5.文件限制
Worker 线程无法读取本地文件,即不能打开本机的文件体系(file://),它所加载的脚本,必须来自网络。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

数据人与超自然意识

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表