Javascript async loop inside another async loop - javascript

I have some code in jQuery that iterate through children in div using each().
Every text inside is splitted into words. Each word is processed with 'for' loop.
This function can take a long time and can freeze the browser so...
Is there a way to create asynchronous loop inside another asynchronous loop but one is waiting for other to finish?
Could anyone tell me the right direction?
I came up with something like this:
var queue = [];
var nlevel = 0;
function work() {
nlevel = queue.length-1;
var arr = queue[nlevel][0];
var process = queue[nlevel][1];
var cnt = queue[nlevel][2];
var item = arr[cnt];
process.apply(item);
cnt++;
queue[nlevel][2] = cnt;
if (cnt < arr.length) {
setTimeout(work, 1);
} else {
if (queue.length>1) {
queue.pop();
setTimeout(work, 1);
}
}
}
function each(arr, process) {
queue.push([arr, process, 0]);
setTimeout(work, 1);
}
each(['one', 'two', 'three'], function() {
alert(this);
each([1, 2, 3, 4], function() {
alert(this);
});
});
but It has some major bug and I couldn't fix it.

You can use Web Workers to run multiple scripts in background threads. But they are not supported in every browsers. See this article from Mozilla or simple ask Google: https://developer.mozilla.org/En/Using_web_workers

You can use SetTimeout(0,...) periodically to "yield" control to the browser to prevent freezing the browser (but it will not execute any faster, in fact it will probably be slightly slower).
See this answer for an example of the technique, I can't be more specific without seeing your code.

One way you could do this is to create a queue of work items to be processed:
var queue = [];
Place items to be processed in this queue instead of processing right away:
queue.push(item);
Then start a timer loop for processing items:
var delayBetweenItems = 10;
var processItemFromQueue = function() {
if (queue.length) {
item = queue.shift();
doStuff(item);
setTimeout(delayBetweenItems, processItemFromQueue);
}
};
setTimeout(processItemFromQueue, delayBetweenItems);

Assuming that your current code is something similar to this:
function processWord(word, i, j) {
// do something with the current word, where
// (if it's needed) i is the index into the elements
// and j is the index into the current element's words
}
$("#divId").children().each(function(i) {
var words = $(this).text().split(" "),
j;
for(j = 0; j < words.length; j++) {
processWord(words[j], i, j);
}
});
You can rewrite that to do both the outer (i) and inner (j) loops with setTimeout():
// assumes the same processWord() function as above
(function (){
var $items = $("#divId").children(),
words,
i, j,
iMax, jMax;
function doIteration() {
if (j === jMax) {
if (++i === iMax)
return;
// outer loop processing
words = $($items[i]).text().split(" ");
jMax = words.length;
j = 0;
}
if (j < jMax) {
// inner loop processing
processWord(words[j], i, j);
j++;
}
setTimeout(doIteration, 1);
}
iMax = $items.length;
i = j = jMax = -1;
doIteration();
})();
Working demo: http://jsfiddle.net/sp8Wr/
The doIteration() function simulates a nested loop by processing some counters appropriately and calling itself via setTimeout() to do the next iteration. The immediately-executed-anonymous-function wrapping the whole thing is not essential to the process, it is there just to restrict the scope of the variables.
Yes this could probably be done much better, but this is just what I came up with on the fly - obviously you'll modify it as appropriate for your own processing.
(If you don't care what order the words get processed in as long as the processWord() function gets the correct values of i and j associated with each word this can easily be made much tidier, but I don't have time to do a neater version that processes in order.)

Related

Creating a closure for .setTimeout() inside a for loop

