1

I've got some experience in PHP, but I'm starting out with javascript and jquery. I'm working on my first project. I thought that scripting is scripting, and there will be little difference between this and PHP. Well was I wrong. For the first time I saw that something which is first in the code executes last!

Please have a look at this function which is meant to get svg and store them in json object to use as inline svg later

var svgIcons = { "arrow_left": "", "arrow_right":"",  } //json object with empty values

    this.getIcons = function() {

    for (var icon_name in svgIcons) {
        if (svgIcons.hasOwnProperty(icon_name)) {
            var url=PHP.plugin_url+'/includes/icons/'+icon_name+'.svg';
            jQuery.get(url, function(data) { 

            svgIcons[icon_name]=data;

            console.log('iterating');
            console.log(svgIcons[icon_name]); //outputs svg
            });
        }
    }

    console.log('this should be after iteration');
    console.log(svgIcons["arrow_left"]); //empty

}
this.getIcons(); //called at object initialization

But the output is:

this should be after iteration

iterating
#document (and svg inside it)
iterating
#document (and svg inside it)

What is the cause of this change of order? Is it the get() function? How do I avoid situations like this?

Kox
  • 25
  • 5

2 Answers2

1

jQuery.get is asynchronous. You are iterating inside the callback for an AJAX call, so that gets executed whenever the AJAX call is completed.

AJAX callbacks, setTimeout and setInterval are some asynchronous Javascript functions. Some threads you might find useful:

Edit: Yes, the function call ends before any of the callback stuff happens. Basically the execution of your JS will be linear, placing functions on a call stack whenever they are called. On the call-stack they are executed one-by-one, line-by-line. However, when one of those lines calls an asynchronous function (like a setTimeout or AJAX), the current execution places the async function on the call-stack and immediately returns to complete itself. So something like:

function myFunc(){
   console.log('a');
   setTimeout(function(){
       console.log('b');
   },0)
   console.log('c');
}
myFunc();

would always log:

a
c
b

...even though the setTimeout is 0.

So, in your case what must be happening is that you are assigning the AJAX-received data to svgIcons[icon_name] inside the async callback (obviously), while the rest of your code which uses the object svgIcons is in the sequential/normal execution. You either have to move the code that uses the object inside the async callback, or use promises (basically promises are functions that are executed after an async call is completed).

2nd Edit: So, the reason you are not able to set svgIcons[icon_name] inside the callback is related to the things I was mentioning in my comment. When synchronous functions are called, they are placed on top of the current stack and executed right away, before returning to the calling function. So if you called a sync function inside a loop:

function outer(){
  function inner(){
      console.log(i);
  }
  for(var i=0;i<3;i++)
      inner();
 }
 outer();

the synchronous inner function would be executed right away inside each loop, and would have access to the current value of i, so it would output 0, 1, 2 (as expected).

If however, inner was asynchronous, e.g

 function outer(){
     for (var i=0;i<3;i++)
        setTimeout(function(){console.log(i)},0);
}

Then you would get 3, 3, 3 as the output! This is because the loop has already finished, including the final i++.

So now I think you can see the problem with your code. Upto calling jQuery.get you have access to the current value of icon_name, but once we are inside that asynchronous callback, the current value disappears and is replaced by the last value for it, because the loop already completed before any of the callbacks were executed.

Try something like this:

var svgIcons = {} 
var props = ["arrow_left","arrow_right"];

this.getIcons = function() {

        props.forEach(function(prop){

               var url=PHP.plugin_url+'/includes/icons/'+prop+'.svg';

               jQuery.get(url, function(data) { 

                       svgIcons[prop]=data;

                       var fullyLoaded = false;

                       for(var i=0;i<props.length;i++) {
                           if(!svgIcons.hasOwnProperty(props[i])){
                               fullyLoaded = false;
                               break;
                            }
                            else fullyLoaded = true;
                        } // end for loop

                        if(fullyLoaded) 
                            callMyFunctionWhereIUseSvgIconsData(); 

               }); //end jQuery.get()

        });//end forEach
}
this.getIcons()

