Best_Name
Best_Name

Reputation: 157

How to force synchronous loop execution in Javascript?

I'm writing a local application running on XAMPP on Google Chrome. It interacts with IndexedDB (I use the promise library of Jake Archibald). And here is my problem.

Let's say I have an object store with 2 properties, day and salary (money made that day). I want to console.log the whole object store for let's say a report. Here is the code snippet:

//Report 4 days of work
for(Day = 1; Day <= 4; Day++) {
  dbPromise.then(function(db) {
    var tx = transaction("workdays", "readonly");
    return tx.objectStore("workdays").get(Day);
  }).then(function(val) {
    console.log(val.day + '\t' + val.salary + '$\n';
  })

What I expected is something like this:

1    100$
2    120$
3    90$
4    105$

But it actually gives an error, saying "can't read value day of undefined". Turns out the loop didn't wait for the dbPromise.then()... but continued asynchronously, and since the IndexDB request is slow, by the time they finished, the Day counter was already 5 and no record was matched and returned.

I struggled for a while and then found a workaround by putting a DayTemp in the loop to capture the Day like this.

//Report 4 days of work
for(Day = 1; Day <= 4; Day++) {
  DayTemp = Day;
  dbPromise.then(function(db) {
    var tx = transaction("workdays", "readonly");
    return tx.objectStore("workdays").get(DayTemp);
  }).then(function(val) {
    console.log(val.day + '\t' + val.salary + '$\n';
  })

And it worked fine. But then it's still not. Here is the result:

1    100$
4    105$
2    120$
3    90$

I need them to be in order. What do I need to do? Thank you a lot!

Note: The situation is a little more complicated than this, so I can't use getAll() or cursor and things like that. I really have to do the looping. And also I'm interested to be enlightened in this synchronous/asynchronous subject of Javascript. I'm a beginner.

Update: I figured it out guys! First of all the result of my second attemp with DateTemp actually turn out like this:

4    105$
4    105$
4    105$
4    105$

Which is basically reflected the same problem in the beginning. The only difference this time is that Date is captured so it didn't increment past 4. But I finally found the solution for all of this. Let me demonstrate with this trivial Javascript snippet: (1)

function dummy(day)
{
    dbPromise.then(function(db) {
      //Create transaction, chooose object stores, etc,..
      objectStore.get(Day);
    }).then(function(db) {
      console.log(day + '\t' + salary + '\$n');
    })
}
for(Day = 1; Day <= 4; Day++)
{
    dummy(Day);
}

If I wrote the code like above, I will always get the right answers!

1    100$
2    120$
3    90$
4    105$

However if I wrote it like this: (2)

for(Day = 1; Day <= 4; Day++)
{
    dbPromise.then(function(db) {
      //Create transaction, chooose object stores, etc,..
      return objectStore.get(Day);
    }).then(function(val) {
      console.log(Day + '\t' + val.salary + '\$n');
    })
}

I will always get an error since the Day counter would be already 5 by the time the database requests finish the transaction creating and enter the get() method which use the Day counter! Crazy right?

Seem like Javascript have something to determine whether a statement should be waited, meaning it should executed completely before other statements behind it can begin executing. Take the (1) code snippet for example. Enter the loop with Day = 1, inside the loop body, Javascript saw that dummy() is pass Day as a parameter, and decided that "no way I'm gonna continue the loop without let the dummy() finished first, else he's gonna messed himself up". And I get a beautiful result.

In the (2) code snippet however. Javascript enter the loop and see that I called then() of dbPromise and it says "Okk executing big boy, but I don't see any reason I have to wait for you. Oh you use Day inside then() ha? I don't care about that, only look the ouside sorry. I will increment Day while you're doing your request!". And things got messy.

That's my first point. My second point is that I also found out that add() requests in indexedDB is asynchronization, even if each add() is inside an different transaction. But the get() is synchronization so it's fine.

Now want your opinions on my update answer above. Pretty sure they are incorrect in some embrassing ways. There have to be some very basics obvious things that I missed about Javascript here.

Upvotes: 0

Views: 7285

Answers (1)

Josh
Josh

Reputation: 18720

I suggest you learn about the following topics:

  • Asynchronous programming
  • Javascript function hoisting
  • The difference between defining a function and calling a function

As a general rule, never define a function within a loop. If you do not define a function in a loop, then you will avoid most of the complexity.

If you insist on defining a function in a loop, you can use the trick of using an immediately executed functional expression:

for(...) {
  (function defined_plus_call(a, b, c) {
    // operate on a and b and c here within this function's body
    // do NOT use the variables arg1ToUseForA, arg2ToUseForB, etc
  }(arg1ToUseForA, arg2ToUseForB, etc));
}

Or, if you are writing for only modern browsers, and are already familiar with promises and async programming, then you can use the new async/await syntax so that you can write an imperative loop:

function get_helper_function(tx, Day) {
  function executor(resolve, reject) {
    var request = tx.objectStore("workdays").get(Day);
    request.onsuccess = function() { resolve(request.result); };
    request.onerror = function() { reject(request.error); };
  }
  return new Promise(executor);     
}

async function foo(...) {
  for(Day = 1; Day <= 4; Day++) {
    var promise_result = await dbPromise;
    var tx = promise_result.transaction("workdays", "readonly");
    var val = await get_helper_function(tx, Day);
    console.log(val.day + '\t' + val.salary + '$\n';
  }
}

And then, if you really wanted to write cleaner code, I would change a few other things. One, these are all basic reads that can share the same database connection and the same transaction. Two, you can use Promise.all to iterate over several promises.

function get_by_day(tx, day) {
  function executor(resolve, reject) {
    var request = tx.objectStore("workdays").get(day);
    request.onsuccess = function() { resolve(request.result); };
    request.onerror = function() { reject(request.error); };
  }
  return new Promise(executor);     
}

function get_all(db) {
  var db = await dbPromise;
  var tx = db.transaction("workdays", "readonly");
  var promises = [];
  for(var day of days) {
    var promise = get_by_day(tx, day);
    promises.push(promise);
  }

  var all_promise = Promise.all(promises);
  return all_promise);
}

get_all().then(function(resolutions) {
  for(var val of resolutions) {
    console.log(val.day + '\t' + val.salary + '$\n';
  }
});

This way get_all resolves the individual get promises in any order, concurrently, at least in theory.

Then, go even further, if you want to try and optimize, and look at the function IDBObjectStore.prototype.getAll. Instead of explicitly getting each day, load a range of days in one function call.

Upvotes: 1

Related Questions