lxf2023-03-16 10:56:01

前面的章节都是告诉你怎么使用Worker,并没有真正的深入Worker原理,这一章我们就来详细的了解一下Worker的原理。

Worker 的全局作用域

WorkerGlobalScopeWorker的全局作用域,它继承自EventTarget

EventTarget

EventTarget是一个接口,它定义了一些方法,用来注册和触发事件,它的实现有WindowWorkerGlobalScopeNodeXMLHttpRequest等。

它拥有三个方法:

  • addEventListener:注册事件
  • removeEventListener:移除事件
  • dispatchEvent:触发事件

这些方法应该都很熟悉,我们在平时的开发中也经常使用,例如:

function onMessage (event) {
  console.log(event.data)
}

window.addEventListener('message', onMessage);

window.dispatchEvent(new MessageEvent('message', { data: 'hello' }));

window.removeEventListener('message', onMessage);

上面就是一个简单的事件注册、触发、移除的例子,这些都是通过EventTarget来实现的。

WorkerGlobalScope

上面提到了WorkerGlobalScopeWorker的全局作用域,它继承自EventTarget,所以它也拥有addEventListenerremoveEventListenerdispatchEvent这三个方法。

它还有一些其他的属性和方法:

标准属性和方法

标准属性指的是规范中已经确定的属性,通常情况下它们都是固定不会再发生太大的变化:

  • self(只读):指向当前的WorkerGlobalScope,它的值和this是一样的;
  • location(只读):指向当前WorkerGlobalScopeURL,它是一个Location对象;
  • navigator(只读):指向当前WorkerGlobalScopeNavigator对象;
  • importScripts():用来加载脚本,它的参数是一个或多个脚本的URL,例如:importScripts('a.js', 'b.js'),它的返回值是undefined,如果加载失败,会抛出一个NetworkError的异常;
  • onerror:用来注册error事件的回调函数;
  • onlanguagechange:用来注册languagechange事件的回调函数;
  • onoffline:用来注册offline事件的回调函数;
  • ononline:用来注册online事件的回调函数;
  • onrejectionhandled:用来注册rejectionhandled事件的回调函数,它是Promise的一个事件;
  • onunhandledrejection:用来注册unhandledrejection事件的回调函数, 它是Promise的一个事件;

在之前的示例中,我们用过selfimportScriptsonerror,这些都是WorkerGlobalScope的标准属性。

参考:the-workerglobalscope-common-interface

非标准属性和方法

非标准属性指的是规范中还没有确定的属性,它们的实现是不稳定的,可能会在未来的版本中发生变化,所以不建议在生产环境中使用。

  • performance:指向当前WorkerGlobalScopePerformance对象,它是一个常规的Performance对象;
  • console:指向当前WorkerGlobalScopeConsole对象,它是一个常规的Console对象;
  • dump():用来打印日志,几乎没有浏览器实现了这个方法,所以不建议使用,直接使用console.log就可以了;

上面这些属性和方法这里就先简单的了解一下,因为每一个属性都够一个单独的文章来讲解,所以这里就暂时不展开了。

self

self是一个只读属性,它指向当前的WorkerGlobalScope,它的值和this是一样的。

上面所有提到的属性都可以使用self来访问,例如:self.locationself.navigatorself.console等等。

同时也可以省略self,直接使用属性名来访问,例如:locationnavigatorconsole等等。

这就像window对象一样,它的属性和方法都可以使用window来访问,也可以省略window,直接使用属性名来访问,例如:

// 通过 window 访问 location 属性
window.location.href

// 直接使用 location 属性
location.href

// 也可以使用 this 访问 location 属性
this.location.href

WorkerGlobalScope中,selfthis是一样的,所以可以省略self,直接使用属性名来访问,例如:

// 通过 self 访问 location 属性
self.location.href

// 直接使用 location 属性
location.href

// 也可以使用 this 访问 location 属性
this.location.href

其他属性的访问方式也是一样的,例如:navigatorconsole等等。

所以我们在写worker.js的时候,可以省略self,直接使用属性名来访问,例如:

addEventListener('message', function (event) {
  console.log(event.data)
})

上面的console其实也是一个WorkerGlobalScope的属性,它指向当前WorkerGlobalScopeConsole对象。

这些都和我们使用window对象是一样的,所以我们可以把WorkerGlobalScope当成一个window对象来使用。

可以使用什么

上面着重的讲解了一下self属性,再加上最开始介绍了一下WorkerGlobalScope的属性和方法,所以我们可以把WorkerGlobalScope当成一个window对象来使用。

但是WorkerGlobalScopewindow对象还是有一些区别的,我们来看一下下面这个例子:

// worker.js
addEventListener('message', function (e) {
  console.log(e.data)
  console.log(location.href)
})

