简体   繁体   English

如何在同步函数中等待 JavaScript 中的异步调用?

[英]How to await an async call in JavaScript in a synchronous function?

I recently had to correct security issues in a web-application (that I didn't create).我最近不得不纠正 Web 应用程序(不是我创建的)中的安全问题。 The security problem was, it was using non-http-only cookies.安全问题是,它使用了非 http-only cookie。 So I had to set the session-cookie http-only, which means you can't read (and set) the cookie's value anymore from javascript.所以我不得不设置会话cookie http-only,这意味着你不能再从javascript读取(和设置)cookie的值。 So far so seamingly easy.到目前为止,非常容易。

The deeper problem was, the web-application used更深层次的问题是,使用的网络应用程序

JSON.parse(readCookie(cookieName)).some_value

on a million places .在一百万个地方

So in order to not have to re-write "a million lines of code", I had to create an ajax-endpoint that gave me the http-cookie's content as JSON and rewrite readCookie to use SYNCHRONOUS ajax requests (instead of reading the cookie), because the rest of the horrible code expects readCookie to be synchronous at these million places, because reading a cookie is synchronous.因此,为了不必重写“一百万行代码”,我必须创建一个 ajax 端点,将 http-cookie 的内容作为 JSON 给我,并重写 readCookie 以使用同步ajax 请求(而不是读取 cookie ),因为其余的可怕代码期望 readCookie 在这百万个地方是同步的,因为读取 cookie 是同步的。

The problem now is, I get a lot of现在的问题是,我得到了很多

Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user's experience.主线程上的同步 XMLHttpRequest 已被弃用,因为它会对最终用户的体验产生不利影响。 For more help, check https://xhr.spec.whatwg.org/ .如需更多帮助,请查看https://xhr.spec.whatwg.org/

which spams the debug console, let alone the possibility someone decides to remove this functionality.这会向调试控制台发送垃圾邮件,更不用说有人决定删除此功能的可能性了。

I am therefore looking into the new ES async/await keywords, to see if that could help somehow in making a asynchronous ajax-request synchronously (i know I have to use wrappers for IE 11).因此,我正在研究新的 ES async/await 关键字,看看这是否有助于以某种方式同步生成异步 ajax 请求(我知道我必须使用 IE 11 的包装器)。

So far, I read these pages到目前为止,我阅读了这些页面
https://www.twilio.com/blog/2015/10/asyncawait-the-hero-javascript-deserved.html https://www.twilio.com/blog/2015/10/asyncawait-the-hero-javascript-deserved.html
https://pouchdb.com/2015/03/05/taming-the-async-beast-with-es7.html https://pouchdb.com/2015/03/05/taming-the-async-beast-with-es7.html
https://jakearchibald.com/2014/es7-async-functions/ https://jakearchibald.com/2014/es7-async-functions/
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function *

but it looks like all the new async stuff seems to only cater to the problem of writing asynchronous code easier, not enabling interop between asynchronous and existing synchronous code.但看起来所有新的异步内容似乎只是为了解决更容易编写异步代码的问题,而不是在异步代码和现有同步代码之间实现互操作。 Using the information I read, I can now await the result of an asynchronous ajax-call like it was synchronous, but the problem is - await is only allowed in async-methods... Which means even if I can await the result like it was synchronous, the getCookie method would still have to be async, which makes all the stuff appear to be completely pointless (unless your entire code would-be async, which it certainly isn't when you don't start from scratch)...使用我阅读的信息,我现在可以等待异步 ajax 调用的结果,就像它是同步的一样,但问题是 - await 只允许在异步方法中......这意味着即使我可以像这样等待结果是同步的,getCookie 方法仍然必须是异步的,这使得所有的东西看起来完全没有意义(除非你的整个代码都是异步的,当你不从头开始时肯定不是).. .

I can't seem to find any information on how to interop between synchronous and asynchronous code.我似乎找不到任何关于如何在同步和异步代码之间进行互操作的信息。

For example, in C#, I can call an async-method from a synchronous context with .Result, eg例如,在 C# 中,我可以使用 .Result 从同步上下文中调用异步方法,例如

 AsyncContext.RunTask(MyAsyncMethod).Result;

or easier but less deadlock-safe like或更简单但不那么死锁安全,例如

MyAsyncMethod(args).Result;

Is there any way to achieve the same in JavaScript ?有什么方法可以在 JavaScript 中实现相同的功能吗?

It seems to make little sense to spread async around, when the rest of the codebase is synchronous, without any possibility of interop... Is there really still no way to achieve this in JavaScript in 2017 AD ?当代码库的其余部分是同步的,没有任何互操作的可能性时,传播异步似乎没有什么意义......在 2017 AD 的 JavaScript 中真的没有办法实现这一点吗?

I emphasize again :我再次强调
I know how I can make a synchronous ajax-call, and I know how to use async ajax calls with callbacks and/or promises.我知道如何进行同步 ajax 调用,也知道如何使用带有回调和/或 promise 的异步 ajax 调用。
But what I'm unable to figure out is how to synchronize an async-ajax-call (no callback) so it can be used from code that expects to be run synchronously (in "a million places") !但是我无法弄清楚如何同步 async-ajax-call (无回调) ,以便可以从期望同步运行的代码中使用它(在“一百万个地方”)!

This is what I have tried so far:这是我到目前为止所尝试的:
(Note that whether I use loadQuote or main , the text "Ron once said" still appears first in the debug-console, which should not be the case if the asynchronous ajax-call had been resolved synchronously ) (请注意,无论我使用loadQuote还是main ,文本“Ron once said”仍然首先出现在调试控制台中,如果异步ajax 调用已同步解决,则不应出现这种情况)

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />

    <meta http-equiv="cache-control" content="max-age=0" />
    <meta http-equiv="cache-control" content="no-cache" />
    <meta http-equiv="expires" content="0" />
    <meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
    <meta http-equiv="pragma" content="no-cache" />

    <meta charset="utf-8" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

    <meta http-equiv="Content-Language" content="en" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />

    <meta name="google" value="notranslate" />


    <!--
    <meta name="author" content="name" />
    <meta name="description" content="description here" />
    <meta name="keywords" content="keywords,here" />

    <link rel="shortcut icon" href="favicon.ico" type="image/vnd.microsoft.icon" />
    <link rel="stylesheet" href="stylesheet.css" type="text/css" />
    -->

    <title>Title</title>

    <style type="text/css" media="all">
        body
        {
            background-color: #0c70b4;
            color: #546775;
            font: normal 400 18px "PT Sans", sans-serif;
            -webkit-font-smoothing: antialiased;
        }
    </style>


    <script type="text/javascript">
        <!-- 
        // http://localhost:57566/foobar/ajax/json.ashx

        var ajax = {};
        ajax.x = function () {
            if (typeof XMLHttpRequest !== 'undefined') {
                return new XMLHttpRequest();
            }
            var versions = [
                "MSXML2.XmlHttp.6.0",
                "MSXML2.XmlHttp.5.0",
                "MSXML2.XmlHttp.4.0",
                "MSXML2.XmlHttp.3.0",
                "MSXML2.XmlHttp.2.0",
                "Microsoft.XmlHttp"
            ];

            var xhr;
            for (var i = 0; i < versions.length; i++) {
                try {
                    xhr = new ActiveXObject(versions[i]);
                    break;
                } catch (e) {
                }
            }
            return xhr;
        };

        ajax.send = function (url, callback, method, data, async) {
            if (async === undefined) {
                async = true;
            }
            var x = ajax.x();
            x.open(method, url, async);
            x.onreadystatechange = function () {
                if (x.readyState == 4) {
                    callback(x.responseText)
                }
            };
            if (method == 'POST') {
                x.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
            }
            x.send(data)
        };

        ajax.get = function (url, data, callback, async) {
            var query = [];
            for (var key in data) {
                query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]));
            }
            ajax.send(url + (query.length ? '?' + query.join('&') : ''), callback, 'GET', null, async)
        };

        ajax.post = function (url, data, callback, async) {
            var query = [];
            for (var key in data) {
                query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]));
            }
            ajax.send(url, callback, 'POST', query.join('&'), async)
        };


        ///////////



        function testAjaxCall() {
            ajax.get("./ajax/json.ashx", null, function (bError, strMessage, iStatus)
                {
                    console.log("args:", arguments);

                    console.log("Error:", bError);
                    console.log("Message:", strMessage);
                    console.log("Status:", iStatus);
                }
                , true
            );

        }
        -->
    </script>

