非同期のJavaScript関数を同期的に呼び出す


222

まず、これは、何千行もの長い同期コードベースに非同期呼び出しを改造するために意図的に間違った方法で行う非常に特殊なケースであり、現在のところ、「行う」変更を行う能力がありませんそれは正しい。」それは私の存在のすべての繊維を傷つけますが、現実と理想はしばしば一致しません。これは最悪だ。

さて、それが邪魔にならないように、私ができるようにするにはどうすればよいですか?

function doSomething() {

  var data;

  function callBack(d) {
    data = d;
  }

  myAsynchronousCall(param1, callBack);

  // block here and return data when the callback is finished
  return data;
}

例(またはその欠如)はすべてライブラリおよび/またはコンパイラを使用していますが、どちらもこのソリューションでは実行できません。UIをフリーズせずにブロックする方法の具体的な例が必要です(たとえば、コールバックが呼び出されるまでdoSomething関数を終了しないでください)。JSでそんなことが可能なら。


16
ブラウザをブロックして待機することは不可能です。彼らはそれをしません。
先のとがった

2
ほとんどのブラウザーでブロックメカニズムを備えたjavascript dosent ...非同期呼び出しが終了してデータが返されたときに呼び出されるコールバックを作成する必要があります
Nadir Muzaffar

8
「以前の関数を非同期で実行するように言ったのはわかっていますが、私はそれを本当に意味していませんでした!」とブラウザに伝える方法を求めています。なぜそれが可能になると予想するのですか?
ウェイン、

2
編集してくれたDanに感謝します。私は厳密に失礼ではありませんでしたが、あなたの言い回しはましです。
ロバートC.バース

2
@ RobertC.Barth JavaScriptでも可能になりました。async await機能はまだ標準で承認されていませんが、ES2017で予定されています。詳細については、以下の私の回答を参照してください。
John

回答:


135

「どうすれば「正しい方法」で何をすべきかについて教えてはいけない」

OK。しかし、あなたは本当にそれを正しい方法で行うべきです...または何でも

「UIをフリーズせずにブロックする方法の具体例が必要です。JSでそのようなことが可能かどうか。」

いいえ、UIをブロックせずに実行中のJavaScriptをブロックすることはできません。

情報が不足していることを考えると、解決策を提供するのは難しいですが、1つのオプションは、呼び出し関数にグローバル変数をチェックするためのポーリングを実行させ、コールバックdataをグローバルに設定することです。

function doSomething() {

      // callback sets the received data to a global var
  function callBack(d) {
      window.data = d;
  }
      // start the async
  myAsynchronousCall(param1, callBack);

}

  // start the function
doSomething();

  // make sure the global is clear
window.data = null

  // start polling at an interval until the data is found at the global
var intvl = setInterval(function() {
    if (window.data) { 
        clearInterval(intvl);
        console.log(data);
    }
}, 100);

これはすべて、を変更できることを前提としていますdoSomething()。カードに記載されているかどうかはわかりません。

変更できる場合は、なぜコールバックを渡しdoSomething()て他のコールバックから呼び出さないのかわかりませんが、問題が発生する前に停止することをお勧めします。;)


ああ、一体何ですか。正しく実行できることを示唆する例を挙げたので、その解決策を紹介します...

function doSomething( func ) {

  function callBack(d) {
    func( d );
  }

  myAsynchronousCall(param1, callBack);

}

doSomething(function(data) {
    console.log(data);
});

例には非同期呼び出しに渡されるコールバックが含まれているため、正しい方法はdoSomething()、コールバックから呼び出される関数を渡すことです。

もちろん、それだけがコールバックで実行されている場合は、func直接渡すだけです...

myAsynchronousCall(param1, func);

22
ええ、私はそれを正しく行う方法を知っています。述べられた特定の理由でそれが正しく行われない方法/方法を知る必要があります。重要なのは、myAsynchronousCallがコールバック関数の呼び出しを完了するまでdoSomething()を離れたくないということです。ブレー、それはできません、私が思ったように、私は私をバックアップするためにインターネットの収集された知恵が必要でした。ありがとうございました。:-)
ロバートC.バース、

2
@ RobertC.Barth:ええ、あなたの疑惑は残念ながら正しかったです。

