javascript callback issue - javascript

I don't really understand how callbacks work. Here is the code (line 49-53 is where the problem is, I want n to be equal to the number of images in my directory): http://pastebin.com/HbALuzGE. I expect onerror callback to happen immediately (as play() calls change() which uses my n var), but that's not the case, yet I don't know how to correct it.

Think of callbacks as a method or way to run functions asynchronously. Normally implementing a callback consist in passing a function as an argument or parameter to an existing function. If specified, this second function will be invoked once as the condition imposed has been met. This is useful in case we want to make sure that the execution context of the first function has been executed and only then pass over to the second function. This way we will make sure, that the second function won't be accessed only when the first function has been fully executed.
The best examples using a callbacks are the server requests, and time animations, where it's absolutely vital to have a certainty that the methods executed inside the functions have been completely satisfied. Here is a very simple example of using callbacks:
function start(num1, num2, callback) {
console.log ("Number 1 is: " + num1 + " and Number 2 is : " + num2);
sum = num1 + num2;
if (callback) {
callback(sum);
}
}
start(8, 2, function(sum) { console.log ("Sum is: " + sum); });
I think you should simplify your code. Maybe i'm wrong, but i think the problem is in the last function when you are defining an image counter i, but when you are passing to onload function you are creating another execution context / closure.
EDIT
Analyzing your code i came to the following solution. You can loop through the images within your image assets with something like this:
var images = [...];
for (var i = 0; i< images.length; i++) {
var img[i] = new Image();
img[i].onload = (function(img) {
return function () {
// do something here
}
img[i].src = images[i];
})(i);
}
...or another way:
function loadImages(images, callback) {
var loadedImages = 0;
for(var i=0; i< images.length; i++) {
images[i] = new Image();
images[i].onload = function() {
if(++loadedImages >= images.length) {
callback(images);
}
};
images[i].src = images[i];
}
}
window.onload = function(images) {
var sources = [...]
loadImages(sources, function(images) {
// do something
});
};
In this case the returning function is important, because this way you will delegate the execution to another function. Hope this help, but i have to admin it was not quite clear what you are trying to do.

Related

How can I make a for loop in Javascript that will set timeouts from an array?

Background (You might want to skip this)
I'm working on a web app that animates the articulation of English phonemes, while playing the sound. It's based on the Interactive Sagittal Section by Daniel Currie Hall, and a first attempt can be found here.
For the next version, I want each phoneme to have it's own animation timings, which are defined in an array, which in turn, is included in an object variable.
For the sake of simplicity for this post, I have moved the timing array variable from the object into the function.
Problem
I set up a for loop that I thought would reference the index i and array t to set the milliseconds for each setTimeout.
function animateSam() {
var t = [0, 1000, 2000, 3000, 4000];
var key = "key_0";
for (var i = 0; i < t.length; i++) {
setTimeout(function() {
console.log(i);
key = "key_" + i.toString();
console.log(key);
//do stuff here
}, t[i]);
}
}
animateSam()
However, it seems the milliseconds are set by whatever i happens to be when the function gets to the top of the stack.
Question: Is there a reliable way to set the milliseconds from the array?
The for ends before the setTimeout function has finished, so you have to set the timeout inside a closure:
function animateSam(phoneme) {
var t = [0,1000,2000,3000,4000];
for (var i = 0; i < t.length; i++) {
(function(index) {
setTimeout(function() {
alert (index);
key = "key_" + index.toString();
alert (key);
//do stuff here
}, t[index]);
})(i);
}
}
Here you have the explanation of why is this happening:
https://hackernoon.com/how-to-use-javascript-closures-with-confidence-85cd1f841a6b
The for loop will loop all elements before the first setTimeout is triggered because of its asynchronous nature. By the time your loop runs, i will be equal to 5. Therefore, you get the same output five times.
You could use a method from the Array class, for example .forEach:
This ensures that the function is enclosed.
[0, 1000, 2000, 3000, 4000].forEach((t, i) => {
setTimeout(function() {
console.log(i);
console.log(`key_${i}`);
//do stuff here
}, t)
});
Side note: I would advise you not to use alert while working/debugging as it is honestly quite confusing and annoying to work with. Best is to use a simple console.log.
Some more clarifications on the code:
.forEach takes in as primary argument the callback function to run on each of element. This callback can itself take two arguments (in our previous code t was the current element's value and i the current element's index in the array):
Array.forEach(function(value, index) {
});
But you can use the arrow function syntax, instead of defining the callback with function(e,i) { ... } you define it with: (e,i) => { ... }. That's all! Then the code will look like:
Array.forEach((value,index) => {
});
This syntax is a shorter way of defining your callback. There are some differences though.
I would suggest using a function closure as follows:
function animateSam(phoneme) {
var t = [0,1000,2000,3000,4000];
var handleAnimation = function (idx) {
return function() {
alert(idx);
key = "key_" + idx.toString();
alert(key);
//do stuff here
};
}
for (var i = 0; i < t.length; i++) {
setTimeout(handleAnimation(i), t[i]);
}
}
I this example you wrap the actual function in a wrapper function which captures the variable and passes on the value.