</head>
<body>

    <script type="text/javascript">

        function getQuote() {
            var quote;

            return new Promise(function (resolve, reject) {

                ajax.get("./ajax/json.ashx", null, function (bError, strMessage, iStatus) {

                    // console.log("args:", arguments);

                    // console.log("Error:", bError);
                    // console.log("Message:", strMessage);
                    // console.log("Status:", iStatus);


                    quote = bError;
                    resolve(quote)

                }, true);


                /*
                request('./ajax/json.ashx', function (error, response, body) {
                    quote = body;

                    resolve(quote);
                });
                */

            });

        }

        async function main() {
            var quote = await getQuote();
            console.log("quote: ", quote);
        }

        function myGetQuote() {
            var quote = async function () { return await getQuote(); };

            console.log("quote: ", quote);

            return quote;
        }

        function spawn(generatorFunc) {
            function continuer(verb, arg) {
                var result;
                try {
                    result = generator[verb](arg);
                } catch (err) {
                    return Promise.reject(err);
                }
                if (result.done) {
                    return result.value;
                } else {
                    return Promise.resolve(result.value).then(onFulfilled, onRejected);
                }
            }
            var generator = generatorFunc();
            var onFulfilled = continuer.bind(continuer, "next");
            var onRejected = continuer.bind(continuer, "throw");
            return onFulfilled();
        }


        function loadQuote() 
        {
            return spawn(function *() {
                try {
                    let story = yield getQuote();

                    console.log("story:", story);
                    // addHtmlToPage(story.heading);
                    // for (let chapter of story.chapterURLs.map(getJSON)) { addHtmlToPage((yield chapter).html); } addTextToPage("All done");
                } catch (err) {
                    //addTextToPage("Argh, broken: " + err.message);
                    console.log("Argh, broken: " + err.message);
                }
                //document.querySelector('.spinner').style.display = 'none';
            });
        }



        function autorun()
        {           
            console.clear();    
            // main();
            // main();
            loadQuote();

            //var quote = myGetQuote();

            // console.log("quote: ", quote);
            console.log('Ron once said,');

        }

        if (document.addEventListener) document.addEventListener("DOMContentLoaded", autorun, false);
        else if (document.attachEvent) document.attachEvent("onreadystatechange", autorun);
        else window.onload = autorun;
    </script>

