'Elegantly reattach methods to object in web worker?
I'm playing around with web workers. I have some medium sized data objects which have operations which take a decent amount of time. Web Workers seem like a good way to parallelize those operations but I'm finding it difficult to choose the best way to pass them to a web worker. Web workers can serialize and deserialize the objects but that leaves them stripped of their class methods. I really want to use their methods of course.
The question is what is a good way to handle reattaching their methods? Clearly one could just copy the methods on to the object passed in but this implies that we know what kind of object we got. To make matters more difficult some objects contain data of other classes meaning we need to reattach methods to those. The first thing that comes to mind is implementing an alternative constructor for each class which takes a JSON object as the parameter. The alternate constructor can then recursively call the JSON constructor for any data member.
Then the problem of how do I choose the correct constructor in the beginning The approaches I've thought of include:
- Implementing different web workers one for each class (awkward)
- Implementing a factory method that calls the correct constructor, (implying I sent directly specify the class of the object sent before it is sent) (better, but still awkward)
- Implement some type guessing function to figure out which class the top level object is. (-_-)
Does anybody have a better idea?
Solution 1:[1]
Apparently, all I really needed to do was understand the __proto__ property of Javascript. This can re-establish the availability of class methods for an object after the object is passed to a web worker or retrieved from local storage.
function reattachMethods(serialized,originalclass) {
serialized.__proto__ = originalclass.prototype;
}
The issue with some of the above mentioned solutions is that you end up remaking/copying objects even though you already have all of the data in the correct format. If you have an object containing n elements then that is n elements that need to be copied into the new object after that n objects was serialized and sent from the browser to the web worker. This is obviously not ideal. Double underscore proto was a nonstandard feature which was implemented in all the major browsers and has been announced as part of the next javascript standard.
EDIT 2022: Modern Answer
While the above was true for 2013 the JavaScript language has had many updates since then. If you were attempting the same operation today there are now language provided methods which are recommended to use instead. __proto__ has become deprecated and its use is now discouraged.
In particular, Object.getPrototypeOf(object) and Object.setPrototypeOf(object, prototype) take over the role.
class LNode {
constructor(value, next = null) {
this.value = value;
this.next = next;
}
size() {
return 1 + (this.next === null ? 0 : this.next.size());
}
}
let test = new LNode(1, new LNode(2));
test.size(); //2
Object.getPrototypeOf(test); //{constructor: ƒ, size: ƒ}
let bareLinkedList = {value: 1, next: {value: 2, next: null}};
bareLinkedList.size //undefined
Object.setPrototypeOf(bareLinkedList, LNode.prototype);
Object.setPrototypeOf(bareLinkedList.next, LNode.prototype);
bareLinkedList.size(); //2
Solution 2:[2]
Option 2 sounds like the best option. If you establish a standard set of methods on your classes that know how to serialize and deserialize themselves, and also a global serialization helper, you can make this easy. Consider the following code:
deserialize = (function() {
var registry = {};
var deserialize = function(obj) {
var cls = obj['class'];
var data = obj['data'];
var fn = registry[cls];
return fn(data);
};
deserialize.registerClass(name, fn) {
registry[name] = fn;
};
return deserialize;
});
And the following class:
/**
* Information about a person
*/
var Person = function(firstName, lastName, age, phoneNumbers) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.phoneNumbers = phoneNumbers;
};
Person.serializeName = 'Person';
/**
* Serialize an instance of a Person
*/
Person.prototype.serialize = function() {
return {
'class': Person.serializeName,
'data': {
'first_name': this.firstName,
'last_name', this.lastName,
'age': this.age,
'phone_numbers': this.phone_numbers.map(function(number) { return number.serialize(); })
}
};
}
/**
* Deserialize an instance of a Person
*/
Person.deserialize(data) {
var phoneNumbers = data.phone_numbers.map(deserialize);
return new Person(data.first_name, data.last_name, data.age, phoneNumbers);
};
deserialize.register(Person.serializeName, Person.deserialize);
You can serialize any Person instance using data = person.serialize();, send this to your worker, and deserialize it using person = deserialize(data); Anything that produces data in the serialization format is deserializable, and you dont have to know what kind of class it is before you deserialize it - The type is carried along with the data. Nested data types are also possible, as demonstrated with the person.phoneNumbers. An implentation of a PhoneNumber class could be:
/*
* Phone numbers
*/
var PhoneNumber = function(number, numberType) {
this.number = number;
this.numberType = numberType;
}
PhoneNumber.prototype.serializeName = 'PhoneNumber';
/**
* Serialize an instance of a PhoneNumber
*/
PhoneNumber.prototype.serialize = function() {
return {
'class': PhoneNumber.serializeName,
'data': {
'number': this.number,
'number_type': this.numberType,
}
};
}
/**
* Deserialize an instance of a PhoneNumber
*/
PhoneNumber.deserialize(data) {
var phoneNumbers = data.phone_numbers.map(deserialize);
return new PhoneNumber(data.first_name, data.last_name, data.age, phoneNumbers);
};
deserialize.register(PhoneNumber.serializeName, PhoneNumber.deserialize);
Again, all the information for deserialiing a data chunk is in the chunk, so sending the data to deserialize will give you back an instance of something. If you reuse your classes across the normal code and the webworker code, you will get back an identical copy of the instance you started with.
Solution 3:[3]
The most straightforward way is to pass plain data to the web worker and then instantiate any custom objects (e.g. classes) you need for the actual work inside the web worker using that data. The code for those custom classes will have to be available to the web worker.
Then, you're not trying to pass methods across the boundary between main thread and web worker (which isn't something you can really do).
Your option 2 sounds closest to this. Pass data to the web worker and have it either have code that knows what kind of object to create from that data or use factory functions that can analyze the data and make the right types of object for the work that needs to be done.
I see no reason for the extra complication of option 1. You want separate web workers for each thread of execution you want to run in parallel, but you don't need different web workers for different types of work. Any given web worker can do any kind of work if given the right code to do so.
I'm not a fan of "guessing" in your option 3. If you want the web worker to know what type of object to create, then pass that data to the web worker explicitly. No reason to guess.
Solution 4:[4]
I think vkThread plugin
http://www.eslinstructor.net/vkthread/
can help you to resolve this problem. With this plugin you can execute any function (including object's method) in a separate thread (worker).
Another useful plugin is JSONfn
http://www.eslinstructor.net/jsonfn/
which can serialize javascript object with member functions.
Hope this helps,
--Vadim
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | Tim Heap |
| Solution 3 | jfriend00 |
| Solution 4 | vadimk |
