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

那年仲夏 提交于 2019-12-03 06:31:59

but the problem is - await is only allowed in async-methods.

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.

The way JavaScript runs on a given thread is that it processes a queue of jobs1:

  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

(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. There is no way to pause a job, run another job from the queue, and the pick up the paused job. async/await provide dramatically simpler syntax for handling asynchronous operations, but they don't change the nature of the job queue.

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. 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).

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):

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). 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.)


1 That's the JavaScript spec terminology. The HTML5 spec (which also touches on this) calls them "tasks" instead of "jobs".

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.

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.

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.

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. 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#. 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, e.g. as HTML meta tag for page requests or HTTP response header for XHR requests, and store it somewhere.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!