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

IT技术 javascript node.js asynchronous synchronous node-fibers
2021-01-23 23:33:20

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

使用回调/Promise的异步函数是 JavasSript 和 Node.js 的 DNA。任何非平凡的 JS 应用程序都可能渗透这种编码风格。但这种做法很容易导致所谓的厄运回调金字塔。更糟糕的是,如果调用链中任何调用方的任何代码都依赖于异步函数的结果,那么这些代码也必须包装在回调函数中,从而对调用方施加编码风格约束。有时我发现需要将异步函数(通常在 3rd 方库中提供)封装到同步函数中,以避免大规模的全局重构。寻找有关此主题的解决方案通常以Node Fibers告终或从它派生的 npm 包。但是 Fibers 无法解决我面临的问题。即使是 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 真的将异步函数 sleep 变为同步,则输出应该是:

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 中工作。

6个回答

deasync将异步函数变为同步函数,通过在 JavaScript 层调用 Node.js 事件循环来实现阻塞机制。因此,deasync 只会阻止后续代码运行,而不会阻止整个线程,也不会导致忙等待。有了这个module,这里是 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。该module是在发布此问题后创建的,但没有找到可行的建议。)

我无法让它正常工作。如果您希望更多地使用它,您应该改进此module的文档。我怀疑作者是否确切知道使用该module的后果是什么,如果他们知道,他们当然不会记录它们。
2021-03-21 23:33:20
我尝试使用它并在我的脚本中得到了一些改进,但我仍然没有找到日期。我修改了代码如下: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 秒的不同!
2021-03-30 23:33:20
到目前为止,github 问题跟踪器中记录了一个已确认的问题。该问题已在 Node v0.12 中修复。我所知道的其余只是毫无根据的推测,不值得记录。如果您认为您的问题是由不同步引起的,请发布一个独立的、可复制的场景,我会调查一下。
2021-04-03 23:33:20
@abbr 可以在没有节点依赖的情况下浏览和使用>
2021-04-08 23:33:20
其他人有这方面的运气吗?我不能让它工作。
2021-04-10 23:33:20

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

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

示例代码

/*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

你必须使用Promise:

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

这最终仍会运行回调。我在一个巨大的同步框架中。我需要打一个电话到aws sts get-caller-identity. 我想使用 aws-sdk 框架来做到这一点......我如何编写一个封装所有异步内容的函数?对于我的生活,我无法弄清楚。:)
2021-04-11 23:33:20

如果函数 Fiber 真的将异步函数 sleep 变成了同步

是的。在光纤内部,函数在记录之前等待okFibers 不会使异步函数同步,但允许编写使用异步函数的同步代码,然后将在Fiber.

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

你不能。使异步代码同步是不可能的。您需要在全局代码中预测到这一点,并从一开始就以异步风格编写它。是否将全局代码封装在一个 Fiber 中,使用 promise、promise 生成器还是简单的回调取决于您的偏好。

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

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

@Bergi 我阅读了宣言,所以我知道论点。但是,在您遇到第一个没有同步等效项的 api 调用时,将现有代码更改为异步并不简单。一切都坏了,每一行代码都必须仔细检查。除非你的代码是微不足道的,否则我保证......在将整个事情转换为异步习语后,转换并让它再次工作需要一段时间。
2021-03-17 23:33:20
@Kris Node 对 IO 任务使用异步模型,因为它快速而简单。您也可以同步做很多事情,但是阻塞很慢,因为您不能同时做任何事情 - 除非您使用线程,这会使一切变得复杂。
2021-03-22 23:33:20
我一般喜欢节点。特别是如果我可以使用typescript而不是纯 js。但是,一旦您决定进行单个异步调用,整个异步废话就会渗透到您所做的一切并从字面上感染调用链中的每个函数,这是我真的......真的很讨厌。异步 API 就像一种传染病,一次调用会感染您的整个代码库,迫使您重写所有代码。我真不明白的人怎么可能认为这是一个很好的事情。
2021-04-03 23:33:20
这是 Node.js 所能做的最糟糕的事情:“使用异步函数然后异步运行的同步代码。” 如果你的 API 这样做,你会毁了生活。如果它是异步的,它应该需要一个回调,如果没有提供回调,则抛出一个错误。这是创建 API 的最佳方式,除非您的目标是欺骗他人。
2021-04-05 23:33:20
@AlexMills:是的,确实很可怕然而,幸运的是,这不是 API 可以做的。异步 API 总是需要接受回调/返回Promise/期望在光纤内运行 - 没有它就无法工作。Afaik,纤维主要用于阻塞且没有任何并发​​性但想要使用异步 API 的 quick'n'dirty 脚本;就像在节点中一样,有时您会使用同步fs方法。
2021-04-10 23:33:20

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

这是 nodejs 中使用 async readline.question 函数的顺序控制台提示示例:

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