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

Posted

技术标签:

【中文标题】如何在同步函数中等待 JavaScript 中的异步调用?【英文标题】:How to await an async call in JavaScript in a synchronous function? 【发布时间】:2017-06-18 00:18:18 【问题描述】:

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

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

JSON.parse(readCookie(cookieName)).some_value

在一百万个地方

因此,为了不必重新编写“一百万行代码”,我必须创建一个 ajax-endpoint,将 http-cookie 的内容作为 JSON 提供给我并重写 readCookie 使用 SYNCHRONOUS ajax 请求(而不是读取 cookie),因为其余的可怕代码期望 readCookie 在这百万个地方是同步的,因为读取 cookie 是同步的。

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

主线程上的同步 XMLHttpRequest 已被弃用,因为 其对最终用户体验的不利影响。如需更多帮助, 检查https://xhr.spec.whatwg.org/。

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

因此,我正在研究新的 ES async/await 关键字,看看这是否有助于以某种方式同步生成异步 ajax 请求(我知道我必须使用 IE 11 的包装器)。

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

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

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

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

 AsyncContext.RunTask(MyAsyncMethod).Result;

或者更简单但不那么死锁安全,比如

MyAsyncMethod(args).Result;

有什么方法可以在 JavaScript 中实现相同的功能吗?

当代码库的其余部分是同步的,没有任何互操作的可能性时,传播异步似乎没有什么意义...... 在 2017 AD 中真的没有办法在 JavaScript 中实现这一点吗?

我再次强调我知道如何进行同步 ajax 调用,我知道如何使用异步带有回调和/或承诺的 ajax 调用。 但我无法弄清楚的是如何同步 async-ajax-call (无回调),以便可以从期望运行的代码中使用它同步 (在“一百万个地方”)!

这是我迄今为止尝试过的:(注意我是否使用 loadQuote main,文字 em> “Ron 曾经说过” 仍然首先出现在调试控制台中,如果 异步 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>

【问题讨论】:

简短回答:不,没有办法让异步代码在 JS 中同步运行,正如您从 C# 中所知道的那样。使一切异步是一种可能的解决方案。 @Lucero:大声笑,和我在下面发布的内容差不多,但要简洁得多。 :-) 【参考方案1】:

但问题是 - await 只允许在异步方法中。

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

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

    接下一个待处理的工作 同步执行该作业的代码 仅当该作业完成后,才返回第 1 步以获取下一个作业

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

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

对于您的情况,我看到的唯一解决方案是一直异步到顶层。这可能没有您想象的那么复杂(或者可能会)。在许多情况下,它会在很多功能上在function 前面添加async。但是,使这些函数异步可能会产生重大的连锁反应(例如,在事件处理程序中同步的东西变为异步会改变与 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>

现在我们想让 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() 返回中捕获错误。)


1 这是 JavaScript 规范术语。 HTML5 规范(也涉及到这一点)称它们为“任务”而不是“工作”。

【讨论】:

是的,最后一段正是我的想法——时间的变化正是它可能会引入很多错误的原因,这正是我不会这样做的原因。这还没有考虑到必须解决 IE11 一直到 IE9 缺乏异步支持所带来的努力和可能出现的问题...... @StefanSteiger:如果你启用异步/等待支持,最后一点很容易用 Babel 处理。但是,是的,这不会对您的代码库产生微不足道的影响。【参考方案2】:

你的方法有问题。首先,要完成await 的部分代码以完成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 获取数据。您无需等待其他任何事情。

【讨论】:

【参考方案3】:

简短的回答:没有办法让异步代码在 JS 中同步运行,正如您从 C# 中所知道的那样。让一切都异步是一种可能的解决方案。

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

【讨论】:

那行不通,因为应用程序也使用纯 html 页面。但是嗯,我可以添加一个脚本标签并将其作为 JSON 获取,这应该可以在任何地方使用。但后来我需要在一百万个地方添加脚本标签:(

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

浏览器中的JavaScript事件循环机制

深入理解jQuery中的callback

如何在同步nodejs函数中等待promise?

如何等待函数在javascript中完成?

JavaScript 运行机制详解

如何使单独的同步功能等待另一个异步功能?