发布于 · 最后修改时间 · 标签: javascript cogitation tools


I am a heavy user of Comlink-v1 and have worked hard to promote it at my company.


It's great, but still has some issues, such as the user having to know how it works behind the scenes, and sometimes triggering some low-level errors when serializing or deserializing it when passing parameters.


Overall, Comlink-v1 has some flaws, but it solves a lot of problems.


Recently I've been rethinking some of the flaws in Comlink-v1 and trying to re-implement it. After a few attempts, I have to say that those flaws are really hard to avoid, and for that I had to sacrifice its versatility. So I finally implemented Comlink-v2. Unfortunately I can't publish the source code, but I can provide the basic idea of the implementation. I believe that this new idea will bring new magic to the JS field.

效果预览 Effect Preview


This is the code that has been tested:

Effect Preview

实现思路 Implementation approach


As you know, Comlink-v1 uses the feature of await to implicitly call the then function to do this.

在Comlink-v2中,我努力将所有异步给消除了。众所周知,在js中,同步是异步特性的根基,也正因此我们能将async/await编译成es5的代码来运行。所以理论上,只要在comlink重摆脱了异步的依赖,那么就彻底拥有了更多的语言特性。比如 instanceofprop in obj 等等。

In Comlink-v2, I tried to eliminate all asynchrony. As you know, synchronization is the basis of asynchronous features in js, and that's why we can compile async/await into es5 code and run it. so theoretically, once we get rid of the asynchronous dependency in comlink, we have more features in the language. For example, instanceof, prop in obj, and so on.


For this, I use the Atomic.wait/notify pair of APIs. Of course, it has to work in a web-worker to work in a browser. This does introduce some limitations, but believe me, the features that will eventually come with Comlink-v2 will indirectly remove these limitations.


With the addition of Atomic-API, we can now pause a worker and wait for other threads to finish executing before waking it up. So you can now use the full magic of Proxy.

具体实现方法 Specific implementation


There are three main areas of work to be done next.

  1. 要将数据分成三类:cloneable(string、number、boolean、bigint、null、undefined);symbol;reference(object、function)

To divide data into three categories: cloneable (string, number, boolean, bigint, null, undefined); symbol; reference (object, function)

  1. 使用Atomic来进行通讯

Using Atomic for communication

  1. 内存的引用与释放

Reference and release of memory


In Comlink-v2, we no longer need serialization and deserialization of registration data, because we embrace synchronization. So for references, we use a unified Proxy implementation, which directly eliminates the various side effects present in Comlink-v1. Just as you would just proxy a reference object in the same isolate.


Secondly, there is the rather special type of symbol. We can't Proxy symbols, but relying on the properties of symbols, which are themselves unshared in different isolate, we can create a copy of the symbol in a different isolate and register a unique id for it.


There are two things to note about Symbol:

  1. 要使用Symbol.keyFor来判断是否是使用Symbol.for创建的;

To use Symbol.keyFor to determine if it was created using Symbol.for.

  1. 我们还需要预先对Symbol.iterator,Symbol.hasInstance...这些特殊的symbol进行预先注册。

We also need to pre-register the special symbols Symbol.iterator,Symbol.hasInstance....


Next let's talk about how to use Atomic for communication.


First we need a shareArrayBuffer, which is used to store all the data needed for the two workers to communicate. But we shouldn't use polling to let the other worker know when to start processing the data, so we also need a messageChannel, which is used to inform the other thread to start processing.

// write message size and content to shareAreayBuffer...
msgPort.postMesaage('Please wake me up');
// read result from shareArrayBuffer


The next problem that comes up is the problem of calling the stack. Executing a task, two threads may need to interact multiple times.

So my solution is to record the call stack lengths:

// write message size and content to shareAreayBuffer...
msgPort.postMesaage('Please wake me up');
const stackLen = sab_i32a[0] = 1;
// loop
if (sab_i32a[0] === stackLen - 1)
// read result from shareArrayBuffer

对此,我的解决方案是 WeakRef + FinalizationRegistry

Lastly, and most importantly. Since we simulate reference objects in JavaScript, when we import an object, then it is registered, which means that it cannot be actively released, and if it is forced to be released, it may cause an exception when other threads use refId (reference ID) to look for it, only to find that the object is released.
For this, my solution is WeakRef + FinalizationRegistry


You only need to listen to the referrer's memory release to notify the data provider of the released object. The only unfortunate thing is that we can't listen for symbol releases. So be careful not to let two workers interact with too many symbol.

编程技巧 Programming skill


Although we use synchronous as the basis for communication, we should also support asynchronous in order to have a richer use case for it.


How can we make the same set of code compatible with both asynchronous and synchronous? My suggestion would be to use callback.

// sync
let res;
console.log('callback ❤️ sync');

// async
const res = new Promise(cb=>flow.dosomething(1,2,3,cb));

console.log('callback ❤️ async');

展望 Look ahead


Let me conclude by talking about the impact that some of its features will have:

  1. 在浏览器中,我们完全可以使用Comlink-v2来代理主线程。从而将业务完全运行在web-worker中。但因为浏览器的局限性,我们需要结合同步和异步的接口来进行实现。这点很像在nodejs中开发native插件。

In the browser, we could have used Comlink-v2 to proxy the main thread. This allows us to run the business entirely in the web-worker. But because of browser limitations, we need to combine synchronous and asynchronous interfaces to do so. This is much like developing native plugins in nodejs.

  1. 我觉得这可以作为JavaScript并发的实现,它可以给我们带来很多想象。虽然Comlink-v1已经做到,但是同步的接口将会更加自然,开发者使用时,局限性更少:想象一下erlang的特性将会基于此实现~

I think this can be implemented as JavaScript concurrency, it can give us a lot to imagine. Although Comlink-v1 already does this, the synchronous interface will be more natural and less restrictive for developers to use: imagine the features of erlang will be based on this implementation ~!

  1. 因为少了注册模型的序列化与反序列化,所以它与其它语言更容易交互。(事实上这正是我们公司将要做的事情)

Because there is less serialization and deserialization of registration models, it is easier to interact with other languages. (In fact this is exactly what we are going to do at our company.)