Trabalhe com o Trabalhador "como desejar" e não "o máximo possível"

Este artigo usará o método DIRTY, inseguro, "muleta", assustador etc. Os fracos de coração não leem!


Devo dizer imediatamente que não foi possível resolver alguns problemas de usabilidade: você não pode usar o fechamento no código que será passado para o worker.
Trabalhe com o trabalhador "como desejar", mas não "o máximo possível"


Todos nós gostamos de novas tecnologias e gostamos quando é conveniente usá-las. Mas no caso do trabalhador, isso não é inteiramente verdade. O Worker trabalha com um arquivo ou link de arquivo, mas isso é inconveniente. Eu gostaria de poder colocar qualquer tarefa no trabalhador, e não apenas o código especialmente planejado.


O que é necessário para tornar o trabalho com o trabalhador mais conveniente? Na minha opinião, o seguinte:


  • Capacidade de executar código arbitrário no trabalhador a qualquer momento
  • Capacidade de passar dados complexos para o trabalhador (instâncias de classe, funções)
  • Capacidade de obter Promise com uma resposta do trabalhador.

Primeiro, precisamos de um protocolo de comunicação entre o trabalhador e a janela principal. Em geral, um protocolo é simplesmente a estrutura e os tipos de dados com os quais a janela e o trabalhador do navegador se comunicarão. Não há nada complicado. Você pode usar algo assim ou escrever sua própria versão. Em cada mensagem, teremos um ID e dados específicos para um tipo específico de mensagem. Para começar, teremos dois tipos de mensagens para o trabalhador:


  • adicionando bibliotecas / arquivos ao trabalhador
  • lançamento do trabalho

Arquivo dentro do trabalhador


Antes de começar a criar um trabalhador, é necessário descrever um arquivo que funcione no trabalhador e suporte o protocolo descrito por nós. Eu amo OOP , então será uma classe chamada WorkerBody. Esta classe deve se inscrever no evento na janela pai.


 self.onmessage = (message) => { this.onMessage(message.data); }; 

Agora podemos ouvir eventos na janela principal. Temos dois tipos de eventos: aqueles aos quais a resposta está implícita e todos os outros. Processamos eventos.
A adição de bibliotecas e arquivos ao trabalhador é feita usando a API importScripts .


E o pior: usaremos eval para executar uma função arbitrária.


 ... onMessage(message) { switch (message.type) { case MESSAGE_TYPE.ADD_LIBS: this.addLibs(message.libs); break; case MESSAGE_TYPE.WORK: this.doWork(message); break; } } doWork(message) { try { const processor = eval(message.job); const params = this._parser.parse(message.params); const result = processor(params); if (result && result.then && typeof result.then === 'function') { result.then((data) => { this.send({ id: message.id, state: true, body: data }); }, (error) => { if (error instanceof Error) { error = String(error); } this.send({ id: message.id, state: false, body: error }); }); } else { this.send({ id: message.id, state: true, body: result }); } } catch (e) { this.send({ id: message.id, state: false, body: String(e) }); } } send(data) { data.body = this._serializer.serialize(data.body); try { self.postMessage(data); } catch (e) { const toSet = { id: data.id, state: false, body: String(e) }; self.postMessage(toSet); } } 

O método onMessage responsável por receber a mensagem e selecionar o manipulador, doWork - inicia a função passada e envia a resposta para a janela pai.


Analisador e serializador


Agora que temos o conteúdo de worker, precisamos aprender a serializar e analisar todos os dados para transmiti-los ao worker. Vamos começar com o serializador. Queremos poder passar quaisquer dados para o trabalhador, incluindo instâncias de classe, classes e funções. Mas com os recursos nativos do worker, só podemos transmitir dados do tipo JSON. Para contornar essa proibição, precisamos avaliar . Tudo o que não pode aceitar JSON, envolveremos as construções de string correspondentes e executaremos do outro lado. Para preservar a imutabilidade, os dados obtidos são clonados em tempo real e os que não podem ser serializados da maneira usual são substituídos por objetos de serviço e, por sua vez, são substituídos pelo analisador do outro lado. À primeira vista, pode parecer que essa tarefa não seja difícil, mas há muitas armadilhas. A pior limitação dessa abordagem é a incapacidade de usar o fechamento, que carrega um estilo ligeiramente diferente de escrever código. Vamos começar com o mais simples, com a função Primeiro, você precisa aprender como distinguir uma função de um construtor de classe.