それは私ですか、それとも「正しく行われた」バージョンだけが機能しますか?質問にはリターンコールが含まれていましたが、その前に非同期コールが終了するのを待つ何かが必要です。この回答のこの最初の部分はカバーしていません...
ravemir

@ravemir:答えは、彼が望むことをすることは不可能であると述べています。それは理解すべき重要な部分です。つまり、UIをブロックせずに非同期呼び出しを行って値を返すことはできません。したがって、最初の解決策は、グローバル変数を使用してその変数が変更されているかどうかを確認するためにポーリングする醜いハックです。2番目のバージョンは正しい方法です。

1
@レオナルド:それは問題で呼び出されている謎の関数です。基本的に、非同期でコードを実行し、受信する必要のある結果を生成するあらゆるものを表します。つまり、AJAXリクエストのようになる可能性があります。callback関数に関数を渡します。関数myAsynchronousCallは非同期のものを実行し、完了時にコールバックを呼び出します。こちらがデモです。

60

非同期機能、機能 ES2017で、使用してコードを見て同期非同期メイク約束(非同期コードの特定の形態)とawaitキーワードを。また、以下のコード例では、async / await関数を表すキーワードのasync前のキーワードに注意してくださいfunctionawaitキーワードは、キーワードが前に付けられた関数内になければ機能しませんasync。現在、これに例外はありません。つまり、トップレベルの待機は機能しません(トップレベルの待機は、関数の外での待機を意味します)。トップレベルの提案awaitがありますが。

ES2017は、2017年6月27日にJavaScriptの標準として承認(つまり、最終決定)されました。非同期待機はブラウザーで既に機能している可能性がありますが、そうでない場合でも、babeltraceurなどのJavaScriptトランスパイラーを使用して機能を使用できます。Chrome 55は非同期機能を完全にサポートしています。したがって、新しいブラウザを使用している場合は、以下のコードを試すことができる場合があります。

ブラウザーの互換については、kangaxのes2017互換性表を参照してください。

以下は、非同期await関数の例です。 doAsyncこれは、3つの1秒のポーズを取り、開始時間からの各ポーズの後の時間差を出力します。

function timeoutPromise (time) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(Date.now());
    }, time)
  })
}

function doSomethingAsync () {
  return timeoutPromise(1000);
}

async function doAsync () {
  var start = Date.now(), time;
  console.log(0);
  time = await doSomethingAsync();
  console.log(time - start);
  time = await doSomethingAsync();
  console.log(time - start);
  time = await doSomethingAsync();
  console.log(time - start);
}

doAsync();

awaitキーワードがpromise値(この場合、promise値は関数doSomethingAsyncによって返される値です)の前に配置されると、awaitキーワードは関数呼び出しの実行を一時停止しますが、他の関数は一時停止せず、続行しますpromiseが解決するまで他のコードを実行します。promiseが解決されると、promiseの値がラップ解除されます。awaitおよびpromise式は、ラップ解除された値に置き換えられていると考えることができます。

したがって、awaitは一時停止を待ってから値をアンラップしてから残りの行を実行するので、配列で待機している時間差を収集して配列を出力する次の例のように、forループや内部関数呼び出しで使用できます。

function timeoutPromise (time) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(Date.now());
    }, time)
  })
}

function doSomethingAsync () {
  return timeoutPromise(1000);
}

// this calls each promise returning function one after the other
async function doAsync () {
  var response = [];
  var start = Date.now();
  // each index is a promise returning function
  var promiseFuncs= [doSomethingAsync, doSomethingAsync, doSomethingAsync];
  for(var i = 0; i < promiseFuncs.length; ++i) {
    var promiseFunc = promiseFuncs[i];
    response.push(await promiseFunc() - start);
    console.log(response);
  }
  // do something with response which is an array of values that were from resolved promises.
  return response
}

doAsync().then(function (response) {
  console.log(response)
})

非同期関数自体がプロミスを返すので、上記のように、または別の非同期await関数内でチェーンのプロミスとして使用できます。

上記の関数は、Promise.allを使用してリクエストを同時に送信したい場合、別のリクエストを送信する前に各応答を待機します。

// no change
function timeoutPromise (time) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(Date.now());
    }, time)
  })
}

// no change
function doSomethingAsync () {
  return timeoutPromise(1000);
}

// this function calls the async promise returning functions all at around the same time
async function doAsync () {
  var start = Date.now();
  // we are now using promise all to await all promises to settle
  var responses = await Promise.all([doSomethingAsync(), doSomethingAsync(), doSomethingAsync()]);
  return responses.map(x=>x-start);
}

// no change
doAsync().then(function (response) {
  console.log(response)
})

promiseが拒否する可能性がある場合は、try catchでラップするか、try catchをスキップして、非同期/待機関数のcatch呼び出しにエラーを伝播させることができます。特にNode.jsでは、promiseエラーを未処理のままにしないように注意する必要があります。以下は、エラーがどのように機能するかを示すいくつかの例です。

function timeoutReject (time) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      reject(new Error("OOPS well you got an error at TIMESTAMP: " + Date.now()));
    }, time)
  })
}

function doErrorAsync () {
  return timeoutReject(1000);
}

var log = (...args)=>console.log(...args);
var logErr = (...args)=>console.error(...args);

async function unpropogatedError () {
  // promise is not awaited or returned so it does not propogate the error
  doErrorAsync();
  return "finished unpropogatedError successfully";
}

unpropogatedError().then(log).catch(logErr)

async function handledError () {
  var start = Date.now();
  try {
    console.log((await doErrorAsync()) - start);
    console.log("past error");
  } catch (e) {
    console.log("in catch we handled the error");
  }
  
  return "finished handledError successfully";
}

handledError().then(log).catch(logErr)

// example of how error propogates to chained catch method
async function propogatedError () {
  var start = Date.now();
  var time = await doErrorAsync() - start;
  console.log(time - start);
  return "finished propogatedError successfully";
}

// this is what prints propogatedError's error.
propogatedError().then(log).catch(logErr)

ここに行くと、ECMAScriptの今後のバージョンの完成した提案を見ることができます。

ES2015(ES6)だけで使用できるこの代替手段は、ジェネレーター関数をラップする特別な関数を使用することです。ジェネレータ関数には、周囲の関数でawaitキーワードを複製するために使用できるyieldキーワードがあります。yieldキーワードとジェネレーター関数はより一般的な目的であり、非同期await関数が実行することよりも多くのことを実行できます。非同期を複製するために使用できるジェネレーター関数ラッパーが必要な場合は、co.jsをチェックしてください。。ちなみに、coの関数は非同期のawait関数とほぼ同じで、promiseを返します。正直なところ、現時点ではブラウザーの互換性はジェネレーター関数と非同期関数の両方でほぼ同じなので、非同期待機機能だけが必要な場合は、co.jsなしの非同期関数を使用する必要があります。

IEを除くすべての主要な現在のブラウザー(Chrome、Safari、およびEdge)の非同期機能(2017年時点)のブラウザーサポートは、実際にはかなり良好です。


2
私はこの答えが好きです
-ycomp

1
どれだけ進んだか:)
デレク

3
これは素晴らしい答えですが、元のポスターの問題については、問題を1レベル上に移動するだけでよいと思います。彼がdoSomethingをawaitを含む非同期関数に変換するとします。この関数はプロミスを返し、非同期であるため、その関数を呼び出す場合は常に同じ問題に対処する必要があります。
dpwrussell

1
@dpwrussellこれは本当です、コードベースに非同期関数と約束のクリープがあります。侵入からすべてへの約束を解決する最良の方法は、同期コールバックを記述することです。このtwitter.com/sebmarkbage/status/941214259505119232のような非常に奇妙で物議を醸す何かを行わない限り、非同期値を同期的に返す方法はありません。お勧めします。質問の最後に編集を追加して、質問に答えるだけでなく、質問に完全に回答できるようにします。
ジョン

これは素晴らしい答えです+1とすべてですが、現状のままでは、これがコールバックを使用するよりも複雑でないことはわかりません。
Altimus Prime

47

JQuery Promisesを見てください。

http://api.jquery.com/promise/

http://api.jquery.com/jQuery.when/

http://api.jquery.com/deferred.promise/

コードをリファクタリングします。

    var dfd = new jQuery.Deferred();


    function callBack(data){
       dfd.notify(data);
    }

    //非同期呼び出しを行います。
    myAsynchronousCall(param1、callBack);

    function doSomething(data){
     //データを処理します...
    }

    $ .when(dfd).then(doSomething);



