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


122

假设您维护一个公开一个函数的库getData。您的用户调用它来获取实际数据:
var output = getData();
数据被保存在文件中,因此您可以getData使用内置的Node.js来实现fs.readFileSync。两者都很明显,getData并且fs.readFileSync都是同步功能。有一天,您被告知将基础数据源切换到一个仓库,例如只能异步访问的MongoDB。还被告知要避免惹恼您的用户,getDataAPI不能更改为仅返回promise或要求回调参数。您如何满足这两个要求?

使用回调/承诺的异步功能是JavasSript和Node.js的DNA。任何不平凡的JS应用程序都可能会渗透这种编码样式。但是这种做法很容易导致所谓的厄运回响金字塔。更糟糕的是,如果调用链中任何调用方中的任何代码都取决于异步函数的结果,则这些代码也必须包装在回调函数中,从而在调用方上施加编码样式约束。我不时发现有必要将异步功能(通常在第三方库中提供)封装到同步功能中,以避免大规模的全局重构。在这个问题上寻找解决方案通常以节点光纤结束或从中派生的npm软件包。但是,光纤无法解决我面临的问题。甚至Fibers作者提供的示例也说明了这一缺陷:

...
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)

如果功能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中可用的解决方案,因此,即使在JSFiddle中不起作用,您也可以自由地使用任何npm软件包。


2
异步函数永远无法在Node中实现同步,即使可以,也不应如此。问题是,在fs模块中,您可以看到用于同步和异步访问文件系统的完全独立的功能。您能做的最好的事情是用promise或协程(ES6中的生成器)掩盖异步的外观。为了管理回调金字塔,请给它们命名而不是在函数调用中定义,并使用异步库之类的东西。
qubyte

8
对于dandavis来说,异步使实现细节陷入了调用链,有时会强制进行全局重构。对于模块化和封闭性很重要的复杂应用程序,这是有害的,甚至是灾难性的。
2014年

4
“厄运金字塔”只是问题的代表。Promise可以隐藏或掩饰它,但不能解决真正的挑战:如果async函数的调用者依赖于async函数的结果,则它必须使用回调,其调用者也要使用回调。这是对约束施加约束的经典示例调用者仅仅是因为实现细节。
2014年

1
@abbr:感谢使用deasync模块,您对问题的描述恰好是我一直在寻找的,并且找不到任何可行的解决方案。我弄乱了生成器和可迭代对象,但得出的结论与您相同。
Kevin Jhangiani

2
值得注意的是,强制异步函数同步几乎不是一个好主意。您几乎总是有一个更好的解决方案,既可以保持函数的异步性不变,又可以达到相同的效果(例如排序,变量设置等)。
马达拉的幽灵2015年

Answers:


104

deasync通过在JavaScript层调用Node.js事件循环,通过阻止机制将异步功能转换为同步功能。结果,不同步仅阻止后续代码运行,而不阻止整个线程,也不保证繁忙的等待。使用此模块,这是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。该模块是在发布此问题后创建的,没有找到可行的建议。)


有人对此感到幸运吗?我不能使它工作。
纽曼

3
我无法使其正常运行。如果您希望更多地使用它,则应该改进此模块的文档。我怀疑作者是否确切知道使用该模块的后果,如果确实如此,他们肯定不会对其进行记录。
亚历山大·米尔斯

5
到目前为止,github问题跟踪器中记录了一个已确认的问题。该问题已在Node v0.12中修复。我所知道的其他内容只是毫无根据的推测,不值得记录。如果您认为您的问题是由异步引起的,请发布一个独立的,可复制的方案,我将进行调查。
简称

