我使用了我自己的非常简单的库,它非常适合我的小型 V8 环境,并且由于 JS 调用堆栈保持不变,因此也很容易调试。不过,要使其与 Nodejs 或 Web 一起使用,它需要进行一些修改。
这里的理由:
对于调试,我们并不真正需要异步代码 - 我们希望在调试器中拥有易于理解的调用堆栈,尤其是。在节点检查器调试器中。
另请注意,到目前为止,所有异步代码都是完全人工的,我们完全同步地执行所有内容,在某种程度上通过 V8 微任务进行了模拟。
回调样式的异步代码在调用堆栈中已经很难理解。
生成器样式的异步代码在传统调试器中完全丢失了调用堆栈信息 - 其中包括与节点检查器一起使用的当前 Chrome Beta Developer Tools V8 调试器。这就是生成器(通常是协程)的本质。更高版本的调试器可能会处理这个问题,但今天并非如此。我们甚至需要一些特殊的 C++ 代码来获取信息。示例代码可以在这里找到:
https://github.com/bjouhier/galaxy-stack/blob/master/src/galaxy-stack.cc
所以如果我们想要有有用的调试器,今天我们不能使用生成器——至少不像通常那样使用。
我们仍然希望使用生成器风格的异步代码,因为与回调风格的代码相比,它使代码更具可读性。
我们引入了一个新函数async 来克服这个问题。想象一下以下回调风格的异步代码:
function doSthX(a, b, callback) { ... }
function doSthY(c, callback) {
doSthX(c/2, 42, function(err, res) {
if(err) return callback(err);
callback(null, c + res);
})
}
现在,与 async 函数和生成器样式代码相同的代码:
function* doSthX(a, b) { ... }
function* doSthY(c) {
var res = yield async(doSthX(c/2, 42));
return c + res;
}
我们可以提供两个版本的async(iter):
- 在堆栈顶部运行迭代器
iter。这将保持一个健全的调用堆栈,一切都按顺序运行。
- 将迭代器交给一些较低的处理程序并使其真正异步。
请注意,有一些值得注意的库可用于第二种方法:
https://github.com/visionmedia/co
http://taskjs.org/
https://github.com/creationix/gen-run
https://github.com/bjouhier/galaxy
目前,我们只实现第一种方法 - 使调试更容易。
如果我们曾经想要同时拥有两者,我们可以引入一个调试标志来通过这两种实现进行切换。
global.async = async;
function async(iter) {
// must be an iterator
assert(iter.next);
var gotValue;
var sendValue;
while(true) {
var next = iter.next(sendValue);
gotValue = next.value;
if(!next.done) {
// We expect gotValue as a value returned from this function `async`.
assert(gotValue.getResult);
var res = gotValue.getResult();
sendValue = res;
}
if(next.done) break;
}
return {
getResult: function() {
return gotValue;
}
};
}
// Like `async`, but wraps a callback-style function.
global.async_call_cb = async_call_cb;
function async_call_cb(f, thisArg /* , ... */) {
assert(f.apply && f.call);
var args = Array.prototype.slice.call(arguments, 2);
return async((function*() {
var gotCalled = false;
var res;
// This expects that the callback is run on top of the stack.
// We will get this if we always use the wrapped enqueueMicrotask().
// If we have to force this somehow else at some point, we could
// call runMicrotasks() here - or some other waiter function,
// to wait for our callback.
args.push(callback);
f.apply(thisArg, args);
function callback(err, _res) {
assert(!gotCalled);
if(err) throw err;
gotCalled = true;
res = _res;
}
assert(gotCalled);
return res;
})());
}
// get the result synchronously from async
global.sync_from_async = sync_from_async;
function sync_from_async(s) {
assert(s.getResult); // see async()
return s.getResult();
}
// creates a node.js callback-style function from async
global.callback_from_async = callback_from_async;
function callback_from_async(s) {
return function(callback) {
var res;
try { res = sync_from_async(s); }
catch(err) {
return callback(err);
}
return callback(null, res);
};
}
global.sync_get = sync_get;
function sync_get(iter) {
return sync_from_async(async(iter));
}
// this is like in gen-run.
// it's supposed to run the main-function which is expected to be a generator.
// f must be a generator
// returns the result.
global.run = run;
function run(f) {
return sync_get(f());
}