I've read tens of answers related to callbacks, promises and other ways to control flow, but I can't still wrap my head around this task, obviously due to my lack of competence.
I have a nested problem:
- In
test_1()
(and the other functions) I would like to ensure that the rows are added to the table according to the order in which the elements are in the object;
- I would like to execute either test_2 or test_3 (or both after each other) only after test_1 has finished completely. Actually the right sequence will only be known at runtime (there will be a switch with the possible sequences, like 1,2,3 or 1,3,2 or 1,2,1,3 or 1,3,3,2, etc...)
Code:
$(function () {
// create table
tbl = document.createElement('table');
tbl.className = "mainTbl";
$("body").append(tbl);
});
function test_1() {
$.each(obj, function () {
var img = new Image();
img.onload = function () {
// add row of data to table
var row = tbl.insertRow(-1);
var c1 = row.insertCell(0);
c1.innerHTML = "loaded";
};
img.onerror = function () {
// add row of data to table
var row = tbl.insertRow(-1);
var c1 = row.insertCell(0);
c1.innerHTML = "not loaded";
};
img.src = this.url;
});
}
function test_2() {
$.each(obj, function () {
var img = new Image();
img.onload = function () {
// add row of data to table
var row = tbl.insertRow(-1);
var c1 = row.insertCell(0);
c1.innerHTML = "loaded";
};
img.onerror = function () {
// add row of data to table
var row = tbl.insertRow(-1);
var c1 = row.insertCell(0);
c1.innerHTML = "not loaded";
};
img.src = this.url;
});
}
function test_3() {
$.each(obj, function () {
var img = new Image();
img.onload = function () {
// add row of data to table
var row = tbl.insertRow(-1);
var c1 = row.insertCell(0);
c1.innerHTML = "loaded";
};
img.onerror = function () {
// add row of data to table
var row = tbl.insertRow(-1);
var c1 = row.insertCell(0);
c1.innerHTML = "not loaded";
};
img.src = this.url;
});
}
I know that calling the functions in sequence doesn't work as they don't wait for each other... I think promises are they way to go but I can't find the right combination and the documentation is way too complex for my skills.
What's the best way to structure the code so that it's executed in the right order?
You've made this question very difficult to answer because your code is so abstract and tons of relevant details are missing such as how you call test_1()
and test_2()
, etc..., what you're doing with the images, what obj
is, etc... I know you tried to simplify things by leaving out detail, but really you just made it too abstract to know how to answer or what problem you're really trying to solve.
In JS, you can't call something and tell it to wait until something else is done. You can tell test_2()
when test_1()
is done. Or, you can register a sequence of functions and any time any test_n()
is done, it can call the next function in the sequence. Or, you could switch to using promises and use the promise functionality for scheduling async things to run in sequence.
In the spirit of your abstract discussion, here's an abstract way of sequencing things. I've made two main changes:
The rows and cells are added synchronously as you iterate the obj
so that guarantees that they are added in the proper order.
The separate functions are scheduled and test_2()
isn't called until all functions scheduled before it have finished. This is orchestrated without calling specific function names by creating a sequenced list of functions and then having each function use seq.increment()
and seq.decrement()
to let the sequencer keep track of when it's done so the next function can be called.
My guess is that an actual implementation doesn't have to be this generic and a more specific solution could be simpler, but since you've kept the discussion very abstract, this is a specific way to get the rows inserted in order and an abstract way to make sure the functions are called in order and test_2()
isn't called until all the images have finished from test_1()
.
// an object to maintain a list of functions that can be called in sequence
// and to manage a completion count for each one
function Sequencer() {
this.list = [];
this.cnt = 0;
}
Sequencer.prototype.add = function(/* list of function references here */) {
this.list.push.apply(this.list, arguments);
}
Sequencer.prototype.next = function() {
var fn = this.list.shift();
if (fn) {
fn(this);
}
}
Sequencer.prototype.increment = function(n) {
n = n || 1;
this.cnt += n;
}
// when calling .decrement(), if the count gets to zero
// then the next function in the sequence will be called
Sequencer.prototype.decrement = function(n) {
n = n || 1;
this.cnt -= n;
if (this.cnt <= 0) {
this.cnt = 0;
this.next();
}
}
// your actual functions using the sequencer object
function test_1(seq) {
seq.increment();
$.each(obj, function () {
seq.increment();
var img = new Image();
var row = tbl.insertRow(-1);
var c1 = row.insertCell(0);
img.onload = function () {
// add row of data to table
c1.innerHTML = "loaded";
seq.decrement();
};
img.onerror = function () {
// add row of data to table
c1.innerHTML = "not loaded";
seq.decrement();
};
img.src = this.url;
});
seq.decrement();
}
function test_2(seq) {
seq.increment();
$.each(obj, function () {
seq.increment();
var img = new Image();
var row = tbl.insertRow(-1);
var c1 = row.insertCell(0);
img.onload = function () {
// add row of data to table
c1.innerHTML = "loaded";
seq.decrement();
};
img.onerror = function () {
// add row of data to table
c1.innerHTML = "not loaded";
seq.decrement();
};
img.src = this.url;
});
seq.decrement();
}
function test_3(seq) {
seq.increment();
$.each(obj, function () {
seq.increment();
var img = new Image();
var row = tbl.insertRow(-1);
var c1 = row.insertCell(0);
img.onload = function () {
// add row of data to table
c1.innerHTML = "loaded";
seq.decrement();
};
img.onerror = function () {
// add row of data to table
c1.innerHTML = "not loaded";
seq.decrement();
};
img.src = this.url;
});
seq.decrement();
}
// code to run these in sequence
var s = new Sequencer();
// add all the functions to the sequencer
s.add(test_1, test_2, test_3);
// call the first one to initiate the process
s.next();
FYI, I would personally never have code test_1, test_2 and test_3 that looks so nearly identical as I would factor it into a common piece that I could pass arguments to, but you've made this abstract so I don't know how to factor something you haven't provided any differences or specifics on.
Chris, I see you have an answer but there are other ways to do this kind of thing, notably to write your test_
functions such that they return a promise of all images having loaded or not loaded, and to exploit these promises to effect an asynchronous sequence.
Ideally we would have a $.allSettled()
method available to us but as jQuery doesn't have such, we need a workaround. Fortunately the workaround is very simple in this case.
For compactness, I exploit jQuery as much as possible and end up with the following :
$(function () {
var $tbl = $('<table class="mainTbl"/>').appendTo("body");
function test_1() {
//First, use $.map() to build an array of promises from `obj`.
var promises = $.map(obj, function() {
return $.Deferred(function(dfrd) {
var $c1 = $("<td/>").appendTo($("<tr/>").prependTo($tbl));//a concise one-liner to make the required table elements and insert them in the DOM
var img = new Image();
img.onload = dfrd.resolve;//a jQuery Deferred's resolve method is "detachable"
img.onerror = dfrd.reject;//a jQuery Deferred's reject method is "detachable"
img.src = this.url;
}).then(function() {//the workaround for lack of an allSettled method is to return a resolved promise from both the done and fail callbacks of a .then() .
$c1.html("loaded");
return $.when(1);//a resolved promise (resolved with 1, though it's not used)
}, function() {
$c1.html("not loaded");
return $.when(0);//a resolved promise (resolved with 0, though it's not used)
});
});
//Now return a new promise, which will be resolved when all images are "settled" (loaded state or error state).
return $.when.apply(null, promises);
}
function test_2() {
// identical to test_1(), or a variant as required
}
function test_3() {
// identical to test_1(), or a variant as required
}
// Now, build your sequence(s) as array(s). The exact way in which you do this will depend on your application.
var sequence = [test_1, test_2, test_1, test_3];//some arbitrary sequence of functions, each of which, when executed, will return a promise.
// To run the seqnence, you simply exploit `array.reduce()` to build and execute a `.then()` chain, as follows :
sequence.reduce(function(promise, fn) {
return promise.then(function(result) {
return fn();
});
}, $.when());
});
all untested
Since the advent of .reduce()
, this is rapidly becoming the de facto solution to this type of problem and maybe more like what you originally envisaged. That's the essence of it anyway. In practice, the code may well be slightly more complicated, for example :
- if you need to pass paramters to the test_ functions
- if you need to handle errors
Final Note: A javascript object is an orderless collection of properties. If order within your obj
is important, then use an array of elements, not an object of properties.