3

I'm trying to wrap my head around promises, and how JavaScript works with it's queue and event loop etc.

I thought that if I put a slow synchronous function inside a promise, that slow sync function would be delegated to the background and I could use .then to deal with it when it was done.

function syncSleep(ms){
    var end = new Date().getTime() + ms;
    var start = new Date().getTime();

    while (start < end) {
      start = new Date().getTime();
    }
}

function p() {
  return new Promise(function(resolve) {
     syncSleep(5000);
     resolve("syncSleep done!");
  });
}

p().then( function(s) {
  var div = document.getElementById('async');
  div.innerHTML = s;
} );

var div = document.getElementById('sync');
div.innerHTML = "This should appear right away! (but it doesn't)";

https://jsfiddle.net/7mw6m2x5/

The UI is unresponsive while this code runs though.

So I was wondering, can someone explain what is going on here? Are Promises only a way to handle code that is already "made to be" async?

(If so, how is that done?)

How do I deal with slow sync code when I don't want it to freeze the UI? Do I have to use a web worker for that?

Grateful for any clarification. Thanks.

john.abraham
  • 145
  • 7
  • _"and I could use .then to deal with it when it was done"_ No value appears to be returned from `syncSleep`? _"How do I deal with slow sync code when I don't want it to freeze the UI?"_ What is the actual `javascript` being implemented? What are you trying to achieve? – guest271314 May 07 '16 at 17:59
  • @guest271314, I am just trying to understand how this JavaScript works. I had a hypothesis about how this code would work, but it turned out I was wrong. So now I'm trying to correct my understanding :-) – john.abraham May 07 '16 at 18:28
  • Yes, you should use WebWorkers or child_process/cluster for this. There is no way to handle slow sync code in a single threaded language. Alternatively, if you're allowed to modify the slow code, you could re-write it using ES6 generators/iterators or whatever they were called. – Florian Wendelborn May 07 '16 at 18:29
  • @john.abraham The only have been able to display `"This should appear right away! (but it doesn't)"` before `while` completes is to use `alert()`, that is, call a native global function. Could alternatively simply update `DOM` before `while` loop is called in `syncSleep` – guest271314 May 07 '16 at 18:33
  • 1
    API tip: don't use `new Date().getTime()`, use `Date.now()`. Same result, no memory wasted on object allocation or time wasted on unnecessary GC. – Mike 'Pomax' Kamermans May 07 '16 at 18:47
  • @john.abraham https://jsfiddle.net/7mw6m2x5/2/ , https://jsfiddle.net/7mw6m2x5/3/ – guest271314 May 07 '16 at 18:57
  • Promises don't magically make a synchronous operation async. They are only a tool to help you manage callbacks and completion/error state. – jfriend00 May 07 '16 at 20:13

3 Answers3

4

The code works as expected.

Since Javascript is single threaded, the UI will block while your loop is executing.

Promises are just a nice way to handle async code. See an introduction here:

http://www.html5rocks.com/en/tutorials/es6/promises/

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

To be able to maintain the UI reponsive while other code is executing in the background, you would need to use WebWorkers:

https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers

Quoting from the page listed above:

"Web Workers provide a simple means for web content to run scripts in background threads. The worker thread can perform tasks without interfering with the user interface."

Update:

following your comments, I crafted this little script to demonstrate the difference between blocking and no-blocking approaches. There is some repetition in the code, but I think it is simple enough to understand.

function setVal(s) {
  var divAsync = document.getElementById('async');
  var innerDiv = document.createElement('div');
  innerDiv.innerHTML = s + '<br>';
  divAsync.appendChild(innerDiv);
}


function syncSleep(ms) {
  var end = new Date().getTime() + ms;
  var now = new Date().getTime();
  var stepBegin = new Date().getTime();
  var step = 0;

  // This loop is blocking
  // The UI will only refresh after the loop completion
  while (now < end) {
    now = new Date().getTime();
    step = now - stepBegin;
    if (step >= 1000) {
      setVal(now);
      step = 0;
      stepBegin = now;
    }
  }

}

function pBlock() {
  return new Promise(function(resolve) {
    syncSleep(3200);
    resolve("pBlock syncSleep done!");
  });
}


function noBlockUpdate(ms, resolve) {
  var end = new Date().getTime() + ms;
  var now = new Date().getTime();
  var stepBegin = new Date().getTime();
  var step = 0;

  function noBlock() {
    now = new Date().getTime();

    // paint every 1000ms;
    step = now - stepBegin;
    if (step >= 1000) {
      setVal(now);
      step = 0;
      stepBegin = now;
    }

    if (now < end) {
      // NB: this is going to be called thousands of times
      // But the UI will still update every 1000 ms
      setTimeout(noBlock);
    } else {
      resolve("pNoBlock done!");
    }
  };
  noBlock();
}

