同步调用异步Javascript函数


221

首先,这是一个非常特殊的情况,它以错误的方式故意将异步调用改型为一个非常同步的代码库,该代码库长成千上万行,并且当前时间不具备进行更改的能力。对的。” 它伤害了我的每一个生命,但是现实和理想常常并没有融合。我知道这很糟糕。

好的,顺便说一句,我该如何做,这样我可以:

function doSomething() {

  var data;

  function callBack(d) {
    data = d;
  }

  myAsynchronousCall(param1, callBack);

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

这些示例(或缺少示例)都使用库和/或编译器,这两种库均不适用于该解决方案。我需要一个如何使其冻结的具体示例(例如,在调用回调之前不要离开doSomething函数)而无需冻结UI。如果在JS中有这种可能。


16
根本不可能使浏览器阻塞并等待。他们只是不会做。
Pointy '02

2
在大多数浏览器上具有阻止机制的javascript角色...您将要创建一个回调,当异步调用完成以返回数据时将调用该回调
Nadir Muzaffar

8
您正在寻找一种告诉浏览器的方法:“我知道我只是告诉您异步运行该先前的功能,但是我并不是真的意思!”。您为什么甚至期望有这种可能?
韦恩'02

2
感谢Dan的编辑。我并不是严格的无礼,但是您的措辞更好。
罗伯特·巴特

2
@ RobertC.Barth现在也可以使用JavaScript。标准中尚未批准异步等待功能,但计划在ES2017中使用。请参阅下面的详细信息。
约翰·约翰(John John)

Answers:


135

“不要告诉我应该怎么做”正确的方式”或其他任何方式”

好。但您确实应该以正确的方式来做...

“我需要一个具体的示例来说明如何使其阻塞...而无需冻结UI。如果在JS中可能发生这种情况。”

不,如果不阻止UI,就无法阻止正在运行的JavaScript。

由于缺乏信息,很难提供解决方案,但是一个选择可能是让调用函数进行一些轮询以检查全局变量,然后将回调设置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 2015年

@ravemir:答案表明不可能做他想做的事。这是需要理解的重要部分。换句话说,在不阻塞UI的情况下,您无法进行异步调用并返回值。因此,第一个解决方案是使用全局变量并轮询以查看该变量是否已被修改的丑陋方法。第二个版本是正确的方法。

1
@Leonardo:这是问题中被调用的神秘函数。基本上,它表示异步运行代码并产生需要接收的结果的任何内容。因此,它可能像一个AJAX请求。您将该callback函数传递给该myAsynchronousCall函数,该函数执行其异步工作并在完成时调用回调。这是一个演示。

60

异步函数ES2017中的一项功能)通过使用promise(一种特殊形式的异步代码)和await关键字使异步代码看起来同步。还要注意在代码示例下面的关键字async前面的function关键字之前,该关键字表示async / await函数。如果await没有关键字预先固定的功能,该关键字将无法工作async。由于当前没有例外,这意味着没有顶级等待将起作用(顶级等待意味着任何功能之外的等待)。尽管有关于高层await建议

ES2017已于2017年6月27日批准(即最终确定)为JavaScript的标准。异步等待可能已在您的浏览器中运行,但是如果没有,您仍然可以使用像babeltraceur这样的javascript编译器来使用该功能。Chrome 55完全支持异步功能。因此,如果您使用的是更新的浏览器,则可以尝试以下代码。

请参阅kangax的es2017兼容性表以了解浏览器兼容性。

这是一个示例async await函数doAsync,该函数需要三秒钟的暂停,并在每次暂停后从开始时间打印时差:

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关键字将暂停函数调用的执行,但不会暂停任何其他函数,并且它将继续执行其他代码,直到承诺解决为止。承诺解决后,它将解包承诺的值,您可以认为等待和承诺表达式现在已被该未包装的值替换。

因此,由于await只是暂停等待,然后在执行其余的行之前取消包装值,因此您可以在循环和内部函数调用中使用它,如以下示例所示,该示例收集数组中等待的时间差并打印出数组。

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

异步函数本身会返回一个Promise,因此您可以像在上面或另一个异步await函数中所做的那样,将它用作与链接的Promise。

如果您想同时发送请求,可以使用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,然后将错误传播到async / await函数catch调用。您应该注意不要遗漏诺言错误,尤其是在Node.js中。以下是一些示例,展示了错误的工作原理。

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)可以使用的替代方法是使用包装生成器函数的特殊函数。生成器函数具有yield关键字,该关键字可用于与周围函数复制await关键字。yield关键字和generator函数具有更多通用性,并且可以完成许多功能,而不仅仅是async await函数。如果你想,可以用来复制异步发电机的功能封装等待我想看看co.js。顺便说一下,co的函数很像异步await函数返回promise。老实说,尽管此时浏览器对生成器功能和异步功能的兼容性几乎相同,所以如果您只想要异步等待功能,则应使用不带co.js的异步功能。

现在,除IE之外,当前所有主流浏览器(Chrome,Safari和Edge)中的异步功能(截至2017年)实际上对浏览器的支持都不错。


2
我喜欢这个答案
ycomp

1
我们走了多远:)
德里克(Derek)

3
这是一个很好的答案,但是对于原始的海报问题,我认为所做的只是将问题上移了一层。说他把doSomething变成了一个内部等待的异步函数。该函数现在返回一个Promise,并且是异步的,因此无论调用该函数如何,他都必须重新处理相同的问题。
dpwrussell

1
@dpwrussell这是真的,在代码库中有大量的异步函数和promise。解决从爬行到一切的承诺的最好方法就是编写同步回调,除非您执行类似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();


    函数callBack(data){
       dfd.notify(data);
    }

    //进行异步调用。
    myAsynchronousCall(param1,callBack);

    函数doSomething(data){
     //处理数据...
    }

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



3
为此答案+1,这是正确的。但是,我将其更新dfd.notify(data)dfd.resolve(data)
Jason

7
代码的这种情况是否给人以同步的错觉,而实际上却不是异步的?
saurshaz

2
Promise是IMO井井有条的回调:)如果您需要异步调用(例如,一些对象初始化),则Promise会有所不同。
webduvet 2014年

10
承诺不同步。
Vans S

6

http://taskjs.org/有一个不错的解决方法

它使用了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中挂起“唯一线程”。在您的诺言得到解决之前,没有回调,事件或任何异步的东西都将无法处理。因此,除非您有像OP这样不可避免的情况(或者就我而言,这是在编写没有回调,事件等的美化的shell脚本),否则请不要这样做!

但是,这是您可以执行的操作:

./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向Service Worker发送同步XHR,而当Service Worker执行异步操作时,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没有定义(甚至没有使用)。
哈维
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.