</body>
</html>

but the problem is - await is only allowed in async-methods.但问题是 - await 只允许在异步方法中。

Exactly, and no, there's no workaround for that.确切地说,不,没有解决方法。 JavaScript's run-to-completion semantics demand that synchronous functions complete before any pending asynchronous action (such as the callback to an XHR handler for an async XHR call) can run. JavaScript 的 run-to-completion 语义要求同步函数在任何挂起的异步操作(例如对异步 XHR 调用的 XHR 处理程序的回调)可以运行之前完成。

The way JavaScript runs on a given thread is that it processes a queue of jobs 1 : JavaScript 在给定线程上运行的方式是它处理作业队列1

  1. Pick up the next pending job拿起下一个待处理的工作
  2. Synchronously execute the code for that job同步执行该作业的代码
  3. Only when that job completes go back to Step 1 to pick up the next job仅当该作业完成时,才返回步骤 1 接下一个作业

(It's a bit more complicated than that, there are two levels to it, but that's not relevant to this particular question.) (比这复杂一点,它有两个级别,但这与这个特定问题无关。)

XHR completions and such are jobs that get scheduled in the queue. XHR 完成等是在队列中安排的作业。 There is no way to pause a job, run another job from the queue, and then pick up the paused job.没有办法暂停一个作业,从队列中运行另一个作业,然后拿起暂停的作业。 async / await provides dramatically simpler syntax for handling asynchronous operations, but they don't change the nature of the job queue. async / await为处理异步操作提供了非常简单的语法,但它们不会改变作业队列的性质。

The only solution I see for your situation is to go async all the way to the top level.对于您的情况,我看到的唯一解决方案是一直异步到顶层。 This may not be as complicated as you might think (or maybe it will be).这可能没有您想象的那么复杂(或者可能会)。 In many cases it's adding async in front of function on a lot of functions.在许多情况下,它会在很多函数的function前面添加async However, making those functions asynchronous is likely to have significant knock-on effects (for instance, something that was synchronous in an event handler becoming asynchronous changes the timing of what happens in relation to the UI).但是,使这些函数异步可能会产生重大的连锁反应(例如,在事件处理程序中同步的东西变为异步会改变与 UI 相关的事情发生的时间)。

