Worker

worker 接口时WebWorker中的一部分,可以创建一个后台运行的脚本,在脚本存活期间可以与调用这建立连接、通信。使用worker只需要调用Worker构造函数

在worker线程内也可以再此创建worker, 嵌套worker必须与创建者同源

在worker中window中部分类无法使用,XHR、fetch对象可以使用,但是XHR responseXML、channel属性始终返回null

worker一旦创建成功就会一直运行,不会被主线程的活动打断。所以一直运行的worker也会比较浪费资源。使用完毕后应该关闭

在使用web worker有几点需要注意:

  1. 同源策略的限制

分配给worker线程运行的脚本文件,必须与主线程的脚本文件同源

  1. DOM限制

worker线程所在的全局对象和主线程不一样,无法读取主线程所在网页的DOM对象,比如confirm、document、parent、alert. 但是worker线程中可以使用navigator、location对象,

  1. 通信的限制

worker线程与主线程不在一个上下文环境,不能直接通信,必须通过特定的方法,比如postMessage

  1. 文件限制

worker线程无法读取本地的文件,就是不能打开本机的文件系统file://, 但是可以把文件转换以后给到worker线程

基本使用

主线程

  1. const worker = new Worker("xxx/work.js");
  2. worker.addEventListener("message", function onMessage(evl) {
  3. let data = evl.data; // 由worker线程发出的数据
  4. let target = evl.target; // evl.currentTarget, 是发送消息的源,在worker线程里一样
  5. });
  6. worker.postMessage("array"); // 向worker线程发送一个消息,string
  7. // worker.terminate(); // 在主线程中关闭worker线程

woker线程

  1. self.addEventListener("message", function onMessage(evl) {
  2. let data = evl.data; // 接受使用线程发来的消息
  3. evl.currentTarget.postMessage(`worker : ${data}`); // 增加字符串返回给源线程
  4. });
  5. // self.close(); 在worker线程中关闭线程

worker API

  • message

注册一个消息监听器

  • messageerror

在worker线程接收到一条无法被反序列化的消息时,会触发该事件

  • error

监听线程之间的错误

  • postMessage

向线程对中发送一条消息

  • importScripts

从worker线程中加载其他脚本

收发数据

在线程之间的通信内容,可以时文本、可以是对象。但是这种是拷贝关系,传值。 在worker线程中修改的内容不会影响到主线程。

主线程和worker线程之间可以交换二进制数据,比如File、Blob、ArrayBuffer等。

  1. // 主线程发送类数组
  2. const u8Arr = new Uint8Array(new ArrayBuffer(1));
  3. u8Arr[0] = 99;
  4. worker.postMessage(u8Arr);
  5. worker.addEventListener("message", function onMsg(evl){
  6. let data = evl.data; // data【0】为90,和发送之前的u8Arr没有影响
  7. });
  8. // 在worker线程中收到的data就是u8Arr
  9. // worker线程
  10. self.addEventListener("message", function onMsg(evl) {
  11. let data = evl.data;
  12. data[0] = 90; // 在worker线程中的修改对主线程中不会影响
  13. evl.target.postMessage(data) // 修改以后返回
  14. });

如果一个文件这样拷贝过去很不方便,在主线程内也会阻塞。所以js允许主线程把二进制文件直接转移到子线程,一旦转移主线程就无法继续使用。

  1. // 具体语法
  2. worker.postMessage(arrayBuffer, [arrayBuffer])
  3. // 主线程
  4. const arr = new ArrayBuffer(1);
  5. const u8arr = new Uint8Array(arr);
  6. u8arr[0]=99; // 在发送前arraybuffer length为1, 0位为99
  7. work.postMessage(arr,[arr]);
  8. // -> work.postMessage(arr,{transfer:[arr]}); postmessage 的泛型
  9. console.log(arr); // 发送后arraybuffer length为0
  10. // worker线程
  11. self.addEventListener("message", function onMsg(evl) {
  12. let data = evl.data;
  13. data[0] === 99; // rue
  14. });

从scriot创建worker

  1. // worker js标签
  2. <script type="worker" >
  3. self.addEventListener("message", function onMsg(evl) {
  4. let data = evl.data;
  5. });
  6. </script>
  7. // 用于主线程
  8. <script >
  9. const blob = new Blob([document.querySelector("script[type=worker]").textContent.trim()],
  10. {type:"application/javascript"}
  11. );
  12. const workerUrl = URL.createObjectURL(blob);
  13. const worker = new Worker(workerUrl);
  14. // worker.postMessage('xxx');
  15. </script>

SharedWorker

sharedWorker 接口是一种特殊的worker,可以从几个浏览器上下文中访问。具有和其他worker不同的作用域

