139

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

使用回调/承诺的异步函数是 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 的解决方案,因此尽管不在 JSFiddle 中工作,您仍可以自由地要求任何 npm 包。

4

10 回答 10

116

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。该模块是在发布此问题后创建的,但没有找到可行的建议。)

于 2014-03-11T19:08:28.770 回答
6

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

于 2016-12-19T06:57:30.317 回答
4

如果功能 Fiber 真的将异步功能睡眠变为同步

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

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

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

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

Promise 和 Fiber 都可以做到这一点。

于 2014-02-17T19:07:31.137 回答
4

你必须使用承诺:

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

将其与回调一起使用,您可以进行不使用承诺的包装:

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

于 2018-05-07T09:51:15.840 回答
2

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

这是 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
于 2016-08-17T20:36:13.560 回答
1

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

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

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

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

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

所以这就是你如何在 aPromise得到解决之前产生一个纤维。此代码采用 async(Promise 返回)函数并在 promise 解决后恢复光纤:

框架入口.js

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

异步 lib​​.js

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

我的插件.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());
};

我的入口.js

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

运行node framework-entry.js时会报错:Error: yield() called with no fiber running. 如果你运行node my-entry.js它会按预期工作。

于 2016-07-14T14:26:15.880 回答
-1

您不应该关注创建光纤的调用周围发生了什么,而应该关注光纤内部发生的事情。一旦进入光纤内部,您就可以以同步方式进行编程。例如:

函数 f1() {
    console.log('等待...' + 新日期);
    睡眠(1000);
    console.log('ok...' + 新日期);   
}

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

纤维(函数(){
    f2();
})。跑();

在您调用的光纤内部f1,它们好像是同步的。f2sleep

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

于 2014-02-17T07:33:29.910 回答
-1

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

});

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

于 2015-07-14T15:51:59.103 回答
-2

起初我在使用 node.js 时遇到了这个问题,而 async.js 是我发现的最好的库来帮助你处理这个问题。如果你想用 node 编写同步代码,方法就是这样。

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
于 2014-05-01T13:59:31.383 回答
-12

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

学会爱上异步代码!

promises不创建回调地狱金字塔的情况下查看异步代码。我推荐node.js 的 promiseQ 库

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

于 2014-02-17T02:50:10.890 回答