For example, consider this synchronous code:例如,考虑这个同步代码:

 var btn = document.getElementById("btn"); btn.addEventListener("click", handler, false); function handler(e) { console.log("handler triggered"); doSomething(); console.log("handler done"); } function doSomething() { doThis(); doThat(); doTheOther(); } function doThis() { console.log("doThis - start & end"); } function doThat() { console.log("doThat - start"); // do something that takes a while var stop = Date.now() + 1000; while (Date.now() < stop) { // wait } console.log("doThat - end"); } function doTheOther() { console.log("doThat - start & end"); }
 .as-console.wrapper { max-height: 80% !important; }
 <input type="button" id="btn" value="Click Me"> <p id="text"></p>

Now we want to make make doThat async ( note : will only work on a recent browser supporting async / await , like Chrome; sadly Stack Snippet's Babel config doesn't include them, so we can't use that option):现在我们想让 make doThat异步(注意:只能在支持async / await的最新浏览器上工作,比如 Chrome;遗憾的是 Stack Snippet 的 Babel 配置不包含它们,所以我们不能使用该选项):

 var btn = document.getElementById("btn"); btn.addEventListener("click", handler, false); // handler can't be async function handler(e) { console.log("handler triggered"); doSomething(); console.log("handler done"); } // doSomething can be async function doSomething() { doThis(); await doThat(); doTheOther(); } function doThis() { console.log("doThis - start & end"); } // make doThat async async function doThat() { console.log("doThat - start"); // simulate beginning async operation with setTimeout return new Promise(resolve => { setTimeout(() => { // do something that takes a while var stop = Date.now() + 1000; while (Date.now() < stop) { // wait } console.log("doThat - end (async)"); }, 0); }); } function doTheOther() { console.log("doThat - start & end"); }
 .as-console.wrapper { max-height: 80% !important; }
 <input type="button" id="btn" value="Click Me"> <p id="text"></p>

The key thing there is we went async as soon as we could, in doSomething (since handler can't be async).关键是我们在doSomething中尽可能地异步(因为handler不能是异步的)。 But of course, that changes the timing of the work in relation to the handler.但是,当然,这会改变与处理程序相关的工作时间。 (Of course, we probably should have updated handler to catch errors from the promise `doSomething() returns.) (当然,我们可能应该更新handler以从 promise `doSomething() 返回中捕获错误。)


1 That's the JavaScript spec terminology. 1这是 JavaScript 规范术语。 The HTML5 spec (which also touches on this) calls them "tasks" instead of "jobs". HTML5 规范(也涉及到这一点)称它们为“任务”而不是“工作”。

There's is a problem with your approach.你的方法有问题。 First, for part of code to await for async operation to finish, it must be wrapped itself in a async function.首先,对于await async操作完成的部分代码,必须将其自身包装在async函数中。

For example:例如:

async function asyncExample () {
    try {
        const response = await myPromise()

        // the code here will wait for the 
        // promise to fullfil
    } catch (error) {
        // the code here will execute if the promise fails
    }
}

function nonAsyncExample () {
    asyncExample () 

    console.log('this will not wait for the async to finish')
    // as it's not wrapped in an async function itself
}

You could try to declare the autorun() function as async , but that may lead to additional complications.您可以尝试将autorun()函数声明为async ,但这可能会导致额外的复杂性。

My suggestion, if your JS app has an entry point, it's triggered by an onload event, try to do your ajax call before this point and then store it locally in a variable and query it from there.我的建议是,如果您的 JS 应用程序有一个入口点,它是由onload事件触发的,请尝试在此之前进行 ajax 调用,然后将其存储在本地变量中并从那里查询。

For example, if your code looks like:例如,如果您的代码如下所示:

function init () {
    // perform initialisations here
}

document.addEventListener("DOMContentLoaded", init)

change that to be将其更改为

document.addEventListener("DOMContentLoaded", function () {
    getAjaxConfig().then(function (response) {
        window.cookieStash = response
        init()
    }
})

and get your data from the cookieStash in the rest of the application.并从应用程序其余部分的cookieStash中获取您的数据。 You won't need to wait for anything else.您无需等待其他任何事情。

Short answer: no there is no way to make async code run synchronous in JS as you know it from C#.简短的回答:没有办法让异步代码在 JS 中同步运行,正如你从 C# 中所知道的那样。 Making everything asynchronous is a possible solution.使一切异步是一种可能的解决方案。

However, since you also control the server side, I have another suggestion (bit of a hack): send along the required information (cookie content) as metadata of the request, eg as HTML meta tag for page requests or HTTP response header for XHR requests, and store it somewhere.但是,由于您还控制服务器端,我有另一个建议(有点小技巧):将所需信息(cookie 内容)作为请求的元数据发送,例如作为页面请求的 HTML 元标记或 XHR 的 HTTP 响应标头请求,并将其存储在某个地方。

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

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