web-dev-qa-db-ja.com

Node.jsまたはJavascriptで非同期関数呼び出しを同期関数にラップする方法は?

関数getDataを公開するライブラリを維持するとします。ユーザーはそれを呼び出して実際のデータを取得します。
var output = getData();
ボンネットデータはファイルに保存されるため、Node.jsの組み込みfs.readFileSyncを使用してgetDataを実装しました。 getDatafs.readFileSyncの両方が同期関数であることは明らかです。ある日、基になるデータソースを、非同期的にのみアクセスできるMongoDBなどのリポジトリに切り替えるように言われました。また、ユーザーを怒らせないように言われました。getData APIを変更して、単にpromiseを返すか、コールバックパラメーターを要求することはできません。両方の要件をどのように満たしていますか?

コールバック/約束を使用した非同期関数は、JavasSriptとNode.jsのDNAです。自明ではないJSアプリには、おそらくこのコーディングスタイルが浸透しています。しかし、このプラクティスは、いわゆる「Doomのコールバックピラミッド」に簡単につながる可能性があります。さらに悪いことに、コールチェーン内の呼び出し元のコードが非同期関数の結果に依存する場合、それらのコードもコールバック関数でラップする必要があり、呼び出し側にコーディングスタイルの制約を課します。大規模なグローバルリファクタリングを回避するために、非同期関数(サードパーティライブラリで提供されることが多い)を同期関数にカプセル化する必要があることが時々あります。このテーマのソリューションを検索すると、通常は Node Fibers またはそれから派生した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パッケージを自由に要求できます。

109
abbr

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の共著者です。この質問を投稿した後にモジュールが作成されましたが、実行可能な提案は見つかりませんでした。)

98
abbr

Npm syncモジュールもあります。クエリを実行するプロセスを同期するために使用されます。

同期クエリで並列クエリを実行する場合、ノードは応答を待機しないため、それを実行するように制限します。同期モジュールは、この種のソリューションに最適です。

サンプルコード

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

5
sanjeev kumar

関数Fiberが非同期関数のスリープを本当に同期化する場合

はい。ファイバー内部では、関数はokを記録する前に待機します。ファイバーは非同期関数を同期化しませんが、非同期関数を使用し、Fiber内で非同期に実行する同期的なコードを作成できます。

大規模なグローバルリファクタリングを回避するために、非同期関数を同期関数にカプセル化する必要があることが時々あります。

それはいけません。非同期コードを同期化することは不可能です。グローバルコードでそれを予測し、最初から非同期スタイルで記述する必要があります。グローバルコードをファイバーでラップするか、promise、promiseジェネレーター、または単純なコールバックを使用するかは、好みによって異なります。

私の目的は、データ取得方法が同期から非同期に変更されたときに、発信者への影響を最小限にすることです

約束と繊維の両方がそれを行うことができます。

4
Bergi

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は非同期ではありません。

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のリスナーを定義し、コールバックを呼び出した場所でイベントを発行します。

1
user2485309

Node.jsコードを同期させることは、データベースなどのいくつかの面で不可欠です。ただし、Node.jsの実際の利点は非同期コードにあります。シングルスレッドノンブロッキングであるため。

重要な機能を使用して同期できますFiber()await()とdefer()を使用しますawait()を使用してすべてのメソッドを呼び出します。次に、コールバック関数をdefer()に置き換えます。

通常の非同期コード。これは、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()、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()));

});

これが役立つことを願っています。ありがとうございました

1

ノードファイバーを使用して解決できないシナリオは見つかりません。ノードファイバーを使用して提供した例は、期待どおりに動作します。重要なのは、ファイバー内で関連するすべてのコードを実行することです。そのため、新しいファイバーをランダムな位置で開始する必要はありません。

例を見てみましょう:アプリケーションのエントリポイントであるフレームワークを使用するとします(このフレームワークは変更できません)。このフレームワークは、nodejsモジュールをプラグインとしてロードし、プラグインのいくつかのメソッドを呼び出します。このフレームワークは同期関数のみを受け入れ、ファイバー自体を使用しないとしましょう。

プラグインの1つで使用するライブラリがありますが、このライブラリは非同期であり、変更する必要もありません。

ファイバーが実行されていない場合、メインスレッドを生成することはできませんが、ファイバーを使用してプラグインを作成できます。フレームワーク全体をファイバー内で開始するラッパーエントリを作成するだけで、プラグインから実行を譲ることができます。

欠点:フレームワークがsetTimeoutまたはPromisesを内部で使用する場合、ファイバーコンテキストをエスケープします。これは、setTimeoutPromise.then、およびすべてのイベントハンドラーをモックすることで回避できます。

これが、Promiseが解決されるまでファイバーを生成する方法です。このコードは、非同期(Promise Return)関数を取り、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
Tamas Hegedus

何が起こるかを見てはいけませんaroundファイバーを作成する呼び出しではなく、何が起こるかinsideファイバー。ファイバ内に入ると、同期スタイルでプログラミングできます。例えば:

 function f1(){
 console.log( 'wait ...' + new Date); 
 sleep(1000); 
 console.log( ' ok ... '+新しい日付); 
} 
 
 function f2(){
 f1(); 
 f1(); 
} 
 
 Fiber(function(){
 f2(); 
})。run(); 

ファイバー内で、f1f2sleepを、それらが同期しているように呼び出します。

典型的なWebアプリケーションでは、HTTPリクエストディスパッチャーでファイバーを作成します。それが完了したら、非同期関数(fs、データベースなど)を呼び出す場合でも、すべての要求処理ロジックを同期スタイルで記述できます。

0
Bruno Jouhier