I am trying to write a javascript program which stores the value from an input element in an array when a button is clicked. The array is the split and each individual letter added to a span element and then appended to the document. The idea is to create a typing effect using setTimeout.
I am running into an issue creating a closure within the loop, so currently the setTimeout function always returns the final value of the iteration.
The function in question is at the bottom of the code block and called addTextToBoard();
var noteButton = document.querySelector('[data-js="button"]');
noteButton.addEventListener("click",function() {
var messageIn = document.querySelector('[data-js="input"]');
var message = messageIn.value;
postToBoard(message);
});
function postToBoard(val) {
var noteBoard = document.querySelector('[data-js="noteboard"]');
var newElement = document.createElement('div');
newElement.classList.add('noteboard__item');
noteBoard.appendChild(newElement);
setTimeout(function(){
newElement.classList.add('active');
}, 200);
addTextToBoard(newElement, val);
}
function addTextToBoard(el, val) {
var wordArray = val.split('');
for(i = 0; i < wordArray.length; i++) {
var letter = document.createElement('span');
letter.innerHTML = wordArray[i];
setTimeout(function(x){
return function() {}
el.appendChild(letter);
}(i),1000);
}
}
I believe I am close, I'm just not fully understanding the syntax for creating the closure. If someone could give poke in the right direction, without necessarily giving the full solution that would be great.
I essentially tried to paste in the following code snippet from here but I've missed something somehwere along the way!
setTimeout(function(x) { return function() { console.log(x); }; }(i), 1000*i);
Best,
Jack
You are close.
Since the "letter" variable changes, you'll add only the last letter over and over again. You need to "save" the current letter on the setTimeout() callback function, One way to go is like this:
function appendMyLetter(letter) {
return(function() {
el.append.Child(letter);
});
}
function addTextToBoard(el, val) {
var wordArray = val.split('');
for(i = 0; i < wordArray.length; i++) {
var letter = document.createElement('span');
letter.innerHTML = wordArray[i];
setTimeout(appendMyLetter(letter), 1000);
}
}
This way, the appendMyLetter() function gets called with a different parameter (one for each letter) and returns a function with the correct "stored" value to be called by setTimeout().
EDIT
Looking at your setTimeout() code closely
setTimeout(function(x){
return function() {}
el.appendChild(letter);
}(i),1000);
It would work fine, if you used the proper parameters and used the appendChild() inside the returned function, like so:
setTimeout(function(x){
return(function() {
el.appendChild(x);
});
}(letter),1000);
You can create an immediately-invoked function expression IIFE to create a closure
function addTextToBoard(el, val) {
var wordArray = val.split('');
for(i = 0; i < wordArray.length; i++) {
(function(index) {
var letter = document.createElement('span');
letter.innerHTML = wordArray[i];
setTimeout(function(){
el.appendChild(letter);
},1000);
})(i);
}
}
I dont know if this will work but here you go a slight change in operator:
letter.innerHTML += wordArray[i];
if you dont get the effect you imagined you will get you better try to increment the timer by i like this
setTimeout(function(){
...
},1000*i);

setTimeout within 'while' causes a browser crash. How can I avoid it?

I am new to coding and I want to built a Text Adeventure Game with HTML , CSS and Javascript. I want to show many peaces of text from an array with a certain time between each text. I tried different methods to avoid an infinite loop because of setTimeout, but I didn't figure out how to apply it on my code.
Here's my code, that causes the crash:
var iCounterText = 0;
var verzog = function() {
document.getElementById('toggleText').insertAdjacentHTML('beforeBegin', '<br>--------------<br>');
document.getElementById('toggleText').insertAdjacentHTML('beforeBegin', part1[iCounterText]);
iCounterText = iCounterText + 1;
playaudio();
}
function forwardingLinks() {
while (iCounterText < part1.length - 1) {
setTimeout(verzog, 500); // Here is the problem //
}
document.getElementById('buttonLinks').innerHTML = part1[part1.length - 1];
}
The following code works fine, but then there is no timeout between the text:
function forwardingLinks() {
while (iCounterText < part1.length - 1) {
verzog();
}
document.getElementById('buttonLinks').innerHTML = part1[part1.length - 1];
}
Edit:
This is my new code with "setInterval". Problem: Value is added by 1, but the function is not using the part2 Array. Instead it is using part1 Array again, although the partvalue already contains part2 array.
var part1 = [ //Texte und Antworten
'Hallo?',
'Test?',
'What',
'hello',
'--------------',
'Was?'
];
var part2 = [ //Texte und Antworten
'part2 goes on....',
'bla bla',
'blablabla'
];
var iCounterText = 0;
var value = 1;
var partvalue = eval("part" + value);
function forwardingLinks() {
var verzog = setInterval(function(){
if(iCounterText < partvalue.length-2){
++iCounterText;
toggleText.insertAdjacentHTML('beforeBegin', '<br>--------------<br>');
toggleText.insertAdjacentHTML('beforeBegin', partvalue[iCounterText]);
playaudio();
}else{
buttonLinks.innerHTML = partvalue[partvalue.length-1];
++value;
iCounterText=0;
clearInterval(verzog);
}
},500);
}
You are misunderstanding the flow of your while loop. JavaScript runs in a single-threaded environment. That means that your calls to the verzog function won't run until the forwardingLinks function completes, but your forwardingLinks function won't ever complete because you have a while loop that is dependent on a counter that is never increased, because verzog hasn't run yet.
Change your while loop, so that the iCounterText variable gets incremented from within the loop, so the loop can end and then the calls to verzog that have stacked up in the event queue can start to run.
Additionally, since you are using a numeric counter, a regular for loop would be better than an while loop because the loop's step value (++iCounterText) is required:
for(var iCounterText = 0; iCounterText < part1.length; ++iCounterText) {
setTimeout(verzog, 500);
}
As an aside from your main problem, it is very inefficient to repeatedly scan the DOM for the same element over and over, as you are doing in your verzog function. Instead, just get the DOM reference once and store it in a variable that can be reused:
// Declare a variable in a scope that is accessible throughout your code
var toggleText = null, buttonLinks = null;
// Set up a callback that runs after the DOM is ready
window.addEventListener("DOMContentLoaded", function(){
// Scan the DOM for the element(s) you'll be needing
toggleText = document.getElementById('toggleText');
buttonLinks = document.getElementById('buttonLinks');
});
function verzog() {
// Now, you can just refer to the DOM element you've already found:
toggleText .insertAdjacentHTML('beforeBegin', '<br>--------------<br>');
toggleText .insertAdjacentHTML('beforeBegin', part1[iCounterText]);
playaudio();
}
function forwardingLinks() {
for(var iCounterText = 0; iCounterText < part1.length; ++iCounterText) {
setTimeout(verzog, 500);
}
// Now, you can just refer to the DOM element you've already found:
buttonLinks.innerHTML = part1[part1.length - 1];
}