我尝试使用它,并且在脚本中得到了一些改进,但是我仍然对日期不满意。我修改了代码,如下所示:function AnticipatedSyncFunction(){ var ret; setTimeout(function(){ var startdate = new Date() //console.log(startdate) ret = "hello" + startdate; },3000); while(ret === undefined) { require('deasync').runLoopOnce(); } return ret; } var output = AnticipatedSyncFunction(); var startdate = new Date() console.log(startdate) console.log("output="+output); 我希望在日期输出中看到3秒的不同!
亚历克斯(Alex)

@abbr可以在没有节点依赖性的情况下进行浏览和使用吗?
Gandhi

5

还有一个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


4

如果功能光纤真正将异步功能睡眠转换为同步

是。在光纤内部,该函数在记录之前等待ok。光纤不会使异步函数同步,而是允许编写使用异步函数的看起来类似同步的代码,然后这些代码将在异步运行Fiber

我不时发现有必要将异步函数封装到同步函数中,以避免大规模的全局重构。

你不能。使异步代码同步是不可能的。您将需要在全局代码中预料到这一点,并从一开始就以异步方式编写它。是否将全局代码包装在光纤中,使用promise,promise生成器还是简单的回调取决于您的首选项。

我的目标是将数据采集方法从同步更改为异步时,对调用者的影响最小化

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


1
这是使用Node.js可以做到的最糟糕的事情:“使用异步函数的异步代码,然后将异步运行。” 如果您的API那样做,您将丧命。如果它是异步的,则应要求回调,如果未提供回调,则抛出错误。除非您的目标是欺骗他人,否则这是创建API的最佳方法。
亚历山大·米尔斯

@AlexMills:是的,确实是可怕的。但是,幸运的是,API不能做任何事情。异步API始终需要接受回调/返回promise /期望在光纤内部运行-没有它就无法工作。在Afaik中,光纤主要用于被阻塞并且没有任何并发​​性的quick'n'dirty脚本中,但是他们想使用异步API。就像在节点中一样,有时您会使用同步fs方法。
Bergi 2015年

2
我通常喜欢节点。特别是如果我可以使用Typescript而不是纯js。但是,当您决定进行单个异步调用时,这种遍及您所做的一切并实际上感染了调用链中每个函数的整个异步废话是我真的……真的很讨厌。异步api就像一种传染病,一次调用会感染整个代码库,迫使您重写所有代码。我真不明白的人怎么可能认为这是一个很好的事情。
克里斯(Kris)'18年

@Kris Node使用异步模型执行IO任务,因为它既快速又简单。您也可以同步执行许多操作,但是阻塞很慢,因为您无法同时执行任何操作-除非您要使用线程,否则会使一切变得复杂。
Bergi '18

@Bergi我读了宣言,所以我知道论点。但是,在您遇到第一个没有同步等效项的api调用时,将现有代码更改为异步并不简单。一切都中断了,必须仔细检查每一行代码。除非您的代码微不足道,否则我保证...将整个过程转换为异步惯用法后,将需要一段时间才能进行转换并使它再次工作。
克里斯(Kris)'18年

2

您必须使用诺言:

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也不是异步的。

在此处输入图片说明

编辑:我意识到很多时候,您需要将一个异步函数包装在一个同步函数中,这是在一个控制器内部。对于那些情况,这是一个聚会的把戏:

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.
}

结合使用回调,您可以进行不使用promise的包装:

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的侦听器,并在调用回调的位置发出事件。


1

我找不到使用节点光纤无法解决的情况。您使用节点光纤提供的示例的行为符合预期。关键是在光纤内运行所有相关代码,因此您不必在随机位置启动新光纤。

让我们看一个例子:假设您使用某个框架,这是您应用程序的切入点(您不能修改此框架)。该框架将nodejs模块作为插件加载,并在插件上调用一些方法。可以说,该框架仅接受同步功能,而不是单独使用光纤。

您想在其中一个插件中使用一个库,但是该库是异步的,您也不想修改它。

没有光纤运行时无法产生主线程,但是您仍然可以使用光纤创建插件!只需创建一个包装程序条目即可在光纤内部启动整个框架,因此您可以从插件中获得执行。

缺点:如果框架在内部使用setTimeoutPromise,则它将逃避光纤上下文。这可以通过嘲讽被合作周围setTimeoutPromise.then以及所有事件处理程序。

因此,这是在a Promise问题解决之前可以产生纤维的方法。此代码采用异步(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它将按预期工作。


0

在某些方面(例如数据库),使Node.js代码同步至关重要。但是Node.js的实际优势在于异步代码。由于它是单线程非阻塞。

我们可以使用重要的功能来同步它Fiber()使用await()和defer()我们使用await()调用所有方法。然后将回调函数替换为defer()。

普通的异步代码,使用回调函数。

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(),await()和defer()同步上述代码

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()));

});

我希望这将有所帮助。谢谢


0

如今,这种生成器模式可以在许多情况下解决。

这是使用异步readline.question函数在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

-1

您不应该查看创建光纤的呼叫周围发生的情况而应该查看光纤内部发生的情况。一旦进入光纤,您就可以同步编程。例如:

函数f1(){
    console.log('wait ...'+ new Date);
    睡眠(1000);
    console.log('ok ...'+ new Date);   
}