Vamos tentar distinguir:


 static isFunction(Factory){ if (!Factory.prototype) { // Arrow function has no prototype return true; } const prototypePropsLength = Object.getOwnPropertyNames(Factory.prototype) .filter(item => item !== 'constructor') .length; return prototypePropsLength === 0 && Serializer.getClassParents(Factory).length === 1; } static getClassParents(Factory) { const result = [Factory]; let tmp = Factory; let item = Object.getPrototypeOf(tmp); while (item.prototype) { result.push(item); tmp = item; item = Object.getPrototypeOf(tmp); } return result.reverse(); } 

Primeiro, descobriremos se a função possui um protótipo. Caso contrário, essa é definitivamente uma função. Em seguida, examinamos o número de propriedades no protótipo e, se no protótipo apenas o construtor e a função não são os herdeiros de outra classe, consideramos que essa é uma função.


Após encontrar uma função, simplesmente a substituímos por um objeto de serviço pelos campos __type = "serialized-function" e template , que é igual ao modelo dessa função ( func.toString() ).


Por enquanto, pule a classe e analise a instância da classe. Além disso, nos dados, precisamos distinguir objetos comuns de instâncias de classe.


 static isInstance(some) { const constructor = some.constructor; if (!constructor) { return false; } return !Serializer.isNative(constructor); } static isNative(data) { return /function .*?\(\) \{ \[native code\] \}/.test(data.toString()); } 

Consideramos que um objeto é comum se não tiver um construtor ou se for uma função nativa. Após identificar a instância da classe, a substituímos por um objeto de serviço por campos:


  • __type - 'instância serializada'
  • data - dados que estavam na instância
  • index - o índice de classe desta instância na lista de serviços de classe.

Para transferir dados, precisamos criar um campo adicional: nele armazenaremos uma lista de todas as classes únicas que passarmos. A parte mais difícil é que, quando uma classe é detectada, pegue não apenas o modelo, mas também o modelo de todas as classes-pai e salve-as como classes separadas - para que cada "pai" seja passado não mais de uma vez - e salve a verificação na instância de. Definir uma classe é fácil: esta é uma função que não passou no teste Serializer.isFunction. Ao adicionar uma classe, verificamos a presença de uma classe na lista de dados serializados e adicionamos apenas dados únicos. O código que coleta a classe em um modelo é bastante grande e fica aqui .


No analisador, primeiro analisamos todas as classes passadas para nós e as compilamos se elas não foram aprovadas antes. Em seguida, percorremos recursivamente cada campo de dados e substituímos os objetos de serviço por dados compilados. A coisa mais interessante está na instância da classe. Temos uma classe e há dados que estavam em sua instância, mas não podemos simplesmente instancia-los, porque a chamada ao construtor pode ter parâmetros que não temos. Um método Object.create quase esquecido vem em nosso auxílio, que retorna um objeto com um determinado protótipo. Portanto, evitamos chamar o construtor e obter uma instância da classe e apenas reescrever as propriedades na instância.


Trabalhador de criação


Para que o trabalhador trabalhe com sucesso, precisamos ter um analisador e serializador dentro e fora do trabalhador; portanto, pegamos o serializador e transformamos o serializador, analisador e corpo do trabalhador em um modelo. Fazemos um blob a partir do modelo e criamos um link de download via URL.createObjectURL (esse método pode não funcionar com algumas "Políticas de segurança de conteúdo"). Este método também é adequado para executar código arbitrário a partir de uma string.


 _createWorker(customWorker) { const template = `var MyWorker = ${this._createTemplate(customWorker)};`; const blob = new Blob([template], { type: 'application/javascript' }); return new Worker(URL.createObjectURL(blob)); } _createTemplate(WorkerBody) { const Name = Serializer.getFnName(WorkerBody); if (!Name) { throw new Error('Unnamed Worker Body class! Please add name to Worker Body class!'); } return [ '(function () {', this._getFullClassTemplate(Serializer, 'Serializer'), this._getFullClassTemplate(Parser, 'Parser'), this._getFullClassTemplate(WorkerBody, 'WorkerBody'), `return new WorkerBody(Serializer, Parser)})();` ].join('\n'); } 

Resultado


Assim, obtivemos uma biblioteca fácil de usar que pode executar código arbitrário no worker. Ele suporta classes do TypeScript. Por exemplo:


 const wrapper = workerWrapper.create(); wrapper.process((params) => { // This code in worker. Cannot use closure! // do some hard work return 100; // or return Promise.resolve(100) }, params).then((result) => { // result = 100; }); wrapper.terminate() // terminate for kill worker process 

Planos adicionais


Infelizmente, esta biblioteca está longe de ser ideal. É necessário adicionar suporte para setters e getters em classes, objetos, protótipos, propriedades estáticas. Também gostaríamos de adicionar armazenamento em cache, executar um script alternativo sem avaliação via URL.createObjectURL e adicionar um arquivo com o conteúdo do worker ao assembly (se a criação não estiver disponível em tempo real). Venha para o repositório !

Source: https://habr.com/ru/post/pt462155/


All Articles