January 1, 2024 by 严肃游戏

从实现看 JavaScript 的事件循环

JS Engine EventLoop 实现一瞥

事件循环这个高雅的词我从出生到现在已经听过无数遍了,关于 Node.JS 的 EventLoop 那张传世经典流程图也见过无数次,基本上所有讲 Node.JS EventLoop 的文章都会引用到这个流程图:

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

这张图,乍看一下有非常多的流程,既有宏任务,又有微任务的各个过程,非常复杂和繁琐,我在相当长的一段时间里都无法完全理解,仅从图上看,我无法想象那么复杂的流程应该怎么理解和记忆,底层的实现又该多复杂,直到有一次机缘巧合,看到了某位前辈的一篇关于「基于 QuickJS 制作 JS Runtime 」的文章,我终于一瞥 EventLoop 的真实实现,他真的就只是一个无限循环的 Loop,只是这样:

// https://vscode.dev/github/bellard/quickjs/blob/master/quickjs-libc.c#L3941-L3942
/* main loop which calls the user JS callbacks */
void js_std_loop(JSContext *ctx)
{
    JSContext *ctx1;
    int err;

    for(;;) {
        /* execute the pending jobs */
        for(;;) {
            err = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1);
            if (err <= 0) {
                if (err < 0) {
                    js_std_dump_error(ctx1);
                }
                break;
            }
        }

        if (!os_poll_func || os_poll_func(ctx))
            break;
    }
}

它的精髓就在于把复杂的异步过程简化为一个单线程循环的过程,这个无限循环的过程就是我们常说的 EventLoop,这个 EventLoop 里面会不断的执行 JS 代码,当遇到异步操作时,会把异步操作的回调函数放到一个队列里面,异步操作由引擎放到其他线程/进程执行,等到 JS 代码执行完毕后,再去执行这个队列里面的回调函数把执行结果返回给调用方。这样一来,就不需要再考虑什么多线程同步和各种锁,也无需考虑什么多进程通信,统一都丢到 Eventloop,使用回调/Promise/async/await,静静等待执行完成后触发回调拿到结果即可。

将异步的复杂流程都丢给 JS Engine,JS 代码只需要考虑触发和回调逻辑,极大地降低了语言的复杂度。这大概也是 JS 看起来这么像玩具语言的原因,它的所有功能基本上都是由宿主/引擎提供的,复杂度都被隐藏在宿主之下,使用 JS 只需要考虑调用和等待返回结果,没有那么多弯弯绕绕,所以才会如此受欢迎。现代语言的趋势大概都如此,复杂度由底层实现,上层只需要调用即可,这样才能让开发者更专注于业务逻辑,而不是底层实现。

当然这里 QuickJS 的实现是比较简化了,但是既然都符合 ECMAScript 的规范,那么可以认为他们是等价的,这里我们就以 QuickJS 的实现为引子,来挖掘下各个 JS Engine 的 EventLoop 实现。

未完待续