在chrome中可以在地址栏中输入chrome://inspect/#workers 可以打开service worker和shared worker控制面板,用于调试

使用SharedWorker的几点限制:

  1. worker资源和使用页面必须是同源,各个页面直接也必须是同源
  2. 每一个worker连接者之间都是新的port接口
  3. 如果name不同的情况下可能连接不到

具体使用

main、child两个页面一样(post的消息有加前缀)

  1. //main.html
  2. <body>
  3. <input type="text"id="txt">
  4. <button id="send">send</button>
  5. <br />
  6. <br />
  7. <label for="result">main:
  8. </label>
  9. <textarea id="result" cols="30" rows="10" readonly></textarea>
  10. <script>
  11. const worker = new SharedWorker("/src/channel/sharedWork.js",{name:"main-worker"});
  12. worker.port.start(); // 开启连接
  13. const send = document.getElementById('send');
  14. const result = document.getElementById("result");
  15. const text = document.getElementById("txt");
  16. worker.port.addEventListener('message', function (e) {
  17. result.value = e.data;
  18. }, false);
  19. send.addEventListener('click', function (e) {
  20. if (text.value === "cancel") {
  21. worker.port.close(); //从外部关闭
  22. return
  23. }
  24. worker.port.postMessage(`mian(${text.value})`);
  25. // worker.port.postMessage(`child(${text.value})`); // child.htnl
  26. }, false);
  27. </script>
  28. </body>

worker.js

  1. const ports = new Set();
  2. self.addEventListener("connect", evl => {
  3. const port = evl.ports[0];
  4. port.start(); // 开启监听
  5. port.addEventListener("message", evl => {
  6. let data = evl.data;
  7. let res = {
  8. data,
  9. length: ports.size,
  10. };
  11. // 不可从内部关闭
  12. // if (data === "closeWorker") {
  13. // port.close();
  14. // ports = ports.filter(el => el !== port);
  15. // ports.forEach(p => p.postMessage(`close ${evl.name}`))
  16. // return
  17. // }
  18. // 广播发送给所有连接者
  19. ports.forEach(p => p.postMessage(JSON.stringify(res, null, 4)));
  20. })
  21. ports.add(port);
  22. })

结果
在mian页面输入
image.png
在child页面输入
image.png
二者之间输入输出都可以得到响应

如果创建共享worker时,得到的name值不同那两个woker不会出现共享情况,被视为不同的worker:

  1. // main.html
  2. const worker = new SharedWorker("/src/channel/sharedWork.js",{name:"main-worker"});
  3. // child.html
  4. const worker = new SharedWorker("/src/channel/sharedWork.js",{name:"main-worker"});
  5. // 如果child.html 的name属性为`child-worker`那俩者之间不会产生联系

关闭对应的worker.port之后,就于worker之间没有了联系,不可于worker之间通信,手动存储的port还会保留,需要手动移除关闭的port

Message Channel

Messag Channel接口可以创建一个异步的新的消息通道,并通过它们两个的端口属性进行收发消息

属于一个类,但很多地方实现了该类的接口,比如worker、iframe, channel可以拿出来单独使用,或者新家一个channel 将所有权给到目标环境

发送的消息值可以是任何有效value,如果发送类数组对象,可以将所有权给出去

使用

  1. <div class="channel-container">
  2. <div>child</div>
  3. <input type="text" id="child-create">
  4. <button id="child-send">发送</button>
  5. <br>
  6. <textarea name="" id="child-result" cols="30" rows="10" readonly></textarea>
  7. </div>
  8. <div class="channel-container">
  9. <div>main</div>
  10. <input type="text" id="main-create">
  11. <button id="main-send">发送</button>
  12. <br>
  13. <textarea name="" id="main-result" cols="30" rows="10" readonly></textarea>
  14. </div>
  15. <script>
  16. const channel = new MessageChannel();
  17. const main = channel.port1;
  18. const child = channel.port2;
  19. const mainBox = {
  20. send: document.querySelector("#main-send"),
  21. create: document.querySelector("#main-create"),
  22. result: document.querySelector("#main-result")
  23. };
  24. const childBox = {
  25. send: document.querySelector("#child-send"),
  26. create: document.querySelector("#child-create"),
  27. result: document.querySelector("#child-result")
  28. };
  29. function pushEventListener(dom, port) {
  30. let tempVal = ``;
  31. dom.send.addEventListener("click", function send() {
  32. port.postMessage(tempVal);
  33. });
  34. dom.create.addEventListener("input", function onInput(e) {
  35. tempVal = e.target.value;
  36. });
  37. port.addEventListener("message", function onMessage(e) {
  38. dom.result.textContent = e.data;
  39. });
  40. port.start(); // 手动开启端口, 如果使用onmessage 属性回调的方式会自动开启
  41. }
  42. pushEventListener(mainBox, main)
  43. pushEventListener(childBox, child)
  44. </script>

