简体   繁体   English

异步调用 Javascript Function 同步

[英]Call An Asynchronous Javascript Function Synchronously

First, this is a very specific case of doing it the wrong way on-purpose to retrofit an asynchronous call into a very synchronous codebase that is many thousands of lines long and time doesn't currently afford the ability to make the changes to "do it right."首先,这是一个非常具体的案例,故意以错误的方式对 retrofit 进行异步调用到非常同步的代码库,该代码库有数千行长并且时间目前无法进行更改以“做对的。” It hurts every fiber of my being, but reality and ideals often do not mesh.它伤害了我的每一根纤维,但现实和理想往往无法融合。 I know this sucks.我知道这很糟糕。

OK, that out of the way, how do I make it so that I could:好的,那不碍事了,我该怎么做才能做到:

function doSomething() {

  var data;

  function callBack(d) {
    data = d;
  }

  myAsynchronousCall(param1, callBack);

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

The examples (or lack thereof) all use libraries and/or compilers, both of which are not viable for this solution.示例(或缺少示例)都使用库和/或编译器,这两者对于此解决方案都不可行。 I need a concrete example of how to make it block (eg NOT leave the doSomething function until the callback is called) WITHOUT freezing the UI.我需要一个具体示例来说明如何阻止它(例如,在调用回调之前不要离开 doSomething function)而不冻结 UI。 If such a thing is possible in JS.如果这样的事情在 JS 中是可能的。

"don't tell me about how I should just do it "the right way" or whatever" “不要告诉我我应该如何以“正确的方式”或其他方式去做”

OK.好的。 but you should really do it the right way... or whatever但你真的应该以正确的方式去做……或者别的什么

" I need a concrete example of how to make it block ... WITHOUT freezing the UI. If such a thing is possible in JS." “我需要一个具体的例子来说明如何阻止它......而不冻结 UI。如果这样的事情在 JS 中是可能的。”

No, it is impossible to block the running JavaScript without blocking the UI.不,不可能在不阻止 UI 的情况下阻止正在运行的 JavaScript。

Given the lack of information, it's tough to offer a solution, but one option may be to have the calling function do some polling to check a global variable, then have the callback set data to the global.鉴于缺乏信息,很难提供解决方案,但一种选择可能是让调用函数进行一些轮询以检查全局变量,然后让回调将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);

All of this assumes that you can modify doSomething() .所有这些都假设您可以修改doSomething() I don't know if that's in the cards.不知道卡里有没有

If it can be modified, then I don't know why you wouldn't just pass a callback to doSomething() to be called from the other callback, but I better stop before I get into trouble.如果它可以修改,那么我不知道你为什么不只是将回调传递给doSomething()以从另一个回调中调用,但我最好在遇到麻烦之前停下来。 ;) ;)


Oh, what the heck.哦,什么鬼。 You gave an example that suggests it can be done correctly, so I'm going to show that solution...你举了一个例子,表明它可以正确完成,所以我将展示该解决方案......

function doSomething( func ) {

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

  myAsynchronousCall(param1, callBack);

}

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

Because your example includes a callback that is passed to the async call, the right way would be to pass a function to doSomething() to be invoked from the callback.由于您的示例包含传递给异步调用的回调,因此正确的方法是将函数传递给doSomething()以从回调中调用。

Of course if that's the only thing the callback is doing, you'd just pass func directly...当然,如果这是回调唯一要做的事情,您只需直接传递func ......

myAsynchronousCall(param1, func);

Async functions , a feature in ES2017 , make async code look sync by using promises (a particular form of async code) and the await keyword. 异步函数ES2017 中的一项功能通过使用promise (一种特殊形式的异步代码)和await关键字使异步代码看起来同步。 Also notice in the code examples below the keyword async in front of the function keyword that signifies an async/await function.还要注意在下面的代码示例中, function关键字前面的关键字async表示 async/await 函数。 The await keyword won't work without being in a function pre-fixed with the async keyword.如果不在以async关键字为async的函数中,则await关键字将无法工作。 Since currently there is no exception to this that means no top level awaits will work (top level awaits meaning an await outside of any function).由于目前没有例外,这意味着没有顶级等待将起作用(顶级等待意味着在任何函数之外等待)。 Though there is a proposal for top-level await .尽管有顶级await建议

