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.
Specification
Here are the User Stories for the "wait module".
- Condition and Success Action
I want to execute a success function as soon as a condition function returns true. Because I do not know at what time the condition function will return true (it might e.g. check the existence of a SCRIPT element), it must be called periodically by the browser until it returns true. Then, and only then, the success function must be called. - Time Parameters
I want to define a maximum time for evaluating the condition, and I want to specify how frequently the evaluation is tried, both as millisecond durations. The software must check that the evaluation interval is not bigger than the maximum wait time. When I pass zero as maximum, or a negative time amount, I want to wait unlimited for the condition (page lifetime). - More Conditions
Maybe there will be several condition functions. For success, either all of them must be true at the same time, or each of them must have been true at least once before the evaluation expires, this must be configurable. - Given-Up Action
Additionally to the condition and success functions, optionally I want to pass a givenUp function to be called when the condition never got true until the evaluation expires. - Progress Action
Further I optionally want to pass a progress function that should be called on each evaluation attempt. This could be useful for displaying the elapsed time, or to animate some activity indicator. Additionally this function should have the opportunity to cancel the waiting, by returning false instead of true.
Design
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.
Implementation
There are some things I want to have done properly:
- default value definitions must be once-and-only-once
- assert that
- required parameters are really given, and their values are in the allowed range, throw an exception when not
- a started
waitInstance
is not started again before its time expired
- avoid code duplication: the processing of a single condition must forward to the processing of several conditions
Generally 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.
Default Value Definitions
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!
Parameter Assertions
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.
Conditions Aggregation
Now I want to wrap the (possibly several) condition functions into one function that tests them all in two different ways:
- either all of them must have been true at least once before wait time expires
- or all of them must be true at the same evaluation attempt.
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:
- The first variant is achieved when
the
conditionChecks
array has been defined (is notundefined
). - The second variant will take place when it is
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.
Start to Wait
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.
Wait
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
.
Test
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.
Maximum wait: Check interval: ms All conditions must be true at same time
Elapsed: .... of .... ms
Full Source
You can always go to my homepage to visit the current state of this project.
Keine Kommentare:
Kommentar veröffentlichen