首页 文章

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

提问于
浏览
103

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

使用回调/保证的异步函数是JavasSript和Node.js的DNA . 任何非平凡的JS应用程序都可能充满了这种编码风格 . 但这种做法很容易导致所谓的厄运金字塔回调 . 更糟糕的是,如果调用链中任何调用者中的任何代码都依赖于异步函数的结果,那么这些代码也必须包含在回调函数中,对调用者施加编码样式约束 . 我不时发现需要将异步功能(通常在第三方库中提供)封装到同步功能中,以避免大规模的全局重新分解 . 搜索关于此主题的解决方案通常最终得到Node Fibers或从中派生的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)

如果函数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中工作 .

10 回答

  • 4

    我找不到使用节点光纤无法解决的场景 . 您使用节点光纤提供的示例表现如预期 . 关键是在光纤内部运行所有相关代码,因此您无需在随机位置启动新光纤 .

    Lets see an example: 假设您使用了一些框架,这是您的应用程序的入口点(您无法修改此框架) . 这个框架将nodejs模块作为插件加载,并调用插件上的一些方法 . 让我们说这个框架只接受同步函数,并且不单独使用光纤 .

    您希望在其中一个插件中使用一个库,但此库是异步的,您也不想修改它 .

    没有光纤运行时,主线程无法生成,但您仍然可以使用光纤创建插件!只需创建一个包装器条目,在光纤内启动整个框架,这样就可以从插件中获得执行 .

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

    所以这就是在解决 Promise 之前你可以产生光纤的方法 . 此代码采用异步(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

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

  • -2

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

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

  • 94

    您不应该关注创建光纤的呼叫周围会发生什么,而应该关注光纤内部发生的情况 . 进入光纤后,您可以以同步方式进行编程 . 例如:

    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,数据库等) .

  • -1

    你必须使用承诺:

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

    利用回调函数,你可以做一个不使用promises的包装:

    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的侦听器,我在其中定义了回调,并在我调用的地方发出事件回调 .

  • -11

    如今,这种发电机模式在许多情况下都是一种解决方案 .

    这里是使用async 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
    
  • 0

    Javascript是一种单线程语言,您不希望阻止整个服务器!异步代码通过显式依赖来消除竞争条件 .

    Learn to love asynchronous code!

    看看 promises 的异步代码而不创建回调地狱的金字塔 . 我推荐promiseQ library for node.js

    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

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

  • 0

    我首先使用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
    
  • 5

    如果功能光纤确实将异步功能睡眠转为同步

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

    有时我发现需要将异步函数封装到同步函数中,以避免大规模的全局重新分解 .

    你不能 . 异步代码不可能同步 . 您需要在全局代码中预测它,并从头开始以异步方式编写它 . 无论是将全局代码包装在光纤中,使用promises,promise生成器还是简单的回调都取决于您的偏好 .

    我的目标是在数据采集方法从同步更改为异步时最大限度地减少对调用者的影响

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

相关问题