This uses the forEach method, which is native to arrays (MDN reference). Inside the function passed to forEach, the first argument is always the current element of the array (which I named as prop). So there is no messy loop or i, and every executing function has access to its own prop property.

Then, inside the AJAX callback, I assign the current prop to the data received, and then loop through all the properties to check if the svgIcons object has received the properties. So fullyLoaded will only evaluate to true once all the callbacks have been executed and the global svgIcons has received all the properties and data. Hence, you can now call the function that uses the object.

Hope this helps, feel free to ask further or let me know if the console throws errors.

Community
  • 1
  • 1
Sidd
  • 1,389
  • 7
  • 17
  • Does it mean that the function can end before the callbacks are executed? Because it's not the only problem I have. The function doesn't change the json object at all. After the callbacks are executed, I view svgIcons from different function, and it's values are still empty, unchanged. – Kox Apr 20 '15 at 10:23
  • Thank you. I kinda understand it now. But what I don't get is why inside the callback I can change global variables or I can even change the object like svgIcons={ "newval":"new" }, but svgIcons[icon_name] didn't work. I think I will just use PHP to get the svg and put it into the script. – Kox Apr 20 '15 at 11:24
  • That's because of how closures work in Javascript. A function always has access to its _execution context_ (the context in which it was called), no matter when it runs. So your callback function has access to variables declared inside itself, inside _this.getIcons_ as well as globally. That's called the function's scope chain. If you don't want it to have access to something outside, you can redeclare it inside using _var_. So if you said something like var svgIcons; inside the callback, it would no longer be able to access the outer variable. – Sidd Apr 20 '15 at 11:29
  • I don't know if you understood me correctly. I mean inside the callback when I do svgIcons[icon_name]=data; it doesn't work, global variable is unchanged, and I didn't 'var' it inside the function, it should be still the global object key. But if I do svgIcons={ "arrow_left": "changed value" }; it does change the global object. So it's like I can't change key values, but I can change the whole object. – Kox Apr 20 '15 at 11:36
  • Ah I see. Let me update my answer. You are dealing here with the trickiest /most confusing part of JS, and it's no wonder you are frustrated. Gimme a sec. – Sidd Apr 20 '15 at 11:37
  • Edited again, @Kox, let me know if the sample I posted works. And welcome to Javascript :) – Sidd Apr 20 '15 at 12:14
  • Thank you very much for taking your time to help me! It works. You explained it very well. – Kox Apr 20 '15 at 13:05
0

Any ajax calls are async therefore it can be run while the ajax call is taking place. If you want to call something after all calls are done then try this:

var svgIcons = { "arrow_left": "", "arrow_right":"",  } //json object with empty values
var executing = 0;
this.getIcons = function() {

    for (var icon_name in svgIcons) {
        //store that this call has started
        exectuing = executing + 1;
        if (svgIcons.hasOwnProperty(icon_name)) {
            var url=PHP.plugin_url+'/includes/icons/'+icon_name+'.svg';
            console.log('this will run as you were expecting'); 

            //this ajax call is then started and moves to next iteration
            jQuery.get(url, function(data) {
                //This is run after the ajax call has returned a response, not in the order of the code
                svgIcons[icon_name]=data;

                console.log('iterating');
                console.log(svgIcons[icon_name]); //outputs svg
                //if you want to call a function after evey call is comeplete then ignore the 'executing' part and just call the function here.
                //decrement value as this call has finished
                executing = executing - 1;

                //if all have finished then call the function we want
                if(executing === 0){
                    executeAfter();
                }
            });
        }
    }

console.log('this should be after iteration');
console.log(svgIcons["arrow_left"]); //empty

}

this.executeAfter(){
    //This will be exectued after all of you ajax calls are complete.
}
this.getIcons(); //called at object initialization
Dhunt
  • 1,584
  • 9
  • 22