ES2017 was ratified (ie finalized) as the standard for JavaScript on June 27th, 2017. Async await may already work in your browser, but if not you can still use the functionality using a javascript transpiler like babel or traceur . ES2017 于 2017 年 6 月 27 日被批准(即最终确定)为 JavaScript 的标准。 Async await 可能已经在您的浏览器中工作,但如果没有,您仍然可以使用babeltraceur等 javascript 转译器使用该功能。 Chrome 55 has full support of async functions. Chrome 55 完全支持异步功能。 So if you have a newer browser you may be able to try out the code below.因此,如果您有较新的浏览器,则可以尝试以下代码。

See kangax's es2017 compatibility table for browser compatibility.有关浏览器兼容性,请参阅kangax 的 es2017 兼容性表

Here's an example async await function called doAsync which takes three one second pauses and prints the time difference after each pause from the start time:这是一个名为doAsync异步等待函数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();

When the await keyword is placed before a promise value (in this case the promise value is the value returned by the function doSomethingAsync) the await keyword will pause execution of the function call, but it won't pause any other functions and it will continue executing other code until the promise resolves.当 await 关键字放在承诺值之前(在这种情况下,承诺值是函数 doSomethingAsync 返回的值),await 关键字将暂停函数调用的执行,但不会暂停任何其他函数,它将继续执行其他代码直到承诺解决。 After the promise resolves it will unwrap the value of the promise and you can think of the await and promise expression as now being replaced by that unwrapped value.在promise解析后,它将解开promise的值,您可以将await和promise表达式视为现在被解包的值替换。

So, since await just pauses waits for then unwraps a value before executing the rest of the line you can use it in for loops and inside function calls like in the below example which collects time differences awaited in an array and prints out the array.因此,由于 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) })

The async function itself returns a promise so you can use that as a promise with chaining like I do above or within another async await function.异步函数本身返回一个承诺,因此您可以将其用作链接的承诺,就像我上面所做的那样,或者在另一个异步等待函数中使用。

The function above would wait for each response before sending another request if you would like to send the requests concurrently you can use Promise.all .上面的函数会在发送另一个请求之前等待每个响应,如果你想同时发送请求,你可以使用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) })

If the promise possibly rejects you can wrap it in a try catch or skip the try catch and let the error propagate to the async/await functions catch call.如果承诺可能拒绝,您可以将其包装在 try catch 中或跳过 try catch 并让错误传播到 async/await 函数 catch 调用。 You should be careful not to leave promise errors unhandled especially in Node.js.你应该小心不要让 promise 错误得不到处理,尤其是在 Node.js 中。 Below are some examples that show off how errors work.下面是一些展示错误如何工作的示例。

 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)

If you go here you can see the finished proposals for upcoming ECMAScript versions.如果你去这里你可以看到即将到来的 ECMAScript 版本的完成提案。

An alternative to this that can be used with just ES2015 (ES6) is to use a special function which wraps a generator function.可以仅与 ES2015 (ES6) 一起使用的替代方法是使用包装生成器函数的特殊函数。 Generator functions have a yield keyword which may be used to replicate the await keyword with a surrounding function.生成器函数有一个 yield 关键字,可用于复制带有周围函数的 await 关键字。 The yield keyword and generator function are a lot more general purpose and can do many more things then just what the async await function does. yield 关键字和生成器函数具有更通用的用途,可以做更多的事情,而不是 async await 函数所做的事情。 If you want a generator function wrapper that can be used to replicate async await I would check out co.js .如果你想要一个可用于复制异步等待的生成器函数包装器,我会查看co.js By the way co's function much like async await functions return a promise.顺便说一下,co 的函数很像 async await 函数返回一个 promise。 Honestly though at this point browser compatibility is about the same for both generator functions and async functions so if you just want the async await functionality you should use Async functions without co.js.老实说,虽然在这一点上,生成器函数和异步函数的浏览器兼容性大致相同,因此如果您只想要异步等待功能,您应该使用没有 co.js 的异步函数。 (I recommend just using async/await it's pretty widely supported in most environments that the above strikethrough is supported in.) (我建议只使用 async/await,它在支持上述删除线的大多数环境中都得到了广泛支持。)

Browser support is actually pretty good now for Async functions (as of 2017) in all major current browsers (Chrome, Safari, and Edge) except IE.现在,除 IE 之外的所有主流浏览器(Chrome、Safari 和 Edge)中的 Async 功能(截至 2017 年)的浏览器支持实际上都非常好。

