同步调用异步 Javascript 函数

Posted

技术标签:

【中文标题】同步调用异步 Javascript 函数【英文标题】:Call An Asynchronous Javascript Function Synchronously 【发布时间】:2012-02-25 16:08:28 【问题描述】:

首先,这是一个非常具体的案例,故意以错误的方式将异步调用改造成非常同步的代码库,该代码库长达数千行,而且时间目前无法进行更改“做对了”。它伤害了我的每一根纤维,但现实和理想往往无法融合。我知道这很糟糕。

好的,那不碍事了,我该怎么做才能做到:

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 中是可能的。

【问题讨论】:

根本不可能让浏览器阻塞并等待。他们就是不会这样做。 javascript dosent 在大多数浏览器上都有阻塞机制...您需要创建一个回调,当异步调用完成以返回数据时调用该回调 您在寻求一种方法来告诉浏览器“我知道我只是告诉您异步运行之前的函数,但我不是真的故意的!”。为什么你甚至期望这是可能的? 感谢 Dan 的编辑。我并不是严格意义上的粗鲁,但你的措辞更好。 @RobertC.Barth 现在也可以使用 JavaScript。异步等待功能尚未在标准中获得批准,但计划在 ES2017 中。有关更多详细信息,请参阅下面的答案。 【参考方案1】:

“不要告诉我应该如何以“正确的方式”或其他方式去做”

好的。 但你真的应该以正确的方式去做......或其他任何事情

“我需要一个具体的例子来说明如何让它阻塞......而不冻结 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);

【讨论】:

是的,我知道如何正确地做到这一点,我需要知道如何/是否由于所述的具体原因而不能正确地做到这一点。关键是我不想离开 doSomething() 直到 myAsynchronousCall 完成对回调函数的调用。 Bleh,这不可能,我怀疑,我只是需要互联网收集的智慧来支持我。谢谢你。 :-) @RobertC.Barth:是的,很遗憾你的怀疑是正确的。 是我还是只有“正确完成”的版本有效?问题包括一个返回调用,在此之前应该有一些等待异步调用完成的东西,这个答案的第一部分没有涵盖...... @Leonardo:这是问题中调用的神秘函数。基本上它代表任何异步运行代码并产生需要接收的结果的东西。所以它可能就像一个 AJAX 请求。您将callback 函数传递给myAsynchronousCall 函数,该函数执行异步操作并在完成时调用回调。 Here's a demo. 我经常遇到的问题是doSomething() 通常是整个程序。套用 OP 的话,期望理论编程能够反映现实是徒劳的。【参考方案2】:

Async functions,in ES2017 的一项功能,通过使用 promises(一种特殊形式的异步代码)和 await 关键字使异步代码看起来同步。还要注意在下面的代码示例中,function 关键字前面的关键字 async 表示异步/等待函数。如果没有在以 async 关键字为前缀的函数中,await 关键字将不起作用。由于目前没有例外,这意味着没有***等待将起作用(***等待意味着任何函数之外的等待)。虽然有一个proposal for top-level await

ES2017 在 2017 年 6 月 27 日被批准(即最终确定)为 JavaScript 的标准。异步等待可能已经在您的浏览器中运行,但如果不是,您仍然可以使用 JavaScript 转译器(如 babel 或 @987654326)来使用该功能@。 Chrome 55 完全支持异步功能。所以如果你有更新的浏览器,你可以试试下面的代码。

有关浏览器兼容性,请参阅kangax's es2017 compatibility table。

这是一个名为 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 关键字将暂停函数调用的执行,但不会暂停任何其他函数并且它将继续执行其他代码,直到 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)
)

异步函数本身返回一个承诺,因此您可以像我在上面那样或在另一个异步等待函数中将其用作具有链接的承诺。

如果你想同时发送请求,上面的函数会在发送另一个请求之前等待每个响应,你可以使用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)

如果您去here,您可以看到即将推出的 ECMAScript 版本的已完成提案。

可以仅用于 ES2015 (ES6) 的替代方法是使用包装生成器函数的特殊函数。生成器函数有一个 yield 关键字,可以用来复制 await 关键字和周围的函数。 yield 关键字和生成器函数更通用,可以做更多的事情,而不是 async await 函数所做的事情。如果您想要一个可用于复制异步等待的生成器函数包装器,我会查看co.js。顺便说一句,co 的函数很像异步等待函数返回一个承诺。老实说,虽然此时浏览器兼容性对于生成器函数和异步函数大致相同,所以如果你只想要异步等待功能,你应该使用不带 co.js 的异步函数。 (我建议只使用 async/await,它在大多数支持上述删除线的环境中都得到了广泛支持。)

除了 IE 之外,当前所有主流浏览器(Chrome、Safari 和 Edge)中的异步功能(截至 2017 年)的浏览器支持实际上都非常好。

【讨论】:

这是一个很好的答案,但对于原始海报问题,我认为它所做的只是将问题提升了一个级别。假设他将 doSomething 变成了一个带有 await 内部的异步函数。该函数现在返回一个承诺并且是异步的,因此无论调用该函数,他都必须重新处理相同的问题。 @dpwrussell 这是真的,代码库中有大量异步函数和承诺。解决 Promise 蔓延到所有事情的最佳方法就是编写同步回调,除非您执行类似 twitter.com/sebmarkbage/status/941214259505119232 这样非常奇怪和有争议的事情,否则无法同步返回异步值,我不推荐。我将在问题的末尾添加一个编辑,以便更全面地回答问题,而不仅仅是回答标题。 这是一个很好的答案 +1 和所有,但按原样编写,我看不出这比使用回调更简单。 @AltimusPrime 这确实是一个见仁见智的问题,但是错误处理比回调有了很大的改进,你总是可以直接使用 Promise,而无需 async/await,这与回调基本相同,但具有更好的错误处理.当你需要将回调传递给函数以在函数的生命周期内多次执行时,回调会胜过承诺。回调甚至不必是异步的。随着时间的推移,Promise 最适合单个值。如果你真的想了解整个价值观,你应该阅读 kriskowal 的 GTOR。 @AltimusPrime 如果随着时间的推移需要多个值,您可以使用 Streams 和 Async Iterables,您可以将这些与 async/await 函数与 for await 语句一起使用,例如 for await (const item of asyncIterable) itemasyncIterable 是变量,其余的是关键字。相关链接:Kris Kowal's GTOR和asyncIterable proposal repo【参考方案3】:

看看 JQuery Promises:

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

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

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

重构代码:

var dfd = new jQuery.Deferred(); 函数回调(数据) dfd.notify(数据); // 执行异步调用。 myAsynchronousCall(param1, callBack); 功能做某事(数据) // 处理数据... $.when(dfd).then(doSomething);

【讨论】:

这个答案+1,这是正确的。但是,我会将dfd.notify(data) 的行更新为dfd.resolve(data) 这是否是代码给人一种同步错觉的情况,但实际上不是异步的? promises 是 IMO 组织良好的回调 :) 如果你需要一个异步调用,比如说一些对象初始化,那么 promises 会有一点不同。 承诺不同步。【参考方案4】:

可以强制 NodeJS 中的异步 JavaScript 与 sync-rpc 同步。

但它肯定会冻结你的 UI,所以当谈到是否可以走你需要走的捷径时,我仍然反对。不可能在 JavaScript 中暂停 One And Only 线程,即使 NodeJS 有时允许您阻止它。在您的承诺解决之前,任何回调、事件、任何异步操作都无法处理。因此,除非您的读者遇到像 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

    这在浏览器中不起作用。 函数的参数必须是可序列化的。您的参数将传入和传出 JSON.stringify,因此函数和原型链等不可枚举属性将丢失。

【讨论】:

这个答案直接解决了问题的核心。我也许可以将此应用于我的一个特定案例。【参考方案5】:

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;

【讨论】:

【参考方案6】:

你想要的现在实际上是可能的。如果你可以在 service worker 中运行异步代码,在 web worker 中运行同步代码,那么你可以让 web worker 向 service worker 发送同步 XHR,当 service worker 做异步事情时,web worker 的线程将等待。这不是一个很好的方法,但它可以工作。

【讨论】:

这是一个干净的方法,仍然不推荐 coruse :) Backend / node.js 解决方案似乎仍然想要..【参考方案7】:

在 Node.js 中,可以编写实际调用异步操作的同步代码。 node-fibers 允许这样做。它是作为 npm 模块提供的第 3 方本机扩展。 它实现了纤程/协程,因此当特定纤程被阻塞等待异步操作时,整个程序事件循环不会阻塞 - 另一个纤程(如果存在)继续其工作。

使用纤维,您的代码将如下所示:

var Fiber = require('fibers');

function doSomething() 
  var fiber = Fiber.current;

  function callBack(data) 
    fiber.run(data);
  

  myAsynchronousCall(param1, callBack);

  // execution blocks here
  var data = Fiber.yield();
  return data;


// The whole program must be wrapped with Fiber
Fiber(function main() 

  var data = doSomething();
  console.log(data);

).run();

请注意,您应该避免使用它并改用async/await。请参阅下面的项目自述文件https://github.com/laverdet/node-fibers 中的注释:

过时注意事项 -- 该项目的作者建议您尽可能避免使用它。该模块的原始版本在 2011 年初针对 nodejs v0.1.x,当时服务器上的 JavaScript 看起来有很大不同。从那时起,async/await、Promises 和 Generators 被标准化,整个生态系统也朝着这个方向发展。

我会尽可能地继续支持更新版本的 nodejs,但是 v8 和 nodejs 是非常复杂和动态的平台。不可避免地有一天这个库会突然停止工作,没有人能对此做任何事情。

