JavaScript is a single-threaded language. JS can not create a parallel running activity that does one thing while another activity thread does another thing. Which implicates that JS needs no synchronization, and concurrency problems do not occur in a JS environment. We should be happy, one less piece of complexity!
But how do we do when we want to set a CSS-class onto an HTML element, but the element is not present yet?
We can't spawn a thread, but we can use the global predefined JS function setTimeout()
to do that later when the element might already exist. That function accepts a callback function and a millisecond timeout as parameters, and it will call the callback function when the timeout has elapsed.
var timeout = 4000; // 4 seconds
setTimeout(
function() {
alert("The timeout of "+timeout+" ms elapsed!");
},
timeout
);
When you put this JS code into a web page, you will see a dialog after 4 seconds. This will not repeat.
The technique of invoking things later occurs in many UI environments. In Java/Swing it is the SwingUtilities.invokeLater()
method, where the event-queue is used to post a Runnable
instance for later execution.
This Blog develops a JS wait() function that permits asynchronous function execution at a time when some condition becomes true. Similar to Promises.
Here are the User Stories for the "wait module".
Here is the well-documented interface of what will be usable from outside, as JS code.
/**
* Creates a wait object for given millisecond parameters.
* @param maximumWaitMillis optional, default 4000 milliseconds, the time
* after which wait should return unsuccessfully when condition never
* became true.
* @param delayMillis optional, default 100 milliseconds, the time after
* which the condition should be tested again.
* @param allMustBeTrueAtSameTime optional, default false, when true,
* all conditions must be true at same time, it is not enough that
* a condition gets true shortly for a time while others are false.
*/
var waitFactory = function(maximumWaitMillis, delayMillis, allMustBeTrueAtSameTime)
{
var that = {};
....
/**
* Waits for 1-n conditions to become true.
* @param conditions required, an array of functions that all must return
* true for success() to be called.
* @param success required, a function to be executed when all conditions
* returned true.
* @param givenUp optional, a function to be executed when time ran out.
* @param progress optional, a function to be executed any time the
* condition is checked.
*/
that.waitForAll = function(conditions, success, givenUp, progress) {
....
};
/**
* Convenience function to wait for just one condition.
*/
that.waitFor = function(condition, success, givenUp, progress) {
that.waitForAll([ condition ], success, progress, givenUp);
};
return that;
};
And here is the (really important!) example usage.
var idToFind = "idToFind";
waitFactory(4000).waitFor( // wait 4 seconds
function() { return document.getElementById(idToFind) !== undefined; },
function() { alert("The element "+idToFind+" exists!"); }
function() { alert("Sorry, the element "+idToFind+" does not exists!"); }
function() { return document.getElementById("cancelBox").checked; }
);
This will wait 4 seconds for the existence of element with id "idToFind". In case the element is already present, or appears within the given amount of time, an alert-dialog will pop up and tell about it. Else another alert will appear after 4 seconds and tell that waiting time elapsed unsuccessfully. When the user clicks at the checkbox with id "cancelBox", the wait loop will end at the next evaluation attempt.
There are some things I want to have done properly:
waitInstance
is not started again before its time expiredGenerally I try to avoid instance variables, I prefer parameters wherever possible. The more instance vars you have, the more vulnerable your object is to state conflicts. With parameters nothing can go wrong, you have the things that you need at the place where it is needed. This is also nice for refactoring.
Defaults must be assigned before assertions check parameter values.
var waitFactory = function(maximumWaitMillis, delayMillis, allMustBeTrueAtSameTime)
{
if (delayMillis === undefined || delayMillis <= 0)
delayMillis = 100;
if (maximumWaitMillis === undefined)
maximumWaitMillis = 4000;
....
Here time defaults are defined when parameters were not given. The default for allMustBeTrueAtSameTime
is false
, but this needs not to be defined here as long as you do not compare the parameter directly to true
or false
: if ( ! allMustBeTrueAtSameTime )
. This will be true
when allMustBeTrueAtSameTime
is either false
or undefined
.
I let pass through maximumWaitMillis
=== 0 or <= 0, this will permit to wait "forever".
Mind that I do not use the maximumWaitMillis = maximumWaitMillis || 4000;
pattern, because this would also strike when maximumWaitMillis
is zero!
Assertions should reject erroneous calls as early as possible, with an adequate message. If you implement functions that do not check their own preconditions, you implement incalculable risks.
var waitFactory = function(maximumWaitMillis, delayMillis, allMustBeTrueAtSameTime)
{
.... // default definitions
if (delayMillis >= maximumWaitMillis)
throw "Maximum wait time ("+maximumWaitMillis+") must be greater than evaluation delay ("+delayMillis+")!";
....
A delayMillis
evaluation interval that is greater than the maximumWaitMillis
time would prevent success in any case, this makes no sense.
that.waitForAll = function(conditions, success, givenUp, progress) {
if ( ! conditions || ! conditions.length || ! success )
throw "Expected conditions and success function!";
for (var i = 0; i < conditions.length; i++)
if ( ! conditions[i] )
throw "Expected condition at index "+i;
....
Undefined functions in the conditions array do not make sense, so it is checked here. Mind that this does not call the success
function, it just tests whether the function pointer is undefined.
The givenUp
and progress
functions are optional, thus they are not checked.
Now I want to wrap the (possibly several) condition functions into one function that tests them all in two different ways:
var conditionChecks;
that.waitForAll = function(conditions, success, givenUp, progress) {
.... // parameter assertions
var condition = function() {
var result = true;
for (var i = 0; i < conditions.length; i++) {
var condition = conditions[i];
if (conditionChecks) { // when caller defined an array, use it to buffer results
if ( ! conditionChecks[i] && ! (conditionChecks[i] = condition()) )
result = false;
}
else if ( ! condition() ) {
return false;
}
}
return result;
};
startToWait(condition, success, givenUp, progress);
};
The conditions
parameter is an array containing all condition functions. The conditionChecks
variable will be initialized to an array holding returned booleans when allMustBeTrueAtSameTime
is true, or undefined
when not.
Now look at the local condition()
function:
conditionChecks
array has been defined (is not undefined
).undefined
, because then the loop is broken by the return statement whenever one of the conditions is false.Mind that the local condition()
function is not executed here, it is passed to startToWait()
for later usage.
Why is the condition()
function a local inner function? Because that way I don't need to store the conditions
parameter into an instance-variable that.conditions
. As I said, I prefer parameters.
This aggregation works for a single condition as well as for several of them.
var working;
var startToWait = function(condition, success, givenUp, progress) {
if (working)
throw "Can not wait for other conditions while working!";
setWorking(true);
wait(condition, success, givenUp, progress, undefined);
};
var setWorking = function(work) {
working = work;
if ( ! allMustBeTrueAtSameTime )
conditionChecks = [];
};
Here comes the private part of this JS module. The startToWait()
function will not be visible outside, nevertheless it is inseparably connected to all variables and functions within the waitFactory
function, and they all together will survive the call of their factory as a closure, being accessible via the that
object. This is the JS encapsulation mechanism.
The startToWait()
function asserts that the object is idle and not in a timeout loop. Then it sets the new state and delegates to wait()
which does the real work.
The setWorking()
function encapsulates the state. Any function that wants to set the working
variable should call this function. It initializes the conditionChecks
array to a value according to the top-level parameter allMustBeTrueAtSameTime
. When this is true, it will always be undefined
, else it will be set to a new array.
This is the final part that concentrates on setTimeout()
calls.
var wait = function(condition, success, givenUp, progress, alreadyDelayedMillis) {
if (alreadyDelayedMillis === undefined)
alreadyDelayedMillis = 0;
if (progress)
if ( ! progress(alreadyDelayedMillis, maximumWaitMillis) )
return;
if (maximumWaitMillis > 0 && alreadyDelayedMillis >= maximumWaitMillis) {
if (givenUp)
givenUp(maximumWaitMillis);
setWorking(false);
}
else if (condition()) {
success(alreadyDelayedMillis);
setWorking(false);
}
else {
setTimeout(
function() {
wait(condition, success, givenUp, progress, (maximumWaitMillis > 0) ? alreadyDelayedMillis + delayMillis : 0);
},
delayMillis
);
}
};
Look at the alreadyDelayedMillis
parameter. This will contain the already elapsed milliseconds. When you go back to the startToWait()
implementation, you see that I call the wait()
function with undefined
as last parameter, which is exactly alreadyDelayedMillis
. All further recursive calls of wait()
will pass the parameter correctly. The parameter is checked at the start of the function, and initialized to zero when it is undefined. Thus it will count from zero to given maximum time. This again is to avoid an instance variable.
Everything else is "as expected".
First the progress()
function is called when it was given. Then the maximum time is checked, and givenUp()
is called when exceeded, and the working
state is reset to idle. Else the condition()
aggregate is called, and success()
is executed when it returned true, inclusive resetting working
state to idle. In any other case the predefined JS function setTimeout()
is used to schedule another call to wait()
, this time with increased alreadyDelayedMillis
.
You can find the full source at bottom of this page. Next is a manual test page that lets try out all waitFactory()
parameters. Especially the allMustBeTrueAtSameTime
parameter should be tested.
As soon as Start was pressed, a script regularly checks whether the elements "Booh" and "Waah" exist in the document. When yes, the text log below will be green, when not and time runs out, it will be red. You can create and delete the elements by pressing the according buttons.
You can always go to my homepage to visit the current state of this project.
ɔ⃝ Fritz Ritzberger, 2015-06-20