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

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

我最近不得不更正网络应用程序(不是我创建的)中的安全问题。 安全问题是,它使用的是非 http-only cookie。 所以我必须将会话 cookie 设置为 http-only,这意味着您不能再从 javascript 读取(和设置)cookie 的值。 到目前为止非常容易。

更深层次的问题是,网络应用程序使用

JSON.parse(readCookie(cookieName)).some_value

在一百万个地方

因此,为了不必重写 "a million lines of code",我必须创建一个 ajax 端点,将 http-cookie 的内容作为 JSON 并重写 readCookie 使用 SYNCHRONOUS ajax 请求(而不是读取 cookie),因为其余可怕的代码期望 readCookie在这百万个地方要同步,因为读一个cookie是同步的。

现在的问题是,我得到了很多

Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user's experience. For more help, check https://xhr.spec.whatwg.org/.

这会向调试控制台发送垃圾邮件,更不用说有人决定删除此功能的可能性了。

因此,我正在研究新的 ES async/await 关键字,看看它是否能以某种方式帮助同步发出异步 ajax 请求(我知道我必须为 IE 11 使用包装器).

到目前为止,我阅读了这些页面
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://jakearchibald.com/2014/es7-async-functions/
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*

但看起来所有新的异步内容似乎都只解决了更容易编写异步代码的问题,而不是启用异步代码和现有同步代码之间的互操作。使用我阅读的信息,我现在可以等待异步 ajax-调用的结果,就像它是同步的一样, 但问题是 - 只允许在异步方法中等待...... 这意味着即使我可以像同步一样等待结果,getCookie 方法仍然必须是异步的,这使得所有的东西看起来完全没有意义(除非你的整个代码都是异步的,但它肯定不是当你不是从头开始时)...

我似乎找不到任何有关如何在同步代码和异步代码之间进行互操作的信息。

例如,在 C# 中,我可以使用 .Result 从同步上下文调用异步方法,例如

 AsyncContext.RunTask(MyAsyncMethod).Result;

或像

那样更容易但死锁安全性较差
MyAsyncMethod(args).Result;

有什么方法可以在 JavaScript 中实现同样的效果吗?

当代码库的其余部分是同步的,没有任何互操作的可能性时,分散异步似乎没有什么意义...... 公元2017年JavaScript真的还没有办法实现吗?

我再强调一下
我知道如何进行同步 ajax 调用,我知道如何使用异步 ajax 调用回调 and/or 承诺。
但我无法弄清楚的是如何同步异步-ajax-调用 (无回调) 以便可以从预期为 [= 的代码中使用它105=] 同步(在"a million places")!

这是我目前尝试过的方法:
(注意无论我用 loadQuote 还是 main, text "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.

没错,不,没有解决方法。 JavaScript 的 run-to-completion 语义 要求 同步函数在任何未决的异步操作(例如异步 XHR 调用的 XHR 处理程序的回调)之前完成可以 运行.

给定线程上 JavaScript 运行 的方式是它处理作业队列1:

  1. 选择下一个待处理的作业
  2. 同步执行该作业的代码
  3. 仅当该作业完成后才返回步骤 1 以获取下一个作业

(比这要复杂一点,有两个级别,但这与这个特定问题无关。)

XHR 完成等是在队列中安排的作业。无法暂停作业,运行 队列中的另一个作业,然后选择暂停的作业。 async/await 提供非常简单的 语法 来处理异步操作,但它们不会改变作业队列的性质。

对于您的情况,我看到的唯一解决方案是一直异步到顶层。这可能并不像您想象的那么复杂(或者可能会如此)。在许多情况下,它会在许多函数的 function 前面添加 async。但是,使这些函数异步可能会产生重大的 knock-on 影响(例如,在事件处理程序中同步的某些东西变得异步会改变与 UI 相关的事件发生的时间)。

例如,考虑这个同步代码:

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>

现在我们要使 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>

关键是我们在 doSomething 中尽快转为异步(因为 handler 不能异步)。但是,当然,这会改变与处理程序相关的工作时间。 (当然,我们可能应该更新 handler 以捕获来自 promise `doSomething() returns 的错误。)


1 这是 JavaScript 规范术语。 HTML5 规范(也涉及到这一点)称它们为 "tasks" 而不是 "jobs".

简短回答:不,正如您从 C# 中了解到的那样,无法在 JS 中使异步代码 运行 同步。使一切异步是一个可能的解决方案。

但是,由于您还控制服务器端,我有另一个建议(有点 hack):将所需信息(cookie 内容)作为请求的元数据发送,例如作为页面请求的 HTML 元标记或 XHR 请求的 HTTP 响应 header,并将其存储在某处。

你的方法有问题。首先,要完成 await for async 操作的部分代码,必须将其自身包装在 async 函数中。

例如:

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
}

您可以尝试将 autorun() 函数声明为 async,但这可能会导致其他问题。

我的建议是,如果您的 JS 应用程序有一个入口点,它是由 onload 事件触发的,请尝试在此点之前执行您的 ajax 调用,然后将其存储在本地变量中,然后从那里查询。

例如,如果您的代码如下所示:

function init () {
    // perform initialisations here
}

document.addEventListener("DOMContentLoaded", init)

将其更改为

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

并从应用程序其余部分的 cookieStash 中获取数据。您无需等待其他任何事情。