Take a look at JQuery Promises:看看 JQuery 承诺:

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

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

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

Refactor the code:重构代码:

var dfd = new jQuery.Deferred();


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

    // do the async call.
    myAsynchronousCall(param1, callBack);

    function doSomething(data) {
     // do stuff with data...
    }

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

You can force asynchronous JavaScript in NodeJS to be synchronous with sync-rpc .可以强制 NodeJS 中的异步 JavaScript 与sync-rpc 同步

It will definitely freeze your UI though, so I'm still a naysayer when it comes to whether what it's possible to take the shortcut you need to take.不过,它肯定会冻结您的用户界面,所以当谈到是否可以采取您需要采取的快捷方式时,我仍然持反对意见。 It's not possible to suspend the One And Only Thread in JavaScript, even if NodeJS lets you block it sometimes.在 JavaScript 中挂起唯一线程是不可能的,即使 NodeJS 有时允许您阻止它。 No callbacks, events, anything asynchronous at all will be able to process until your promise resolves.在您的承诺解决之前,任何回调,事件,任何异步都无法处理。 So unless you the reader have an unavoidable situation like the OP (or, in my case, are writing a glorified shell script with no callbacks, events, etc.), DO NOT DO THIS!因此,除非你的读者有像 OP 这样不可避免的情况(或者,在我的例子中,正在编写一个没有回调、事件等的美化 shell 脚本),不要这样做!

But here's how you can do this:但您可以这样做:

./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;

LIMITATIONS:限制:

These are both a consequence of how sync-rpc is implemented, which is by abusing require('child_process').spawnSync :这些都是sync-rpc实现方式的结果,这是通过滥用require('child_process').spawnSync

  1. This will not work in the browser.这在浏览器中不起作用。
  2. The arguments to your function must be serializable.函数的参数必须是可序列化的。 Your arguments will pass in and out of JSON.stringify , so functions and non-enumerable properties like prototype chains will be lost.您的参数将传入和传出JSON.stringify ,因此函数和不可枚举的属性(如原型链)将丢失。

There is one nice workaround at http://taskjs.org/ http://taskjs.org/ 上有一个很好的解决方法

It uses generators which are new to javascript.它使用对 javascript 来说是新的生成器。 So it's currently not implemented by most browsers.所以目前大多数浏览器都没有实现。 I tested it in firefox, and for me it is nice way to wrap asynchronous function.我在 Firefox 中对其进行了测试,对我来说这是包装异步函数的好方法。

Here is example code from project GitHub这是来自项目 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;
}

What you want is actually possible now.你想要的现在实际上是可能的。 If you can run the asynchronous code in a service worker, and the synchronous code in a web worker, then you can have the web worker send a synchronous XHR to the service worker, and while the service worker does the async things, the web worker's thread will wait.如果您可以在 Service Worker 中运行异步代码,并在 Web Worker 中运行同步代码,那么您可以让 Web Worker 向 Service Worker 发送同步 XHR,当 Service Worker 执行异步操作时,Web Worker 的线程将等待。 This is not a great approach, but it could work.这不是一个很好的方法,但它可以工作。

One thing people might not consider: If you control the async function (which other pieces of code depend on), AND the codepath it would take is not necessarily asynchronous, you can make it synchronous (without breaking those other pieces of code) by creating an optional parameter.人们可能不会考虑的一件事:如果您控制异步函数(其他代码段所依赖的),并且它所采用的代码路径不一定是异步的,您可以通过创建同步(不破坏其他代码段)一个可选参数。

Currently:目前:

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

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

Consider:考虑:

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'

Of course this doesn't work if the async function relies on inherently async operations (network requests, etc.), in which case the endeavor is futile (without effectively waiting idle-spinning for no reason).当然,如果异步功能依赖于固有的异步操作(网络请求等),则这不起作用,在这种情况下,努力是徒劳的(没有有效地无故等待空闲旋转)。

Also this is fairly ugly to return either a value or a Promise depending on the options passed in.此外,根据传入的选项返回值或 Promise 是相当丑陋的。