Populating local array from callback: synchronization issues?

Here's the deal:
I populate a local array in a callback like so:
var datasource = datasources[id];
var contexts = [];
datasource.data($selected.parent().data(), function (items) {
var dataarr = items.data;
for (var i = 0; i < dataarr.length; ++i) {
contexts.push(dataarr[i]);
}
});
foo(contexts);
Now, in foo, I run a simple check like:
function foo(contexts) {
if (contexts.length < 2) {
return;
}
}
If I break at the return statement above, contexts.length is in fact, greater than 2. Similarly, if I run the code step by step in a debugger, everything works as expected.
This make me suspect that when not running in a debugger, this code is being executed before the callback has completed.
Fine. But how can I control the execution order? Or, perhaps, is there a better paradigm to go about this if the only way I know can acquire items.data is from within that callback?
Thanks!
I'm not sure about the context here a little bit, but if you want your method foo to be called after the array has been populated, why not call it after the for loop? like so:
datasource.data($selected.parent().data(), function (items) {
var dataarr = items.data;
for (var i = 0; i < dataarr.length; ++i) {
contexts.push(dataarr[i]);
}
foo(contexts);
});
if you're uncomfortable with that, look at PubSubJS which allows you to emit events and handle them asynchronously. I wrote an article about it's usage here.
Edit: An example:
datasource.data($selected.parent().data(), function (items) {
var dataarr = items.data;
for (var i = 0; i < dataarr.length; ++i) {
contexts.push(dataarr[i]);
}
PubSub.publish('contextsPopulated', contexts);
});
and then modify foo as:
function foo(message, contexts) {
if (contexts.length < 2) {
return;
}
}
now register foo to be called whenever 'contextsPopulated' is signalled.
PubSub.subscribe('contextsPopulated', foo);

Cannot access array elements properly within setTimeout

Here what I'm trying to do.
I'm having an array like the following
var my_array = ['1', '2', '3' ... ,'1000000000000000'];
What I want to do is create a bunch of HTML elements for every element of that array, and since the array can contain a huge number of elements I attempted to do the following so the browser won't freeze.
for(var i in my_array)
{
if(my_array.hasOwnProperty(i))
{
setTimeout(function(){
do_something_with_data(my_array[i]);
});
}
}
What happens though is that the my_array[i] within the setTimeout doesn't have the value it should.
To be more accurate, when I try to console.log(my_array[i]) what I get is something like this:
"getUnique" function (){
var u = {}, a = [];
for(var i = 0, l = this.length; i < l; ++i){
if(u.hasOwnProperty(this[i])) {
continue;
}
a.push(this[i]);
u[this[i]] = 1;
}
return a;
}
getUnique is a function I've added to the Array prototype just like this:
Array.prototype.getUnique = function(){
var u = {}, a = [];
for(var i = 0, l = this.length; i < l; ++i){
if(u.hasOwnProperty(this[i])) {
continue;
}
a.push(this[i]);
u[this[i]] = 1;
}
return a;
};
Can please somebody help me with this issue?
the setTimeout is executed after the loop is done, and i is the last key or some garbage value at that point. You can capture the i like so:
for (var i in my_array) {
if (my_array.hasOwnProperty(i)) {
(function(capturedI) {
setTimeout(function() {
do_something_with_data(my_array[capturedI]);
});
})(i);
}
}
You should also not use for..in loops for arrays because it's an order of magnitude slower (especially so with the .hasOwnProperty check) than a for loop and the iteration order is not defined
If you have jQuery or willing to add some extra code for older browsers, you can do:
my_array.forEach( function( item ) {
setTimeout( function() {
do_something_with_data( item );
}, 1000);
});
With jQuery:
$.each( my_array, function( index, item ) {
setTimeout( function() {
do_something_with_data( item );
}, 1000);
});
See docs for [].forEach
The problem is that the functions you're creating have a reference to the i variable, not a copy of its value, and so when they run they see i as it is at that point in time (past the end of the array, presumably). (More: Closures are not complicated)
I'd recommend a completely different approach (below), but first, let's look at how to make your existing approach work.
To do what you were trying to do, with the for loop, you have to have the functions close over something that won't change. The usual way to do that is to use a factory function that creates the timeout functions such that they close over the argument to the factory. Or actually, you can pass in the array element's value rather than the index variable.
for(var i in my_array)
{
if(my_array.hasOwnProperty(i))
{
setTimeout(makeFunction(my_array[i]));
}
}
function makeFunction(entry) {
return function(){
do_something_with_data(entry);
};
}
But, I would probably restructure the code so you're not creating masses and masses of function objects unnecessarily. Instead, use one function, and have it close over an index that it increments:
// Assumes `my_array` exists at this point, and that it
// has at least one entry
var i = 0;
setTimeout(tick, 0);
function tick() {
// Process this entry
if (my_array.hasOwnProperty(i)) {
do_something_with_data(my_array[i]);
}
// Move to next
++i;
// If there are any left, schedule the next tick
if (i < my_array.length) {
setTimeout(tick, 0);
}
}
Its just a guess. Try it like:
for(var i in my_array)
{
if(my_array.hasOwnProperty(i))
setTimeout("do_something_with_data('"+my_array[i]+"')", 500);
}

