同步调用异步 Javascript 函数

IT技术 javascript asynchronous
2021-02-01 15:45:31

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

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

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

6个回答

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

行。但你真的应该以正确的方式去做……或者别的什么

“我需要一个具体的例子来说明如何阻止它......而不冻结 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()通常是整个程序。套用 OP,期望理论编程能够反映现实是徒劳的。
2021-03-13 15:45:31
是的,我知道如何正确地做到这一点,我需要知道如何/是否可以因为所述的具体原因而错误地做到这一点。关键是我不想离开 doSomething() 直到 myAsynchronousCall 完成对回调函数的调用。Bleh,这是不可能的,正如我所怀疑的,我只需要互联网收集的智慧来支持我。谢谢你。:-)
2021-03-14 15:45:31
@RobertC.Barth:是的,不幸的是,您的怀疑是正确的。
2021-04-02 15:45:31
是我还是只有“正确完成”版本有效?该问题包括一个返回调用,在此之前应该有一些东西等待异步调用完成,这个答案的第一部分不包括......
2021-04-05 15:45:31
@Leonardo:这是问题中调用的神秘函数。基本上它代表任何异步运行代码并产生需要接收的结果。所以它可能就像一个 AJAX 请求。您将callback函数传递给函数,该myAsynchronousCall函数执行异步操作并在完成时调用回调。这是一个演示。
2021-04-07 15:45:31

异步函数ES2017 中的一个特性,它通过使用promise(一种特殊形式的异步代码)和await关键字使异步代码看起来同步还要注意在下面的代码示例中,关键字async前面的function关键字表示 async/await 函数。如果await不在关键字为前缀的函数中关键字将无法工作async由于目前没有例外,这意味着没有顶级等待将起作用(顶级等待意味着在任何函数之外等待)。尽管有顶级await.

ES2017 于 2017 年 6 月 27 日被批准(即最终确定)为 JavaScript 的标准。 Async await 可能已经在您的浏览器中工作,但如果没有,您仍然可以使用babeltraceur等 javascript 转译器使用该功能Chrome 55 完全支持异步功能。因此,如果您有较新的浏览器,您可以试试下面的代码。

有关浏览器兼容性,请参阅kangax 的 es2017 兼容性表

这是一个调用的示例异步等待函数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,因此您可以将其用作带有链接的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 调用。你应该小心不要让 promise 错误得不到处理,尤其是在 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 关键字和生成器函数具有更通用的用途,可以做更多的事情,而不是 async await 函数所做的事情。如果你想要一个可用于复制异步等待的生成器函数包装器,我会查看co.js顺便说一下,co 的函数很像 async await 函数返回一个 promise。老实说,虽然在这一点上,生成器函数和异步函数的浏览器兼容性大致相同,所以如果你只想要异步等待功能,你应该使用没有 co.js 的异步函数。 (我建议只使用 async/await,它在支持上述删除线的大多数环境中得到了广泛支持。)

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

我们走了多远:)
2021-03-11 15:45:31
这是一个很好的答案 +1 和所有,但按原样编写,我看不出这比使用回调更简单。
2021-03-12 15:45:31
我喜欢这个答案
2021-03-20 15:45:31
这是一个很好的答案,但对于原始海报问题,我认为它所做的只是将问题上移了一个层次。假设他将 doSomething 转换为内部带有 await 的异步函数。该函数现在返回一个 promise 并且是异步的,因此无论调用该函数,他都必须再次处理相同的问题。
2021-03-23 15:45:31
@dpwrussell 确实如此,代码库中存在大量异步函数和 promise。解决Promise从蔓延到一切的最好方法就是编写同步回调,除非你做一些非常奇怪和有争议的事情,比如这个twitter.com/sebmarkbage/status/941214259505119232我不这样做,否则无法同步返回异步值推荐。我将在问题的末尾添加一个编辑,以更完整地回答问题,而不仅仅是回答标题。
2021-03-26 15:45:31

看看 JQuery Promise:

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


这是代码给人一种同步的错觉,而实际上不是异步的情况吗?
2021-03-14 15:45:31
Promise不同步。
2021-03-16 15:45:31
Promise是 IMO 只是组织良好的回调 :) 如果您需要异步调用让我们说一些对象初始化,那么Promise会有所不同。
2021-03-17 15:45:31
+1 对于这个答案,这是正确的。不过,我会更新行dfd.notify(data)dfd.resolve(data)
2021-03-26 15:45:31

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

不过,它肯定会冻结您的用户界面,所以当谈到是否可以采取您需要采取的快捷方式时,我仍然持反对意见。在 JavaScript 中挂起唯一线程是不可能的,即使 NodeJS 有时允许您阻止它。在您的Promise解决之前,任何回调,事件,任何异步都无法处理。因此,除非您的读者有像 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,因此函数和不可枚举的属性(如原型链)将丢失。
这个答案直接解决了问题的核心。我也许可以将其应用于我的特定案例。
2021-03-21 15:45:31

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