省略main.js代码,很简单,这里只探索worker.js的代码。

虽然这里也可以使用location属性,但是它的指向并不是window.location,而是WorkerLocation.location

它们之间的区别也很明显,window.location指向的是当前页面的地址,而WorkerLocation.location指向的是worker.js文件的地址。

不过这个并不是我们这次要谈论的主要问题,只是告诉大家,虽然在WorkerGlobalScope能用到很多和window对象一样的属性和方法,但是它们的结果,或者说指向的对象是不一样的。

Worker中可以使用Web API有很多,但是它并不是挂载子啊WorkerGlobalScope,而是通过类似混入的方式,进入到Worker的上下文中,有如下:

  • Broadcast Channel API
  • Cache API
  • Channel Messaging API
  • Console API
  • Crypto
  • CustomEvent
  • Data Store(仅 Firefox)
  • DOMRequest 和 DOMCursor
  • Fetch
  • FileReader
  • FileReaderSync(仅在 worker 中可用)
  • FormData
  • ImageData
  • IndexedDB
  • Network Information API
  • Notifications
  • Performance
  • PerformanceEntry
  • PerformanceMeasure (en-US)
  • PerformanceMark (en-US)
  • PerformanceObserver
  • PerformanceResourceTiming
  • Promise
  • Server-sent 事件
  • ServiceWorkerRegistration
  • TextEncoder 和 TextDecoder
  • URL
  • WebGL 中的 OffscreenCanvas(通过特性首选项 gfx.offscreencanvas.enabled 启用)
  • WebSocket
  • XMLHttpRequest(尽管 responseXML 和 channel 属性始终为 null)

上面的列表来自MDN Worker 中可用的 Web API

除了上面提到的这些Web API,还有一些WorkerGlobalScope的属性和方法,就是文章最开始提到的,其他的就都不能使用了,例如documentwindowparent等等。

DedicatedWorkerGlobalScope

上面讲到WorkerGlobalScope其实也就是一个接口,它是DedicatedWorkerGlobalScope的父接口;

DedicatedWorkerGlobalScope就是Web Worker的父类,这个就是我们今天的主角。

属性和方法

直接看函数签名:

interface DedicatedWorkerGlobalScope extends WorkerGlobalScope {
    readonly name: DOMString;

    postMessage(message: any, transfer?: Transferable[]): void;
    postMessage(message: any, options?: Transferable): void;

    close(): void;

    onmessage: EventHandler;
    onmessageerror: EventHandler;
}

在这里就出现了我们最开始使用的postMessageonmessageonmessageerror,这三个属性和方法是DedicatedWorkerGlobalScope的属性和方法。

看到函数签名之后,我们发现postMessage方法有两种重载,第二个参数是可选的,目前并没有找到它的具体用法;

通过HTML Standard的描述,这个参数是用来传输数据的,具体怎么用还没找到相关的资料,这个暂时先不讨论了。

Worker就是实现了这个接口,所以我们可以在Worker中使用这些属性和方法。

Worker是我之前讲的三个Web Worker最简单的一个,看这个函数签名也就明白了,并没有太多的东西。

数据传输

Workerwindow之间的数据传输,是通过postMessageonmessage来实现的。

在我最开始的文章就讲过,postMessage只能传递JSON格式的数据,而且传递的数据是clone的,所以在Worker中修改传递过来的数据,不会影响到window中的数据。

但是这个clone的过程是有一定的限制的,并不是单纯的使用JSON.stringifyJSON.parse来实现的,而是使用结构化克隆算法来实现的,可以参考structuredClone

上面这个API是浏览器的一个新特性,并不是Web Worker的特性。

JSON.stringify来举例,在转换的数据中如果包含functionundefinedsymbol等类型的数据,会忽略这些数据类型的数据,将符合转换的数据转换成JSON格式的。

但是在Worker中,这些类型的数据是不能被clone的,所以会直接报错,错误信息会通过onmessageerror来传递。

const worker = new Worker('worker.js');
const obj = {
  a: 1,
  b: undefined,
  c: function () {},
  d: Symbol('d'),
};
console.log(JSON.stringify(obj)); // {"a":1}
worker.postMessage(obj); // Uncaught DOMException: Failed to execute 'postMessage' on 'Worker'

上面的示例中,使用JSON.stringify来转换数据,可以看到bcd这三个属性都没有被转换,但是在Worker中,这三个属性都会报错。

const worker = new Worker('worker.js');
var obj1 = {
    name: 'obj1'
}

var obj2 = {
    obj1: obj1,
    name: 'obj2'
}
obj1.obj2 = obj2;
worker.postMessage(obj2);
console.log(JSON.stringify(obj2));

相关专题