$.get in a loop: why the function is performed after incrementing?

I am a beginner in Javascript and I feel that there is something wrong with me about the $.get jQuery.
Normally, you can assign it to a function that will execute after the data is retrieved correctly.
But if I put my $.get in a loop, the loop continues to execute even if the data is not yet retrieved, and here is my problem.
Here is my code (this is for GreaseMonkey):
var1 = document.getElementsByClassName("some_class");
i = 0;
while (i < var1.length) {
url = var1[i].getElementsByTagName("some_tag")[0].href;
$.get(url, function(data) {
if (data.contains("some_string")) {
alert(i);
}
});
i++;
}
Here, the alert returns var1.length event if it should returns 1 for exemple.
I try to put an alert(i) just after the url declaration and I understood that i++ was done before the function in my $.get.
This is surely a trivial problem, but I can not grasp the logic to not make this happen.
Wrap your $.get function thus:
(function(i) {
$.get(url, function(data) {
if (data.contains("some_string")) {
alert(i);
}
});
})(i);
The immediately invoked function expression causes the current value of i that's in the outer scope to be bound via the function's parameter i (which then hides the outer variable). If you like, give the function parameter a different name.
Note that this only fixes the problem you actually stated, which is that the loop variable is incremented independently of the callbacks. If you wish to ensure that the AJAX requests run one at a time then there are other solutions, e.g.:
var els = document.getElementsByClassName("some_class");
var i = 0;
(function loop() {
if (i < els.length) {
var el = els[i];
var url = el.getElementsByTagName("some_tag")[0].href;
$.get(url).done(function(data) {
if (data.contains("some_string")) {
alert(i);
}
i++;
}, loop); // .done(f1, f2) - see below
}
})();
The .done() call is in the form .done(callback, loop) and the two functions will be called in order. So the i++ line always happens first, and then it arranges for loop to be called pseudo-recursively to process the next element.
Since you're using jQuery, you can simplify your code quite a bit:
$('.some_class').each( function( i, element ) {
var url = $(element).find('some_tag')[0].href;
$.get( url, function( data ) {
if( data.contains("some_string") ) {
alert( i );
}
});
});
Changes from the original code are:
jQuery calls instead of the getElementsBy* functions.
jQuery .each() for the loop.
Added missing var where needed. (Very important in any version of the code!)
Note that the use of .each() automatically gives you the same effect as the immediately invoked function expression (IIFE) in another answer, but without the extra complication. That's because .each() always uses a callback function, and that creates the closure needed to preserve the i variable (and element too) uniquely for each iteration of the loop.
You can also do this when you have an ordinary while or for loop, and you still don't need the IIFE. Instead, simply call a function in the loop. Written this way, the code would be:
var $elements = $('.some_class');
for( var i = 0; i < $elements.length; i++ ) {
checkElement( i, $elements[i] );
}
function checkElement( i, element ) {
var url = $(element).find('some_tag')[0].href;
$.get( url, function( data ) {
if( data.contains("some_string") ) {
alert( i );
}
});
}
As you can see, the checkElement function is identical to the .each() callback function. In fact, .each() simply runs a similar for loop for you and calls the callback in exactly the same way as this code. Also, the for loop is more readable than the while loop because it puts all the loop variable manipulation in one place. (If you're not familiar with the for loop syntax it may seem less readable at first, but once you get used to it you will probably find that you prefer the for loop.)
In general, when tempted to use an IIFE in the middle of a loop, try breaking that code out into a completely separate function instead. In many cases it leads to more readable code.
Here's a little demo for you to investigate further.
$("#output").empty();
var startTime = new Date().getTime();
// try experimenting with async = true/false and the delay
// don't set async to false with too big a delay,
// and too high a count,
// or you could hang your browser for a while!
// When async==false, you will see each callback respond in order, followed by "Loop finished".
// When async==true, you could see anything, in any order.
var async = true;
var delay = 1;
var count = 5;
function createClosure(i) {
// return a function that can 'see' i.
// and i's remains pinned within this closure
return function (resp) {
var duration = new Date().getTime() - startTime;
$("#output").append("\n" + i + " returned: " + resp + " after " + duration + "ms");
};
}
for (var i = 0; i < count; i++) {
// jsfiddle url and params
var url = "/echo/html/";
var data = {
html: "hello " + i,
delay: delay
};
$.ajax(url, {
type: "post",
data: data,
async: async
}).then(createClosure(i));
}
var duration = new Date().getTime() - startTime;
$("#output").append("\n" + "Loop finished after " + duration + "ms");
Sample async=true output:
Loop finished after 7ms
0 returned: hello 0 after 1114ms
1 returned: hello 1 after 1196ms
2 returned: hello 2 after 1199ms
4 returned: hello 4 after 1223ms
3 returned: hello 3 after 1225ms
Sample async=false output (and the browser hangs for 5558ms!):
0 returned: hello 0 after 1113ms
1 returned: hello 1 after 2224ms
2 returned: hello 2 after 3329ms
3 returned: hello 3 after 4444ms
4 returned: hello 4 after 5558ms
Loop finished after 5558ms

Understanding closures: Constructing a meta-function that queues functions together

In terms of solving the problem, I have a fully working solution that I just finished here:
// synchronous dynamic script loading.
// takes an array of js url's to be loaded in that specific order.
// assembles an array of functions that are referenced more directly rather than
// using only nested closures. I couldn't get it going with the closures and gave up on it.
function js_load(resources, cb_done) {
var cb_list = []; // this is not space optimal but nobody gives a damn
array_each(resources, function(r, i) {
cb_list[i] = function() {
var x = document.body.appendChild(document.createElement('script'));
x.src = r;
console.log("loading "+r);
x.onload = function() {
console.log("js_load: loaded "+r);
if (i === resources.length-1) {
cb_done();
} else {
cb_list[i+1]();
}
};
};
});
cb_list[0]();
}
I am completely happy with this because it does what I want now, and is probably far easier to debug than what my first approach, if it had succeeded, would have been.
But what i can't get over is why I could never get it to work.
It looked something like this.
function js_load(resources, cb_done) {
var cur_cont = cb_done;
// So this is an iterative approach that makes a nested "function stack" where
// the inner functions are hidden inside the closures.
array_each_reverse(resources, function(r) {
// the stack of callbacks must be assembled in reverse order
var tmp_f = function() {
var x = document.body.appendChild(document.createElement('script'));
x.src = r;
console.log("loading "+r);
x.onload = function() { console.log("js_load: loaded "+r); cur_cont(); }; // TODO: get rid of this function creation once we know it works right
};
cur_cont = tmp_f; // Trying here to not make the function recursive. We're generating a closure with it inside. Doesn't seem to have worked :(
});
cur_cont();
}
It kept trying to call itself in an infinite loop, among other strange things, and it's really hard to identify which function a function is and what a function contains within it, during debugging.
I did not dig into the code, but it appears that jQuery.queue has also implemented a similar mechanism to my working one (using an array to track the queue of continuations) rather than using only closures.
My question is this: Is it possible to build a Javascript function that can take a function as argument, and enhance it with a list of other functions, by building closures that wrap functions it creates itself?
This is really hard to describe. But I'm sure somebody has a proper theory-backed mathematical term for it.
P.S. Referenced by the code above are these routines
// iterates through array (which as you know is a hash), via a for loop over integers
// f receives args (value, index)
function array_each(arr, f) {
var l = arr.length; // will die if you modify the array in the loop function. BEWARE
for (var i=0; i<l; ++i) {
f(arr[i], i);
}
}
function array_each_reverse(arr, f) {
var l = arr.length; // will die if you modify the array in the loop function. BEWARE
for (var i=l-1; i>=0; --i) {
f(arr[i], i);
}
}
The problem is how you were setting the value of cur_cont for every new function you made, and calling cur_cont in the onload callback. When you make a closure like tmp_f, any free variables like cur_cont are not 'frozen' to their current values. If cur_cont is changed at all, any reference to it from within tmp_f will refer to the new, updated value. As you are constantly changing cur_cont to be the new tmp_f function you have just made, the reference to the other functions are lost. Then, when cur_cont is executed and finishes, cur_cont is called again. This is exactly the same function that had just finished executing - hence the infinite loop!
In this sort of situation, where you need to keep the value of a free variable inside a closure, the easiest thing to do is to make a new function and call that with the value you want to keep. By calling this new function, a new variable is created just for that run, which will keep the value you need.
function js_load(resources, cb_done) {
var cur_cont = cb_done;
array_each_reverse(resources, function(r) {
// the stack of callbacks must be assembled in reverse order
// Make a new function, and pass the current value of the `cur_cont`
// variable to it, so we have the correct value in later executions.
// Within this function, use `done` instead of `cur_cont`;
cur_cont = (function(done) {
// Make a new function that calls `done` when it is finished, and return it.
// This function will become the new `cur_cont`.
return function() {
var x = document.body.appendChild(document.createElement('script'));
x.src = r;
console.log("loading "+r);
x.onload = function() {
console.log("js_load: loaded "+r);
done();
};
};
})(cur_cont);
});
// Start executing the function chain
cur_cont();
}
EDIT: Actually, this can be made even simpler by using the Array.reduce function. Conceptually, you are taking an array and producing a single function from that array, and each successive function generated should be dependant upon the last function generated. This is the problem that reduce was designed to help solve:
function js_load(resources, done) {
var queue = resources.reduceRight(function(done, r) {
return function() {
var x = document.body.appendChild(document.createElement('script'));
x.src = r;
console.log("loading "+r);
x.onload = function() {
console.log("js_load: loaded "+r);
done();
};
};
}, done);
queue();
};
Note that reduce and reduceRight are not available for older browsers (<= IE8). A JavaScript implementation can be found on the MDN page.

Passing parameters into a closure for setTimeout

I've run into an issue where my app lives in an iframe and it's being called from an external domain. IE9 won't fire the load event when the iframe loads properly so I think I'm stuck using setTimeout to poll the page.
Anyway, I want to see what duration is generally needed for my setTimeout to complete, so I wanted to be able to log the delay the setTimeout fires from my callback, but I'm not sure how to pass that context into it so I can log it.
App.readyIE9 = function() {
var timings = [1,250,500,750,1000,1500,2000,3000];
for(var i = 0; i < timings.length; i++) {
var func = function() {
if(App.ready_loaded) return;
console.log(timings[i]);
App.readyCallBack();
};
setTimeout(func,timings[i]);
}
};
I keep getting LOG: undefined in IE9's console.
What's the proper method to accomplish this?
Thanks
This is happening because you are not closing around the value of i in your func. When the loop is done, i is 8 (timings.length), which doesn't exist in the array.
You need to do something like this:
App.readyIE9 = function() {
var timings = [1,250,500,750,1000,1500,2000,3000];
for(var i = 0; i < timings.length; i++) {
var func = function(x) {
return function(){
if(App.ready_loaded) return;
console.log(timings[x]);
App.readyCallBack();
};
};
setTimeout(func(i),timings[i]);
}
};
When your function gets called by setTimeout sometime in the future, the value of i has already been incremented to the end of it's range by the for loop so console.log(timings[i]); reports undefined.
To use i in that function, you need to capture it in a function closure. There are several ways to do that. I would suggest using a self-executing function to capture the value of i like this:
App.readyIE9 = function() {
var timings = [1,250,500,750,1000,1500,2000,3000];
for(var i = 0; i < timings.length; i++) {
(function(index) {
setTimeout(function() {
if(App.ready_loaded) return;
console.log(timings[index]);
App.readyCallBack();
}, timings[index]);
})(i);
}
};
As a bit of explanation for who this works: i is passed to the self-executing function as the first argument to that function. That first argument is named index and gets frozen with each invocation of the self-executing function so the for loop won't cause it to change before the setTimeout callback is executed. So, referencing index inside of the self-executing function will get the correct value of the array index for each setTimeout callback.
This is a usual problem when you work with setTimeout or setInterval callbacks. You should pass the i value to the function:
var timings = [1, 250, 500, 750, 1000, 1500, 2000, 3000],
func = function(i) {
return function() {
console.log(timings[i]);
};
};
for (var i = 0, len = timings.length; i < len; i++) {
setTimeout(func(i), timings[i]);
}
DEMO: http://jsfiddle.net/r56wu8es/

How to pass variable to anonymous function

I want to pass variable setTimeoutfunction and do something with that. When I alert value of i it shows me numbers that i did not expected. What i m doing wrong? I want log values from 1 till 8.
var end=8;
for (var i = 1; i < end; i ++) {
setTimeout(function (i) {
console.log(i);
}, 800);
}
The standard way to solve this is to use a factory function:
var end=8;
for (var i = 1; i < end; i ++) {
setTimeout(makeResponder(i), 800);
}
function makeResponder(index) {
return function () {
console.log(index);
};
}
Live example | source
There, we call makeResponder in the loop, and it returns a function that closes over the argument passed into it (index) rather than the i variable. (Which is important. If you just removed the i argument from your anonymous function, your code would partially work, but all of the functions would see the value of i as of when they ran, not when they were initially scheduled; in your example, they'd all see 8.)
Update From your comments below:
...will it be correct if i call it in that way setTimeout(makeResponder(i),i*800);?
Yes, if your goal is to have each call occur roughly 800ms later than the last one, that will work:
Live example | source
I tried setTimeout(makeResponder(i),setInterval(i));function setInterval(index) { console.log(index*800); return index*800; } but it's not work properly
You don't use setInterval that way, and probably don't want to use it for this at all.
Further update: You've said below:
I need first iteration print 8 delay 8 sec, second iteration print 7 delay 7 sec ........print 2 delay 2 sec ...print 0 delay 0 sec.
You just apply the principles above again, using a second timeout:
var end=8;
for (var i = 1; i < end; i ++) {
setTimeout(makeResponder(i), i * 800);
}
function makeResponder(index) {
return function () {
var thisStart = new Date();
console.log("index = " + index + ", first function triggered");
setTimeout(function() {
console.log("index = " +
index +
", second function triggered after a further " +
(new Date() - thisStart) +
"ms delay");
}, index * 1000);
};
}
Live example | source
I think you now have all the tools you need to take this forward.
Your problem is that you are referring to the variable i some time later when your setTimeout() function fires and by then, the value of i has changed (it's gone to the end of the for loop. To keep each setTimeout with it's appropriate value of i, you have to capture that value i separately for each setTimeout() callback.
The previous answer using a factory function does that just fine, but I find self executing functions a little easier than factory functions to type and follow, but both can work because both capture the variables you want in a closure so you can reference their static value in the setTimeout callback.
Here's how a self executing function would work to solve this problem:
var end=8;
for (var i = 1; i < end; i ++) {
(function (index) {
setTimeout(function() {
console.log(index);
}, 800);
})(i);
}
To set the timeout delay in proportion to the value of i, you would do this:
var end=8;
for (var i = 1; i < end; i ++) {
(function (index) {
setTimeout(function() {
console.log(index);
}, index * 800);
})(i);
}
The self executing function is passed the value of i and the argument inside that function that contains that value is named index so you can refer to index to use the appropriate value.
Using let in ES6
With the ES6 of Javascript (released in 2015), you can use let in your for loop and it will create a new, separate variable for each iteration of the for loop. This is a more "modern" way to solve a problem like this:
const end = 8;
for (let i = 1; i < end; i++) { // use "let" in this line
setTimeout(function() {
console.log(i);
}, 800);
}
The main reason for this to not to work, is because, of the setTimeout which is set to run after 800 and the scope of i.
By the time it executes which the value of i will already have changed. Thus no definitive result could be received. Just like TJ said, the way to work this around is through a handler function.
function handler( var1) {
return function() {
console.log(var1);
}
}
var end = 8;
for (var i = 1; i < end; i++) {
setTimeout(handler(i), 800);
}
Demo
setTimeout accepts variables as additional arguments:
setTimeout(function(a, b, c) {
console.log(a, b, c);
}, 1000, 'a', 'b', 'c');
Source.
EDIT: In your example, the effective value of i will likely be 8, since the function is merely to be called after the loop has finished. You need to pass the current value of i for each call:
var end=8;
for (var i = 1; i < end; i ++) {
setTimeout(function (i) {
console.log(i);
}, 800, i);
}

Categories

Resources