function pNoBlock() {
  return new Promise(function(resolve) {
    noBlockUpdate(3200, resolve);
    setVal("pNoBlock launched!");
  });
}



pNoBlock().then(setVal);

var divSync = document.getElementById('sync');
divSync.innerHTML = "This appears right away!";



// Just wait 4 seconds so the non-blocking code completes
setTimeout(function() {
  // Clear all div's
  document.getElementById('sync').innerHTML = '';
  document.getElementById('async').innerHTML = '';

  var divSync = document.getElementById('sync');
  divSync.innerHTML = "This does not appear right away, only after the blocking operation is complete!";

  pBlock().then(setVal);
}, 4000);
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>

<body>

  <div id="sync"></div>

  <div id="async"></div>

</body>

</html>
VRPF
  • 3,118
  • 1
  • 14
  • 15
  • 1
    But why isn't the code inside the Promise pushed to the back of the queue? I.e. why does the code freeze right away and not after "div.innerHTML = ... " (at the bottom) – john.abraham May 07 '16 at 18:22
  • Page JS only has one thread avaiable (if you need off-thread work, you need to use webworkers or the more general service worker). Your promise can, and thus does, immediately execute, and so immediately locks the thread by running your (incredibly bad, because it'll force the CPU into overdrive - five seconds of 100% cpu noops...) spin loop. – Mike 'Pomax' Kamermans May 07 '16 at 18:49
  • @Mike'Pomax'Kamermans If I use "*setTimeout(()=>{syncSleep(5000);resolve("syncSleep done!!")},0)*" inside the Promise, it behaves as I expected. Am I right in thinking that this is because *setTimeout()* is provided by the DOM, and it actually runs *syncSleep* in a different thread? – john.abraham May 07 '16 at 19:58
  • 1
    not quite. `setTimeout` schedules whatever you pass in as "to be evaluated *at least* X ms from now, when the thread is idle". The "at least" part is important here: if some code path blocks the thread longer than your timeout needs, your timeout will fire *after* that block resolves. So because your code doesn't really do anything else, the timeout does "the right thing", but if you had a whole bunch of poorly written synchronous code that ran after your promise setup, the thread would be occupied until it finished processing that. Page JS *really* only has one thread. – Mike 'Pomax' Kamermans May 07 '16 at 20:06
  • @Mike'Pomax'Kamermans Actually when I do this is works as I initially expected some of the times, but other times it freezes. What's going on? – john.abraham May 07 '16 at 20:07
  • 1
    @john.abraham I tried to address your questions with an update – VRPF May 07 '16 at 22:23
3

Are Promises only a way to handle code that is already 'made to be' async?

Yes. Promises don't truly create asynchronous operations. Their intention is just to make working with asynchronous operations easier, by defining a consistent API. But, they don't avoid blocking on their own beyond one small delay between when resolve() and .then() callbacks are invoked.

The function provided to the Promise constructor is invoked immediately and synchronously. And, once a .then() callback begins executing, it will have to run to completion before the engine does anything else.

How do I deal with slow sync code when I don't want it to freeze the UI?

Try to avoid long-running synchronous operations as much as possible. An asynchronous alternative to syncSleep() would be setTimeout().

function p() {
  return new Promise(function(resolve) {
    setTimeout(function () {
      resolve("syncSleep done!");
    }, 5000);
  });
}

If a long-running synchronous operation can't be avoided, then you'll want to try to move it to a separate process / instance of the engine – and, in browsers, that can be done with Web Workers.

A possible middle ground may be, if you can break up the operation into multiple steps, to separate each step by a brief setTimeout(). Those breaks will give the engine time periodically to work on other events, including updating the page for the user. You'll want each step to be small to have as many breaks as often as possible, since each step will still block everything else once it starts.

Jonathan Lonowski
  • 121,453
  • 34
  • 200
  • 199
  • Thank you for your reply, of course I would never use a synchronous sleep function in a real application. I've done this for pedagogical reasons; trying to understand how it works. – john.abraham May 07 '16 at 18:26
2

Promises are not threads. They're just a sugar for handling success and failure events (callbacks) in single-threaded code.

Callback to new Promise(cb) constructor is executed immediately and synchronously. Callbacks given to .then(cb)/.catch(cb) are executed on next tick after the promise is resolved/rejected, but they also run on the same—and only—thread.

Kornel
  • 97,764
  • 37
  • 219
  • 309