显示的结果:
image.png
输入并发送
image.png
两个端口之间,postMessage发送消息会触发另一个端口的message事件,比如child发送一次消息,会显示在main的文本域中

交出对象的所有权:

  1. let array = new ArrayBuffer(10);
  2. let u8arr = new Uint8Array(array);
  3. u8arr[0]=201;
  4. u8arr[1]=204;
  5. function pushEventListener(dom, port) {
  6. let tempVal = ``;
  7. dom.send.addEventListener("click", function send() {
  8. port.postMessage(array,[array]);
  9. });
  10. dom.create.addEventListener("input", function onInput(e) {
  11. tempVal = e.target.value;
  12. });
  13. port.addEventListener("message", function onMessage(e) {
  14. console.log(e.data); // length = 10
  15. console.log(array); // lenght = 0
  16. });
  17. port.start();
  18. }

和worker之间搭配使用:

// 主页面
let worker1 = new Worker('./worker1.js');
let worker2 = new Worker('./worker2.js');


const channel = new MessageChannel();
const main = channel.port1;
const child = channel.port2;

worker2.postMessage("child", [child]) // 先存在监听才可以收到其他端口的消息
worker1.postMessage("main", [main])

// worker2.js
onmessage = function (e) {
  const port = e.ports[0];
  port.onmessage = portEvent => {
   console.log("worker2的port收到:",portEvent.data);
  }
  console.log(e.data,"work2");
}

// worker1.js
onmessage = function(e) {
  console.log("work1收到:",e.data);
  e.ports[0].postMessage("worker1发出: "+e.data)
}

/* 结果:
  work1收到: main
  child work2
  worker2的port收到: worker1发出: main
*/

Iframe:

// mian.html
<input type="text" id="child-create">
<button id="child-send">发送</button>
<br>
<textarea name="" id="main-result" cols="30" rows="10" readonly></textarea>
<script>
  const childBox = {
    send: document.querySelector("#child-send"),
    create: document.querySelector("#child-create"),
    result: document.querySelector("#main-result")
  }

  function pushEventListener(dom, port) {
    let tempVal = ``;

    dom.send.addEventListener("click", function send() {
      port.postMessage(tempVal);
    });
    dom.create.addEventListener("input", function onInput(e) {
      tempVal = e.target.value;
    });
  }

  globalThis.onmessage = e => {
    childBox.result.textContent = e.data;
  }

  const iframe = document.createElement("iframe");
  iframe.setAttribute("src", "./iframe.html");
  iframe.style.setProperty("width", "500px")
  iframe.style.setProperty("height", "500px")
  iframe.onload = () => {
    const port = iframe.contentWindow;
    pushEventListener(childBox, port)
  }

  document.body.insertBefore(iframe, document.body.firstElementChild)

</script>

// iframe.html
<h1>iframe</h1>
<div class="result">

</div>
<script>
  console.log(top === window); // 在iframe 中top对象和window不相等
  const result = document.querySelector(".result");

  globalThis.addEventListener("message", e => {
    let data = e.data;

    if (data === "send") {
      top.postMessage("iframe 发送数据值") // 向顶层对象发送消息
      return
    }

    result.textContent = e.data;
  })
</script>

open:

// open
<h1>Open</h1>
<div class="result">
</div>
<script>
  const result = document.querySelector(".result");

  let globalePort = null;

  globalThis.addEventListener("message", e => {
    let data = e.data;
    let port = e.source;

    globalePort = port;

    if (data === "send") {
      port.postMessage("open 发送数据值"); // 也可以使用opener对象
      // opener对象在页面被open打开时会获得源页面的window引用
      return
    }

    result.textContent = e.data;
  });
</script>

// mian
<button id="click">跳转</button>
<input type="text" id="child-create">
<button id="child-send">发送</button>
<br>
<textarea name="" id="main-result" cols="30" rows="10" readonly></textarea>
<script>
  const childBox = {
    send: document.querySelector("#child-send"),
    create: document.querySelector("#child-create"),
    result: document.querySelector("#main-result")
  };

  function onMsg(e) {
    childBox.result.textContent =e.data;
  };

  globalThis.name="main";


  document.querySelector("#click").addEventListener("click", () => {
    port = open("/iframe.html");
    port.opener = null;
    pushEventListener(childBox);
  })

  function pushEventListener(dom, port) {
    let tempVal = ``;
    dom.send.addEventListener("click", function send() {
      port.postMessage(tempVal, [ch.port2]);
    });
    dom.create.addEventListener("input", function onInput(e) {
      tempVal = e.target.value;
    });
  }

  globalThis.onmessage = e => {
    childBox.result.textContent = e.data;
  }


</script>