关于我对js运行时的一些浅薄的理解:(2,给我们的js异步的能力,eventloop)
quickjs 究竟是什么
上一篇讲到这个轮子本质上其实就是一个编译器了。或者说是一个独立的js引擎。通过混入原生环境的方式大幅提高性能!当然了,也可以看作运行时,因为已经是一个可以进行系统调用的平台了
QuickJS 出自传奇程序员 Fabrice Bellard(膜拜)
给我们的js实现简单的异步
异步并非天生就有,计算机执行代码只有同步。所谓的异步无非就是用同步去模拟异步。一个很经典的思路是轮询(poll)
poll这个词在操作系统很常见,本质上就是通过等待队列实现异步。
前置知识 轮询和中断(不懂的建议复习操作系统)
quickjs中settimeout实现
虽然我们几乎每天都用到settimeout这api,但js引擎并没有这个api,这个api是每个运行时自己实现的,q浏览器和nodejs本身都实现了自己的setimeout
import { setTimeout } from "os";
setTimeout(() => { /* ... */ }, 0);
这就是在quickjs中调用settimeout的方式,和之前的fib并没啥区别
static JSValue js_os_setTimeout(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
int64_t delay;
JSValueConst func;
JSOSTimer *th;
JSValue obj;
func = argv[0];
if (!JS_IsFunction(ctx, func))
return JS_ThrowTypeError(ctx, "not a function");
if (JS_ToInt64(ctx, &delay, argv[1]))
return JS_EXCEPTION;
obj = JS_NewObjectClass(ctx, js_os_timer_class_id);
if (JS_IsException(obj))
return obj;
th = js_mallocz(ctx, sizeof(*th));
if (!th) {
JS_FreeValue(ctx, obj);
return JS_EXCEPTION;
}
th->has_object = TRUE;
th->timeout = get_time_ms() + delay;
th->func = JS_DupValue(ctx, func);
list_add_tail(&th->link, &os_timers);
JS_SetOpaque(obj, th);
return obj;
}
源码很简单,就是可以看出,这个 setTimeout 的实现中,并没有任何多线程或 poll 的操作,只是把一个存储 timer 信息的结构体通过 JS_SetOpaque 的方式,挂到了最后返回的 JS 对象上而已,是个非常简单的同步操作。因此,就和调用原生 fib 函数一样地,在 eval 执行 JS 代码时,遇到 setTimeout 后也是同步地执行一点 C 代码后就立刻返回,没有什么特别之处。
为什么能异步,可以翻看我之前的那个文章实现fib时候的代码,很简单,main函数里调用了loop()
int main(int argc, char **argv)
{
// ...
// eval JS 字节码
js_std_eval_binary(ctx, qjsc_hello, qjsc_hello_size, 0);
// 启动 Event Loop
js_std_loop(ctx);
// ...
}
而loop实现也很简单
/* main loop which calls the user JS callbacks */
void js_std_loop(JSContext *ctx)
{
JSContext *ctx1;
int err;
for(;;) {
/*在这里执行pending操作*/
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;
}
}
和网络上所有的evetloop博客写的一样,双重无限循环,先执行所有的回调,然后在进行os_poll_func
这里的os_poll_func
就是原理类似的 poll 系统调用,从而可以借助操作系统的能力,使得只在【定时器触发、文件描述符读写】等事件发生时,让进程回到前台执行一个 tick,把此时应该运行的 JS 回调跑一遍,而其余时间都在后台挂起。在这条路上继续走下去,就能以经典的异步非阻塞方式来实现整个运行时啦。