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

22

最近我不得不修复一个Web应用程序中的安全问题(我没有创建它)。 安全问题是,它使用了非HttpOnly Cookie。 因此,我必须将会话Cookie设置为HttpOnly,这意味着您无法再从Javascript中读取(和设置)Cookie的值。 到目前为止,看起来很容易。

更深层次的问题是,该Web应用程序使用了

JSON.parse(readCookie(cookieName)).some_value

在许多地方上

为了不必重写“数百万行代码”,我必须创建一个Ajax端点,将http-cookie的内容作为JSON返回,并重写readCookie以使用同步 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.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调用。
但是,我无法想出如何同步异步ajax调用(没有回调),以便可以从期望同步运行的代码中使用它(在“数百万个地方”运行)!

到目前为止,我尝试过以下方法:(请注意,无论我使用 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>

5
简短回答:不,无法像在C#中那样使异步代码运行同步。将所有内容改为异步是一种可能的解决方案。 - Lucero
@Lucero:哈哈,基本上就是我在下面发布的内容,但更加简洁明了。 :-) - T.J. Crowder
我认为重要的是要明白,你所请求的不是Javascript中的功能缺失,而是由于Javascript的单线程(只有一个线程执行用户代码)异步事件驱动执行模型,这根本就没有意义。如果你强制XHR同步执行,它将阻塞整个事件循环(因此也会阻塞UI)每个 XHR。@T.J.Crowder的答案提到了这一点,但我认为明确说明原因是有用的。 - Inigo
我知道这已经晚了五年,但一个可能的解决方案是在前面一次性进行异步 cookie 读取 XHR,将结果保存在全局变量或缓存中,并且仅在它完成后(使用 awaitthen)开始依赖于它的 UI 代码。然后,用同步的 readCookieFromCache 调用替换 "million" 同步的 readCookie 调用。 - Inigo
3个回答

15
但问题是 - await只能在async方法中使用。
没错,而且没有解决办法。 JavaScript的按顺序执行机制要求同步函数在任何挂起的异步操作(例如针对异步XHR调用的回调的XHR处理程序)之前完成。
JavaScript在给定线程上运行的方式是处理作业队列1:
选取下一个待处理作业 同步执行该作业的代码 只有当该作业完成后才返回到步骤1以选取下一个作业
XHR完成等都是会进入队列中的作业。没有办法暂停一个作业,运行另一个队列中的作业,然后继续执行被暂停的作业。async/await提供了更简单的语法来处理异步操作,但它们不改变作业队列的本质。
我看到你的情况唯一的解决方案就是全部转换为异步处理。这可能并不像你想象的那么复杂(或者可能确实很复杂)。在很多情况下,只需要在许多函数前面加上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以从doSomething()返回的Promise对象中捕获错误。)


1 这是JavaScript规范术语。HTML5规范(也涉及此问题)将其称为“任务”而不是“作业”。


1
是的,最后一段正是我所思考的 - 时间变化正是为什么它可能会引入很多错误的原因,这也正是我不会这样做的确切原因。而且,在兼容IE11到IE9的缺少异步支持的情况下解决问题所需的努力和可能出现的问题都未计算在内... - Stefan Steiger
2
@StefanSteiger:如果您启用了async/await支持,那么最后一部分可以很容易地通过Babel处理。但是,是的,这对您的代码库来说并不是一个微不足道的影响。 - T.J. Crowder
让一切都异步化 - 如果我需要覆盖一个“同步”的第三方服务方法,但是在返回结果之前,我需要插入一个“等待异步调用”的内容,这个该怎么处理呢? - Alexander
@Alexander - (打破我对罢工的沉默。) 如果那个覆盖必须根据等待异步调用返回结果,那么恐怕你可能运气不佳。:-| 你不能让一个同步方法等待同一 JavaScript 领域内的异步结果,如果你在主线程上工作,这是不可取的(因为它会锁定用户界面和主线程上的其他工作)。在某些环境中,可能存在跨领域(甚至跨进程)的选项。在浏览器的主线程上,你只能使用同步 AJAX 向执行异步工作的服务器进程发送请求(而且这种方式正在逐渐消失)。 - T.J. Crowder
@T.J.Crowder非常感谢。我猜我会尝试始终从“它不应该这样工作”的角度看待任务。在给定的情况下,我只是隐藏了一个期望异步结果的UI元素,并在数据准备好时显示它。 - Alexander

6

你的方法存在问题。首先,如果代码的一部分需要等待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获取您的数据。您不需要等待其他任何内容。


2
短暂回答:不,JS中没有像C#一样将异步代码变为同步运行的方法。将所有内容都变成异步是一种可能的解决方案。
但是,由于您还控制服务器端,我有另一个建议(有点骇客):将所需信息(cookie内容)作为请求的元数据发送,例如对于页面请求,可以作为HTML meta标签或对于XHR请求,可以作为HTTP响应头,并将其存储在某个位置。

那行不通,因为应用程序还使用纯HTML页面。但是嗯,我可以添加一个脚本标签并将其作为JSON获取,这应该在任何地方都可以工作。但是然后我需要在无数个地方添加脚本标签:( - Stefan Steiger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接