How do I run a nested loop synchronously?


How do I run a nested loop synchronously?



I'm writing a simple script that prints text to the screen one character at a time.



I'm making it so that the function (which I've called slowPrint) can receive an Array of Strings. Each element in the Array represents a message.


slowPrint



This is the code I have so far:



However, I am not getting the expected output.
I suspect this is in part to the asynchronous nature of the code, though I don't have a full and clear understanding of what's happening and how to fix it.



To begin with, the <br /> tags are being printed before any of the messages, which tells me that the outer loop is finishing before the nested one even starts.


<br />



When the nested loops does begin, however, each string in the array is being printed one second apart, but in their entirety rather than character by character.



What am I missing?



Additionally, can someone please explain the following behavior of the setTimeout method?


setTimeout



Scenario 1: When I set the second argument to i * 1000, the second string prints one second after the other (again, entire string rather that char by char)


i * 1000




const messages = [
"all systems are operational",
"you may proceed"
];

function slowPrint(args) {

let screen = document.getElementById('screen');

for (let i = 0; i < args.length; i++) {

let message = args[i];

for (let j = 0; j < message.length; j++) {
setTimeout(function () {
screen.innerHTML += message[j];
}, i * 1000);
}

screen.innerHTML += '<br />';

}

}

slowPrint(messages)




Scenario 2: When I set the second argument to j * 1000, the output is completely unexpected: every second characters print in sets of 2, but in an order that is unintelligible; only the last word of the last arguments prints as everything else should.


j * 1000



Scenario 3: When I set the second argument to just 1000, ALL strings in the array print after one second.


1000



What is happening?





#1. It really should be a combination of i and j....#2. It really should be a combination of i and j.... #3. Well they are all set to the same time.... Seems logical they all would run at the same time
– epascarello
Jun 29 at 16:38






I tried out scenario 1 with console.log and it prints character by character. What u r missing is message.charAt(j). This explicitly selects each character at the index specified by j.
– Abrar Hossain
Jun 29 at 16:45


console.log


message.charAt(j)


j




4 Answers
4



This video is one of the best explanations of how js works in the browser: here



Basically whatever you put inside setTimeout's callback gets put on the backburner for at the number of ms you pass to the second argument. Then it's put in callback queue until the call stack is empty and it's the next item in the queue



If you copy and paste your code into http://latentflip.com/loupe/ you'll see how it actually runs behind the scenes



Using an async function and a helper function called sleep() to wrap your setTimeout() in a Promise and await it, you can accomplish this with minimal changes.


async


sleep()


setTimeout()


Promise


await




const messages = [
'all systems are operational',
'you may proceed'
];

const sleep = ms => new Promise(resolve => { setTimeout(resolve, ms) })

async function slowPrint(args) {
let screen = document.getElementById('screen');

for (let i = 0; i < args.length; i++) {
let message = args[i];

for (let j = 0; j < message.length; j++) {
await sleep(100);
screen.innerHTML += message[j];
}

screen.innerHTML += '<br />';
}
}

slowPrint(messages)




setTimeout()'s callback is performed asynchronously, so the order of execution will always occur like this:


setTimeout()


// first

setTimeout(function () {
// at *least* after all the current synchronous code has completely finished
})

// second



As noted in the comments, async / await is only supported in browsers that implement ECMAScript 2017.


async


await





FYI ^^ not all browsers support async and promises....
– epascarello
Jun 29 at 16:40





@epascarello I find it disappointing that internet explorer is still relevant. Most browsers automatically update to the newest stable release periodically by default, and IE is the only major browser that doesn't implement ES2017.
– Patrick Roberts
Jun 29 at 16:48



You can do this with pretty succinct code just using setInterval. You just need to manage the indexes properly. This code uses i to iterate through each letter and j to iterate through the array. When i hits the limit j is incremented; when j hits the limit, the interval is cleared.


setInterval


i


j


i


j


j




let screen = document.getElementById('screen');
const messages = [
"all systems are operational",
"you may proceed"
];

function slowPrint(args) {
let i=0, j = 0
let ivl = setInterval(() => {
screen.innerHTML += args[j][i]
i++
if (i == args[j].length ){
i = 0;
j++
screen.innerHTML += '<br>'
}
if (j === args.length) clearInterval(ivl)
}, 200)
}
slowPrint(messages)




The reason your code is having problems is that the for loop doesn't stop and wait for the timeout. The for loop lets all the timeouts to start almost simultaneously, so after 1000 ms they all fire. setInterval is normally a better method when you need something to happen periodically.


setInterval



There are, of course, many other ways to do this. Just an example of something a little more exotic, here's a way to do it with a simple generator. It's a little harder to understand, but quite clean looking once you're used to generators:




const out = document.getElementById('screen')
const messages = ["all systems are operational","you may proceed"];

function *iter(messages) {
for(m of messages){
for(letter of m) yield letter
yield '<br>'
}
}

const gen = iter(messages)
const int = setInterval(() => {
let n = gen.next()
if (n.done) return clearInterval(int)
out.innerHTML += n.value
}, 100)




There are tons of ways to do this from using a queue to using math to figure out the ranges. Without modifying your code too much, you can just check to see if you are at the last character and than append a line break and use a variable to keep track of the current time for the output.




const messages = [
"all systems are operational",
"you may proceed"
];

function slowPrint(args) {

let screen = document.getElementById('screen');
let delay = 0;
const timeDelay = 100;
for (let i = 0; i < args.length; i++) {


let message = args[i];

for (let j = 0; j < message.length; j++) {
setTimeout(function () {
let lineBr = j === message.length - 1 ? '<br>' : ''
screen.innerHTML += message[j] + lineBr;
}, delay);
delay += timeDelay
}

}

}

slowPrint(messages)




Personally I would use more of a queue so I do not have tons of timers created.




const messages = [
"all systems are operational",
"you may proceed"
];

function slowPrint(args) {

let screen = document.getElementById('screen');

// combine all the strings into one character array
var characters = messages.reduce( function (a, s) {
// turn string into an array of characters
var letters = s.split('')
// last character, add a line break
var l = letters.length-1
letters[l] = letters[l] + '<br/>'
// append it to our current list
return a.concat(letters);
}, );

function next() {
// append the first character of the array to our output
screen.innerHTML += characters.shift()
// if we still have more characters, than run it again
if (characters.length) window.setTimeout(next, 100);
}
// kick off the script to output the characters
next()

}

slowPrint(messages)







By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Comments

Popular posts from this blog

paramiko-expect timeout is happening after executing the command

Export result set on Dbeaver to CSV

Opening a url is failing in Swift