函数f2(){
    f1();
    f1();
}

纤维(功能(){
    f2();
})。跑();

纤维内部调用f1f2sleep就好像它们是同步。

在典型的Web应用程序中,您将在HTTP请求分派器中创建Fiber。一旦完成,就可以以同步样式编写所有请求处理逻辑,即使它调用了异步函数(fs,数据库等)也是如此。


谢谢布鲁诺。但是,如果我需要在服务器绑定到tcp端口之前需要执行的引导程序代码中的同步样式(例如必须从异步打开的db中读取的配置或数据),该怎么办?我可能最终将整个server.js包装在Fiber中,并且我怀疑这将在整个过程级别上杀死并发性。但是,这是值得验证的建议。对我而言,理想的解决方案应该能够包装异步函数以提供同步调用语法,并且仅在调用者链中阻塞下一行代码,而不会牺牲进程级的并发性。
2014年

您可以将整个引导程序代码包装在一个大的Fiber调用中。并发应该不是问题,因为引导代码通常需要先运行才能完成,然后再开始提供请求。而且,一根光纤不会阻止其他光纤运行:每次您遇到一个收益调用时,您都会给其他光纤(和主线程)一个运行的机会。
Bruno Jouhier 2014年

我已经用光纤包裹Express引导文件server.js。我正在寻找执行顺序,但是该包装对请求处理程序没有任何影响。因此,我想必须将相同的包装应用于EACH调度程序。我在这一点上放弃了,因为它似乎并没有更好地帮助避免全局重构。我的目标是在DAO层中将数据获取方法从“同步”更改为“异步”时,将对调用者的影响降到最低,而Fiber仍然不足以应对挑战。
2014年

@fred:“同步”事件流(如请求处理程序)没有多大意义-您需要进行while(true) handleNextRequest()循环。将每个请求处理程序包装在光纤中。
Bergi 2014年

@fred:fibre对Express不会有多大帮助,因为Express的回调不是连续回调(该回调总是被调用一次,无论是错误还是结果)。但是,当您在具有连续回调的异步API之上编写大量代码(例如fs,mongodb以及许多其他代码)时,光纤将解决灾难的金字塔。
Bruno Jouhier 2014年

-2

首先,我使用node.js对此感到苦恼,而async.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

2
async在您的示例b / c中有效,它是main,与呼叫者无关。想象一下,所有代码都包装在一个函数中,该函数应该返回异步函数调用之一的结果。通过console.log('return');在代码末尾添加,可以很容易地证明它不起作用。在这种情况下,的输出return将在之后in main之前发生step 1
2014年

-11

Javascript是一种单线程语言,您不想阻塞整个服务器!异步代码通过明确显示依赖关系来消除竞争条件。

学习爱异步代码!

promises不创建回调地狱金字塔的情况下查看异步代码。我建议使用node.jspromiseQ库

httpGet(url.parse("http://example.org/")).then(function (res) {
    console.log(res.statusCode);  // maybe 302
    return httpGet(url.parse(res.headers["location"]));
}).then(function (res) {
    console.log(res.statusCode);  // maybe 200
});

http://howtonode.org/promises

编辑:这是迄今为止我最有争议的答案,节点现在具有yield关键字,它使您可以将异步代码视为同步。http://blog.alexmaccaw.com/how-yield-will-transform-node


1
Promise仅改写了回调参数,而不是将函数转换为同步。
2014年

2
您不希望它被同步,否则您的整个服务器将被阻止!stackoverflow.com/questions/17959663/...
roo2

1
理想的是在不阻止其他事件(例如由Node.js处理另一个请求)的情况下进行同步调用。根据定义,Sync函数仅意味着在产生结果之前(不仅是承诺),它不会返回给调用者。呼叫被阻止时,它不会将服务器不排除处理其他事件。
2014年

@fred:我认为您没有兑现承诺的要点。它们不仅是观察者模式的抽象,而且确实提供了一种链接和组成异步动作的方法。
Bergi 2014年

1
@Bergi,我经常使用promise,并且确切地知道它的作用。有效地实现的所有功能是将单个异步函数调用分解为多个调用/语句。但它不会改变结果-调用者返回时,它无法返回异步函数的结果。看看我在JSFiddle中发布的示例。在这种情况下,调用方是函数AnticipatedSyncFunction,异步函数是setTimeout。如果您可以用诺言回答我的挑战,请告诉我。
简称
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.