("Why would I have written an async function if it didn't use async constructs?" one might ask? Perhaps some modalities/parameters of the function require asynchronicity and others don't, and due to code duplication you wanted a monolithic block rather than separate modular chunks of code in different functions... For example perhaps the argument is either localDatabase (which doesn't require await) or remoteDatabase (which does). Then you could runtime error if you try to do {sync:true} on the remote database. Perhaps this scenario is indicative of another problem, but there you go.) (“如果不使用异步构造,我为什么要编写异步函数?”有人可能会问?也许函数的某些模式/参数需要异步性而其他不需要,并且由于代码重复,您需要一个整体块而不是在不同的函数中分离模块化的代码块......例如,参数可能是localDatabase (不需要await)或remoteDatabase (需要)。那么如果您尝试执行{sync:true}则可能会运行时错误{sync:true}在远程数据库上。也许这种情况表明存在另一个问题,但你去了。)

In Node.js it's possible to write synchronous code which actually invokes asynchronous operations.在 Node.js 中,可以编写实际调用异步操作的同步代码。 node-fibers allows this. node-fibers允许这样做。 It's a 3rd party native extension provided as an npm module.它是作为 npm 模块提供的第 3 方本机扩展。 It implements fibers/coroutines, so when a specific fiber is blocked waiting for asynchronous operation, the whole program events loop doesn't block - another fiber (if exists) continues its job.它实现了纤程/协程,因此当特定纤程被阻塞等待异步操作时,整个程序事件循环不会阻塞 - 另一个纤程(如果存在)继续其工作。

With fibers your code would look like this:使用光纤,您的代码将如下所示:

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

Note, that you should avoid it and use async/await instead.请注意,您应该避免它并使用async/await代替。 See below a note from the project readme https://github.com/laverdet/node-fibers :请参阅以下项目自述文件https://github.com/laverdet/node-fibers中的注释:

NOTE OF OBSOLESCENCE -- The author of this project recommends you avoid its use if possible.过时注意事项——该项目的作者建议您尽可能避免使用它。 The original version of this module targeted nodejs v0.1.x in early 2011 when JavaScript on the server looked a lot different.这个模块的原始版本在 2011 年初针对 nodejs v0.1.x,当时服务器上的 JavaScript 看起来有很大不同。 Since then async/await , Promises , and Generators were standardized and the ecosystem as a whole has moved in that direction.从那时起async/awaitPromisesGenerators被标准化,整个生态系统都朝着这个方向发展。

I'll continue to support newer versions of nodejs as long as possible but v8 and nodejs are extraordinarily complex and dynamic platforms.我将尽可能继续支持较新版本的 nodejs,但 v8 和 nodejs 是非常复杂和动态的平台。 It is inevitable that one day this library will abruptly stop working and no one will be able to do anything about it.不可避免地有一天,这个库会突然停止工作,没有人能够对此做任何事情。

I'd like to say thank you to all the users of fibers, your support over the years has meant a lot to me.我想对光纤的所有用户说声谢谢,你们多年来的支持对我来说意义重大。

Using Node 16's worker threads actually makes this possible, The following example the main thread is running the asynchronous code while the worker thread is waiting for it synchronously.使用 Node 16 的工作线程实际上使这成为可能,以下示例主线程正在运行异步代码,而工作线程正在同步等待它。

Not that is is very useful, but it at least does vaguely what the original question asked by waiting for asynchronous code synchronously.这不是很有用,但它至少模糊地完成了通过同步等待异步代码提出的原始问题。

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

This ability of promises includes two key features of synchronous operations as follows (or then() accepts two callbacks). Promise 的这种能力包括同步操作的两个关键特性,如下所示(或者 then() 接受两个回调)。 When you get the result, call resolve() and pass the final result.得到结果后,调用resolve()并传递最终结果。 In case of error, call reject().如果出现错误,调用reject()。

The idea is that the result is passed through the chain of .then() handlers.这个想法是通过 .then() 处理程序链传递结果。

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

使用Async Await和Promise.resolve / Promise.all😊

let result;
async_function().then(r => result = r);
while (result === undefined) // Wait result from async_function
    require('deasync').sleep(100);

You can also convert it into callbacks.您也可以将其转换为回调。

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

function foo() {    
  var fooVariable;

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

  return fooVariable;
}

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

The idea that you hope to achieve can be made possible if you tweak the requirement a little bit如果您稍微调整一下需求,您希望实现的想法就可以实现

The below code is possible if your runtime supports the ES6 specification.如果您的运行时支持 ES6 规范,则可以使用以下代码。

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM