如何在 Node.js 或 Javascript 中将异步函数调用包装成同步函数?

假设您维护了一个公开函数 getData的库,您的用户调用它来获取实际数据:
var output = getData();
遮罩下的数据保存在一个文件中,因此您使用内置的 fs.readFileSync中的 Node.js 实现了 getData。很明显,getDatafs.readFileSync都是同步函数。有一天,您被告知将底层数据源切换到只能异步访问的 repo,比如 MongoDB。您还被告知要避免激怒您的用户,getData API 不能被更改为仅返回一个承诺或要求回调参数。你如何满足这两个要求?

使用回调/承诺的异步函数是 JavasScript 和 Node.js 的 DNA。任何非平凡的 JS 应用程序都可能充斥着这种编码风格。但是这种做法很容易导致所谓的回调金字塔的厄运。更糟糕的是,如果调用链中的任何调用方中的任何代码都依赖于异步函数的结果,那么这些代码也必须包装在回调函数中,从而对调用方施加编码样式约束。有时我发现需要将一个异步函数(通常由第三方库提供)封装成一个同步函数,以避免大规模的全局重构。在这个主题上搜索解决方案通常会得到从中派生的 节点光纤或 npm 包。但纤维不能解决我面临的问题。甚至费伯斯的作者提供的例子也说明了这一缺陷:

...
Fiber(function() {
console.log('wait... ' + new Date);
sleep(1000);
console.log('ok... ' + new Date);
}).run();
console.log('back in main');

实际产出:

wait... Fri Jan 21 2011 22:42:04 GMT+0900 (JST)
back in main
ok... Fri Jan 21 2011 22:42:05 GMT+0900 (JST)

如果 function Fiber 真的将异步函数睡眠转换为同步,那么输出应该是:

wait... Fri Jan 21 2011 22:42:04 GMT+0900 (JST)
ok... Fri Jan 21 2011 22:42:05 GMT+0900 (JST)
back in main

我已经在 JSFiddle中创建了另一个简单的示例,并寻找产生预期输出的代码。我将接受一个只能在 Node.js 中工作的解决方案,因此您可以自由地要求任何 npm 包,尽管不能在 JSFiddle 中工作。

149611 次浏览

你不应该看到发生了什么 在附近的呼叫,创造纤维,而是发生了什么 在里面的纤维。一旦你进入光纤,你就可以同步编程了。例如:

function f1() {
console.log('wait... ' + new Date);
sleep(1000);
console.log('ok... ' + new Date);
}


function f2() {
f1();
f1();
}


Fiber(function() {
f2();
}).run();

在光纤内部,你称之为 f1f2sleep,就好像它们是同步的。

在典型的 Web 应用程序中,您将在 HTTP 请求调度程序中创建光纤。完成这一步之后,即使调用异步函数(fs、数据库等) ,也可以以同步方式编写所有请求处理逻辑。

如果函数纤维真的将异步函数睡眠转换为同步

是的。在光纤内部,函数在记录 ok之前等待。纤程不使异步函数同步,但允许编写使用异步函数的同步代码,然后在 Fiber中异步运行。

有时我发现需要将一个异步函数封装成一个同步函数,以避免大规模的全局重构。

你不能。不可能使异步代码同步。您需要在全局代码中预先考虑到这一点,并从一开始就以异步方式编写它。是否将全局代码包装在光纤中、使用承诺、承诺生成器或简单的回调取决于您的偏好。

我的目标是在将数据采集方法从同步更改为异步时尽量减少对调用方的影响

承诺和纤维都可以做到这一点。

Desync 将异步函数转换为 sync,通过在 JavaScript 层调用 Node.js 事件循环使用阻塞机制实现。因此,desync 只会阻止后续代码运行,而不会阻塞整个线程,也不会导致繁忙的等待。使用这个模块,下面是 jsFiddle 挑战的答案:

function AnticipatedSyncFunction(){
var ret;
setTimeout(function(){
ret = "hello";
},3000);
while(ret === undefined) {
require('deasync').runLoopOnce();
}
return ret;
}




var output = AnticipatedSyncFunction();
//expected: output=hello (after waiting for 3 sec)
console.log("output="+output);
//actual: output=hello (after waiting for 3 sec)

(免责声明: 我是 deasync的合著者。该模块是在发布这个问题后创建的,没有发现任何可行的建议。)

一开始,我在使用 node.js 时遇到了一些问题,而且我发现,在帮助您处理这个问题时,node.js 是最好的库。如果要用节点编写同步代码,方法是这样的。

var async = require('async');


console.log('in main');


doABunchOfThings(function() {
console.log('back in main');
});


function doABunchOfThings(fnCallback) {
async.series([
function(callback) {
console.log('step 1');
callback();
},
function(callback) {
setTimeout(callback, 1000);
},
function(callback) {
console.log('step 2');
callback();
},
function(callback) {
setTimeout(callback, 2000);
},
function(callback) {
console.log('step 3');
callback();
},
], function(err, results) {
console.log('done with things');
fnCallback();
});
}

本节目将永远制作以下..。

in main
step 1
step 2
step 3
done with things
back in main

使 Node.js 代码同步在某些方面(如数据库)是必不可少的。但是 Node.js 的实际优势在于异步代码。因为它是单线程非阻塞。

我们可以使用重要的功能纤维()来同步它 使用 wait ()和 defer () 我们使用 waiting ()调用所有方法。

正常异步代码。这使用 CallBack 函数。

function add (var a, var b, function(err,res){
console.log(res);
});


function sub (var res2, var b, function(err,res1){
console.log(res);
});


function div (var res2, var b, function(err,res3){
console.log(res3);
});

使用纤维()同步上面的代码,等待()和延迟()

fiber(function(){
var obj1 = await(function add(var a, var b,defer()));
var obj2 = await(function sub(var obj1, var b, defer()));
var obj3 = await(function sub(var obj2, var b, defer()));


});

希望这个能帮上忙,谢谢

我找不到一个不能用节点光纤解决的方案。您提供的使用节点纤维的示例的行为与预期的一样。关键是在一根光纤中运行所有相关的代码,这样你就不必在随机的位置启动一根新的光纤。

让我们看一个例子: 假设您使用某个框架,这是您的应用程序的入口点(您不能修改这个框架)。这个框架将 nodejs 模块作为插件加载,并在插件上调用一些方法。假设这个框架只接受同步函数,并且不使用光纤本身。

有一个库你想在你的插件中使用,但是这个库是异步的,你也不想修改它。

当没有纤维运行时,主线程不能产生,但是您仍然可以使用纤维创建插件!只需创建一个包装器条目,在一个光纤中启动整个框架,这样就可以从插件中生成执行。

缺点: 如果框架在内部使用 setTimeoutPromise,那么它将逃脱光纤上下文。这可以通过模拟 setTimeoutPromise.then和所有事件处理程序来解决。

这就是在 Promise解析之前如何生成纤维的方法。这段代码接受一个异步(承诺返回)函数,并在解决承诺问题时恢复光纤:

Framework-entry. js

console.log(require("./my-plugin").run());

Async-lib. js

exports.getValueAsync = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve("Async Value");
}, 100);
});
};

My-plugin. js

const Fiber = require("fibers");


function fiberWaitFor(promiseOrValue) {
var fiber = Fiber.current, error, value;
Promise.resolve(promiseOrValue).then(v => {
error = false;
value = v;
fiber.run();
}, e => {
error = true;
value = e;
fiber.run();
});
Fiber.yield();
if (error) {
throw value;
} else {
return value;
}
}


const asyncLib = require("./async-lib");


exports.run = () => {
return fiberWaitFor(asyncLib.getValueAsync());
};

My-entry. js

require("fibers")(() => {
require("./framework-entry");
}).run();

当你运行 node framework-entry.js时,它会抛出一个错误: Error: yield() called with no fiber running。如果你运行 node my-entry.js,它会像预期的那样工作。

现在,这种生成器模式可以在许多情况下成为一种解决方案。

下面是使用异步 readline.ask 函数在 nodejs 中执行顺序控制台提示的示例:

var main = (function* () {


// just import and initialize 'readline' in nodejs
var r = require('readline')
var rl = r.createInterface({input: process.stdin, output: process.stdout })


// magic here, the callback is the iterator.next
var answerA = yield rl.question('do you want this? ', r=>main.next(r))


// and again, in a sync fashion
var answerB = yield rl.question('are you sure? ', r=>main.next(r))


// readline boilerplate
rl.close()


console.log(answerA, answerB)


})()  // <-- executed: iterator created from generator
main.next()     // kick off the iterator,
// runs until the first 'yield', including rightmost code
// and waits until another main.next() happens

还有一个 npm 同步模块,用于同步执行查询的过程。

当您希望以同步方式运行并行查询时,节点限制这样做,因为它从不等待响应。同步模块非常适合这种解决方案。

样本代码

/*require sync module*/
var Sync = require('sync');
app.get('/',function(req,res,next){
story.find().exec(function(err,data){
var sync_function_data = find_user.sync(null, {name: "sanjeev"});
res.send({story:data,user:sync_function_data});
});
});




/*****sync function defined here *******/
function find_user(req_json, callback) {
process.nextTick(function () {


users.find(req_json,function (err,data)
{
if (!err) {
callback(null, data);
} else {
callback(null, err);
}
});
});
}

参考链接: https://www.npmjs.com/package/sync

你必须使用承诺:

const asyncOperation = () => {
return new Promise((resolve, reject) => {
setTimeout(()=>{resolve("hi")}, 3000)
})
}


const asyncFunction = async () => {
return await asyncOperation();
}


const topDog = () => {
asyncFunction().then((res) => {
console.log(res);
});
}

我更喜欢箭头函数定义。但是形式为“() = > { ... }”的任何字符串也可以写成“ function (){ ... }”

所以 topDog 不是异步的,尽管它调用了一个异步函数。

enter image description here

编辑: 我意识到很多时候你需要在一个同步函数中包装一个异步函数是在一个控制器中。对于这些情况,这里有一个聚会技巧:

const getDemSweetDataz = (req, res) => {
(async () => {
try{
res.status(200).json(
await asyncOperation()
);
}
catch(e){
res.status(500).json(serviceResponse); //or whatever
}
})() //So we defined and immediately called this async function.
}

利用这个回调函数,你可以做一个不使用承诺的回调函数:

const asyncOperation = () => {
return new Promise((resolve, reject) => {
setTimeout(()=>{resolve("hi")}, 3000)
})
}


const asyncFunction = async (callback) => {
let res = await asyncOperation();
callback(res);
}


const topDog = () => {
let callback = (res) => {
console.log(res);
};


(async () => {
await asyncFunction(callback)
})()
}

通过将此技巧应用于 EventEmitter,可以得到相同的结果。在定义回调的位置定义 EventEmitter 的侦听器,并在调用回调的位置发出事件。