我要感谢所有使用 Fiber 的用户,您多年来的支持对我来说意义重大。

【讨论】:

“node-fibers”的作者建议您尽可能避免使用它 @MuhammadInaamMunir 是的,答案中提到了【参考方案8】:

人们可能不会考虑的一件事:如果您控制异步函数(其他代码段所依赖的),并且它所采用的代码路径不一定是异步的,您可以使其同步(而不破坏其他代码段) 通过创建一个可选参数。

目前:

async function myFunc(args_etcetc) 
    // you wrote this
    return 'stuff';


(async function main() 
    var result = await myFunc('argsetcetc');
    console.log('async result:' result);
)()

考虑:

function myFunc(args_etcetc, opts=) 
    /*
        param opts :: sync:Boolean -- whether to return a Promise or not
    */
    var sync=false = opts;
    if (sync===true)
        return 'stuff';
    else
        return new Promise((RETURN,REJECT)=> 
            RETURN('stuff');
        );



// async code still works just like before:
(async function main() 
    var result = await myFunc('argsetcetc');
    console.log('async result:', result);
)();
// prints: 'stuff'

// new sync code works, if you specify sync mode:
(function main() 
    var result = myFunc('argsetcetc', sync:true);
    console.log('sync result:', result);
)();
// prints: 'stuff'

当然,如果异步函数依赖于固有的异步操作(网络请求等),这将不起作用,在这种情况下,努力是徒劳的(没有有效地等待空闲旋转)。

此外,根据传入的选项返回值或 Promise 是相当难看的。

(“如果它不使用异步构造,我为什么要编写一个异步函数?”有人可能会问?也许函数的某些模式/参数需要异步,而另一些则不需要,并且由于您想要的代码重复一个整体块,而不是不同函数中单独的模块化代码块......例如,参数可能是localDatabase(不需要等待)或remoteDatabase(需要)。然后你可能会出现运行时错误,如果你尝试在远程数据库上执行sync:true。也许这种情况表明存在另一个问题,但你去吧。)

【讨论】:

【参考方案9】:

使用 Node 16 的工作线程实际上使这成为可能,以下示例主线程正在运行异步代码,而工作线程正在同步等待它。

这不是很有用,但它至少模糊了原始问题通过同步等待异步代码提出的问题。

const 
    Worker, isMainThread, parentPort, receiveMessageOnPort
 = require('worker_threads');
if (isMainThread) 
    const worker = new Worker(__filename);
    worker.on('message', async () => 
        worker.postMessage(await doAsyncStuff());
    );
 else 
    console.log(doStuffSync());


function doStuffSync()
    parentPort.postMessage(fn: 'doStuff');
    let message;
    while (!message) 
        message = receiveMessageOnPort(parentPort)
    
    return message;


function doAsyncStuff()
    return new Promise((resolve) => setTimeout(() => resolve("A test"), 1000));

【讨论】:

【参考方案10】:

promise 的这种能力包括如下同步操作的两个关键特性(或者 then() 接受两个回调)。 得到结果后,调用 resolve() 并传递最终结果。 如果出错,调用reject()。

这个想法是通过 .then() 处理程序链传递结果。

const synchronize = (() => 
    let chain = Promise.resolve()
    return async (promise) => 
        return chain = chain.then(promise)
    
)()

【讨论】:

【参考方案11】:
let result;
async_function().then(r => result = r);
while (result === undefined) // Wait result from async_function
    require('deasync').sleep(100);

【讨论】:

您的答案可以通过额外的支持信息得到改进。请edit 添加更多详细信息,例如引用或文档,以便其他人可以确认您的答案是正确的。你可以找到更多关于如何写好答案的信息in the help center。 虽然此代码可能会回答问题,但提供有关它如何和/或为什么解决问题的额外上下文将提高​​答案的长期价值。您可以在帮助中心找到更多关于如何写好答案的信息:***.com/help/how-to-answer。祝你好运?【参考方案12】:

您也可以将其转换为回调。

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


function foo()     
  var fooVariable;

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

  return fooVariable;


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

【讨论】:

好吧,如果 thirdPartyFoo 正在做一些异步操作,那么您将在 temp 中得到 null always【参考方案13】:

如果你稍微调整一下需求,你希望实现的想法就可以实现

如果您的运行时支持 ES6 规范,则可以使用以下代码。

更多关于async functions

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;

【讨论】:

Firefox 给出错误:SyntaxError: await is only valid in async functions and async generators。更不用说 param1 没有定义(甚至没有使用)。

以上是关于同步调用异步 Javascript 函数的主要内容,如果未能解决你的问题,请参考以下文章

Javascript - 异步调用后同步

进阶学习5:JavaScript异步编程——同步模式异步模式调用栈工作线程消息队列事件循环回调函数

JavaScript 异步操作之回调函数

Java异步调用转同步的5种方式

java 异步调用方法

java 异步调用方法