How to use setTimeout / .delay() to wait for typing between characters

I am creating a simple listbox filter that takes the user input and returns the matching results in a listbox via javascript/jquery (roughly 5000+ items in listbox). Here is the code snippet:
var Listbox1 = $('#Listbox1');
var commands = document.getElementById('DatabaseCommandsHidden'); //using js for speed
$('#CommandsFilter').bind('keyup', function() {
Listbox1.children().remove();
for (var i = 0; i < commands.options.length; i++) {
if (commands.options[i].text.toLowerCase().match($(this).val().toLowerCase())) {
Listbox1.append($('<option></option>').val(i).html(commands.options[i].text));
}
}
});
This works pretty well, but slows down somewhat when the 1st/2nd char's are being typed since there are so many items.
I thought a solution I could use would be to add a delay to the textbox that prevents the 'keyup' event from being called until the user stops typing. The problem is, I'm not sure how to do that, or if its even a good idea or not.
Any suggestions/help is greatly appreciated.
You can do a delay like this:
$('#CommandsFilter').keyup(function() {
clearTimeout($.data(this, 'timer'));
var wait = setTimeout(search, 500);
$(this).data('timer', wait);
});
function search() {
var temp = $("<select />");
for (var i = 0; i < commands.options.length; i++) {
if (commands.options[i].text.toLowerCase().match($(this).val().toLowerCase())) {
$('<option></option>', { val: i, html: commands.options[i].text }).appendTo(temp);
}
}
Listbox1.empty().append(temp.children());
}
This stores a timeout on the element you're typing in, if 500ms (adjust as needed) passes between keystrokes, a search executes. Also this appends the elements in a document fragment then into the DOM (still preserving encoding, etc). Depending on the number of items, this may be a decent performance boost as well.
If the commands drop-down isn't changing, I'd suggest the following (note I've dropped jQuery for better performance and compatibility). There are several improvements:
Timer to delay updating the filtered list once half a second has elapsed since the last keypress
List of command texts is pre-cached
Unnecessary use of match replaced with indexOf
Uses fast native DOM manipulation that works in all scriptable browsers since the 1990s
A quick test suggests that for a drop-down with 5000 options containing short strings, it's between 10 and 30 times faster than the jQuery equivalent in most browsers.
Code:
var commands = document.getElementById("DatabaseCommandsHidden");
var filteredDropDown = document.getElementById("Listbox1");
var filterInput = document.getElementById("CommandsFilter");
var timer;
// Create a cached list of the lower case text of the commands drop-down
var commandTexts = [], commandText;
for (var i = 0, len = commands.options.length; i < len; ++i) {
commandText = commands.options[i].text;
commandTexts.push({original: commandText, lower: commandText.toLowerCase()});
}
function populateFilteredDropDown() {
timer = null;
var val = filterInput.value.toLowerCase(), commandText;
var opts = filteredDropDown.options;
filteredDropDown.length = 0;
for (var i = 0, len = commandTexts.length; i < len; ++i) {
commandText = commandTexts[i];
if (commandText.lower.indexOf(val) > -1) {
opts[opts.length] = new Option(commandText.original);
}
}
}
filterInput.onkeyup = function() {
if (timer) {
window.clearTimeout(timer);
}
timer = window.setTimeout(populateFilteredDropDown, 500);
};

Categories

Resources