async 函数是 javascript 中处理异步编程的重要关键字。如我们所知, async 函数执行时会在遇到第一个await关键字时立即返回一个 promise。本文将介绍 quickjs 是如何处理这样一个流程的。
测试样例代码如下
async function func() {
console.log("1");
let ret = await new Promise((resolve)=>{
console.log("4");
resolve(10);
console.log("3");
});
return ret;
}
func().then((ret)=> {
console.log(ret);
});
console.log("2");
将其编译成 quickjs 字节码后如下
input.js:3: function: <null>
args: resolve
stack_size: 3
opcodes:
;; (resolve)=>{
;; console.log("4");
get_var console
get_field2 log
push_const8 0: 1"4"
call_method 1
drop
;; resolve(10);
get_arg0 0: resolve
push_i8 10
call1 1
drop
;; console.log("3");
get_var console
get_field2 log
push_const8 1: 1"3"
call_method 1
;; }
return_undef
/* async func 编译的字节码 */
input.js:1: function: func
locals:
0: let ret [level:1 next:-1]
stack_size: 3
opcodes:
;; async function func() {
set_loc_uninitialized 0: ret
;; console.log("1");
get_var console
get_field2 log
push_const8 0: 1"1"
call_method 1
drop
;; let ret = await new Promise((resolve)=>{
get_var Promise
dup
;; console.log("4");
;; resolve(10);
;; console.log("3");
;; });
fclosure8 1: [bytecode <null>]
call_constructor 1
await
put_loc0 0: ret
;; return ret;
get_loc_check 0: ret
return_async
;; }
input.js:12: function: <null>
args: ret
stack_size: 3
opcodes:
;; (ret)=> {
;; console.log(ret);
get_var console
get_field2 log
get_arg0 0: ret
call_method 1
;; }
return_undef
/*整个模块的字节码*/
input.js:1: function: <eval>
locals:
0: var <ret>
stack_size: 3
opcodes:
check_define_var func,64
fclosure8 0: [bytecode func]
define_func func,0
get_var func
call0 0
get_field2 then
fclosure8 1: [bytecode <null>]
call_method 1
put_loc0 0: "<ret>"
get_var console
get_field2 log
push_const8 2: 1"2"
call_method 1
set_loc0 0: "<ret>"
return
我们知道,quickjs 中执行 js 函数的代码是 JS_CallInternal
根据以上字节码信息,JS_CallInternal
第一次调用,会进入执行整个模块的代码,执行到
call0 0
时,会进入执行
async func() { ... } 的代码
挂 gdb 在 JS_CallInternal
打断点试一下
这是第一个 JS_CallInternal
执行 call0
字节码的地方
我们 s 进去,看看怎么执行 async
函数的
在判断函数类型的时候,发现不是一个 字节码函数
打印 p->class_id 发现它是 52,对应到
eunm {
...
JS_CLASS_ASYNC_FUNCTION,
...
}
非常合理
然后进入16271 行的 c 函数执行
这个call_func 实际上是:
static JSValue js_async_function_call(JSContext *ctx, JSValueConst func_obj,
JSValueConst this_obj,
int argc, JSValueConst *argv, int flags)
{
JSValue promise;
JSAsyncFunctionState *s;
s = async_func_init(ctx, func_obj, this_obj, argc, argv);
if (!s)
return JS_EXCEPTION;
promise = JS_NewPromiseCapability(ctx, s->resolving_funcs);
if (JS_IsException(promise)) {
async_func_free(ctx->rt, s);
return JS_EXCEPTION;
}
js_async_function_resume(ctx, s);
async_func_free(ctx->rt, s);
return promise;
}
代码并不长,重要逻辑是
- 创建了 Promise,用来做 resolve 之后回复 async 函数的执行
- js_async_function_resume 是真正开始执行 async 函数。虽然叫 resume(恢复),但是第一次执行和后续的恢复执行逻辑是一样的,所以就用这个了。
根据上面的字节码信息,await 关键字 被翻译成了 OP_await
字节码
他的操作很简答,如下
记录下当前栈帧位置然后直接退出函数执行。
后续的操作就由 Promise 来接管了。
本来想到这里就结束的。结果发现接下来的操作更有有趣,就继续写一下。promise 是如何接管接下来的异步操作的?
我们先建立如下模型,理解其中的两个 promise
async func() {
code1
await new promise; // promise1
code2
}
let promise2 = func();
调用 func 之后,我们期待的行为是这样的
- 执行 code1
- 执行 promise1,然后退出 func
- promise1,resolve 后 执行code2。相当于恢复 func 的执行。
quickjs 的做法是,退出 func 时保存 func 的执行状态。利用一个 promise2 和 promise1 建立联系。
promise2 的 链式调用(相当于使用 then 注册的回调) 就是执行
js_async_function_resume
也就是上面说的恢复执行函数。
而 promise1 的 then,其实就是将 promise2 的状态改变为 resolve 态。
所以 quickjs 在执行完一次 func 后,还需要将内部的 promise1 拿出来,和自己保存的 promise2 建立一个联系
代码在这一行,关键代码注释如下:
value = s->frame.cur_sp[-1]; // value 是 await 的对象,一般是promise1
...
// 下面这一句处理 value 不是一个 promsie 的情况。毕竟 await 3; 这样的代码也是正确的
promise = js_promise_resolve(ctx, ctx->promise_ctor,
1, (JSValueConst *)&value, 0);
// 创建了 promise2 的 resolve 函数,其类型是:JS_CLASS_ASYNC_FUNCTION_RESOLVE
if (js_async_function_resolve_create(ctx, s, resolving_funcs)) {
JS_FreeValue(ctx, promise);
goto fail;
}
...
// 下面这一句建立 promise1 和 promise2 之间的联系,
// 也就是吧 resolving_funcs 放入 promise1 的链式调用队列中。
res = perform_promise_then(ctx, promise,
(JSValueConst *)resolving_funcs,
(JSValueConst *)resolving_funcs1);