3
この回答の+1、これは正しいです。ただし、次の行を更新dfd.notify(data)しますdfd.resolve(data)
Jason

7
これは、実際には非同期ではなく、同期しているように見えるコードの場合ですか?
saurshaz 2013年

2
promiseはIMOであり、適切に構成されたコールバックです。:)たとえば、オブジェクトの初期化で非同期呼び出しが必要な場合、promiseは少し違いがあります。
webduvet 2014

10
プロミスは同期していません。
Vans S

6

http://taskjs.org/に 1つの素晴らしい回避策があります。

それはjavascriptに新しいジェネレーターを使用します。そのため、現在ほとんどのブラウザで実装されていません。私はそれをfirefoxでテストしました、そして私にとってそれは非同期関数をラップする素晴らしい方法です。

プロジェクトGitHubのサンプルコードは次のとおりです

var { Deferred } = task;

spawn(function() {
    out.innerHTML = "reading...\n";
    try {
        var d = yield read("read.html");
        alert(d.responseText.length);
    } catch (e) {
        e.stack.split(/\n/).forEach(function(line) { console.log(line) });
        console.log("");
        out.innerHTML = "error: " + e;
    }

});

function read(url, method) {
    method = method || "GET";
    var xhr = new XMLHttpRequest();
    var deferred = new Deferred();
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            if (xhr.status >= 400) {
                var e = new Error(xhr.statusText);
                e.status = xhr.status;
                deferred.reject(e);
            } else {
                deferred.resolve({
                    responseText: xhr.responseText
                });
            }
        }
    };
    xhr.open(method, url, true);
    xhr.send();
    return deferred.promise;
}

3

NodeJSの非同期JavaScriptを強制的に同期させることができます sync-rpc同期

ただし、UIが確実にフリーズするので、必要なショートカットを取ることができるかどうかに関しては、私はまだ反対者です。NodeJSが時々それをブロックすることを許可したとしても、JavaScriptで1つだけのスレッドを一時停止することはできません。コールバック、イベント、非同期は、約束が解決されるまで処理できません。したがって、読者がOPのような避けられない状況(または、私の場合は、コールバックやイベントなどのない栄光のシェルスクリプトを書いている)でない限り、これを行わないでください!

しかし、これを行う方法は次のとおりです。

./calling-file.js

var createClient = require('sync-rpc');
var mySynchronousCall = createClient(require.resolve('./my-asynchronous-call'), 'init data');

var param1 = 'test data'
var data = mySynchronousCall(param1);
console.log(data); // prints: received "test data" after "init data"

./my-asynchronous-call.js

function init(initData) {
  return function(param1) {
    // Return a promise here and the resulting rpc client will be synchronous
    return Promise.resolve('received "' + param1 + '" after "' + initData + '"');
  };
}
module.exports = init;

制限:

これらはどちらも、sync-rpc悪用による実装方法の結果ですrequire('child_process').spawnSync

  1. これはブラウザでは機能しません。
  2. 関数の引数はシリアライズ可能でなければなりません。引数はとの間でやり取りされるJSON.stringifyため、関数やプロトタイプチェーンなどの列挙できないプロパティは失われます。

1

コールバックに変換することもできます。

function thirdPartyFoo(callback) {    
  callback("Hello World");    
}

function foo() {    
  var fooVariable;

  thirdPartyFoo(function(data) {
    fooVariable = data;
  });

  return fooVariable;
}

var temp = foo();  
console.log(temp);

0

あなたが望むものは実際に今可能です。Service Workerで非同期コードを実行でき、Web Workerで同期コードを実行できる場合、Web Workerに同期XHRをService Workerに送信させることができます。ServiceWorkerが非同期処理を実行している間、Web Workerのスレッドは待機します。これは素晴らしいアプローチではありませんが、うまくいくかもしれません。


-4

要件を少し調整すると、達成したいという考えを実現できます。

ランタイムがES6仕様をサポートしている場合、以下のコードが可能です。

非同期関数の詳細

async function myAsynchronousCall(param1) {
    // logic for myAsynchronous call
    return d;
}

function doSomething() {

  var data = await myAsynchronousCall(param1); //'blocks' here until the async call is finished
  return data;
}

4
Firefoxはエラーを出します:SyntaxError: await is only valid in async functions and async generators。param1が定義されていない(使用されていない)ことは言うまでもありません。
Harvey
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.