简体   繁体   中英

setTimeout inside setTimeout inside a setInterval

I'm trying to make a string that will write itself letter by letter until completing the sentence, and the speed of appearing each letter is based on an input that varies from 1 to 10. At the end of the string, it will blink for 5 seconds until that an alien will appear. My idea was to create a setInterval to add the letters and when the counter added the array size it would return the final animation of the loop with the new setInterval call, and before it was called again it had already been cleared, and called again in a recursion by setTimout callback to maintain the infinite loop. But it's not reaching setTimout, why?

//script.js

const speedInput = document.getElementsByClassName('speed--input')[0];
const alien = document.getElementsByClassName('alien')[0];
const textDiv = document.getElementsByClassName('text')[0];
const textShow = document.getElementsByClassName('text--show')[0];

const textDB = 'We go to dominate the world.';
const textStr = '';

let count = 0;
let speed = speedInput.value * 100;

const textChangeHandle = (count, textStr, textDB) => setInterval(() => {
  if (count < textDB.length) {
    textStr += textDB[count];
    textShow.innerHTML = textStr;
    textDiv.style.width = `${40 * count}px`;
    count++;
  } else {
    textShow.style.animation = 'bip 1s linear 1s infinite'
    return () => {
      setTimeout(() => {
        textShow.style.animation = '';
        textStr = '';
        count = 0;
        textShow.style.opacity = 0;
        alien.style.opacity = 1;

        setTimeout(() => {
          alien.style.opacity = 0;
          textShow.style.opacity = 1;
          textChangeHandle(count, textStr, textDB)
        }, 5000)
      }, 5000);
      clearInterval(textChangeHandle);
    }
  }
}, speed)

textChangeHandle(count, textStr, textDB);

//index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="style/style.css">

  <title>Document</title>
</head>
<body>
  <div class="text">
    <p class="text--show"></p>
    <img class="alien" src="alien.png" alt="Aki é Jupiter karai">
  </div>

  <div class="speed">
    <span>Speed</span>
    <input class="speed--input" type="number" min="1" max="10" value="1">
  </div>

  <script src="script.js"></script>
</body>
</html>

//style.css

*,
*::after,
*::before {
  margin: 0;
  padding: 0;
  -webkit-box-sizing: border-box;
          box-sizing: border-box;
}

body {
  font-family: sans-serif;
  background-color: #3cd070;
}

.text {
  position: fixed;
  top: 50%;
  left: 50%;
  height: auto;
  -webkit-transform: translate(-50%, -50%);
          transform: translate(-50%, -50%);
  font-size: 40px;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-pack: center;
      -ms-flex-pack: center;
          justify-content: center;
  -webkit-box-align: center;
      -ms-flex-align: center;
          align-items: center;
}

.text .alien {
  opacity: 0;
  height: 600px;
  width: auto;
  margin-bottom: 50px;
  position: absolute;
}

.text .text--show {
  font-size: 40px;
  width: 100%;
  position: absolute;
  display: block;
  text-align: center;
  -webkit-animation: bip 1s linear 1s infinite;
          animation: bip 1s linear 1s infinite;
}

.speed {
  position: fixed;
  top: 90%;
  left: 50%;
  -webkit-transform: translate(-50%, -50%);
          transform: translate(-50%, -50%);
  background-color: rgba(0, 0, 0, 0.2);
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  padding: 10px 20px;
  -webkit-box-align: center;
      -ms-flex-align: center;
          align-items: center;
  -webkit-box-pack: center;
      -ms-flex-pack: center;
          justify-content: center;
}

.speed .speed--input {
  border: 0;
  outline: 0;
  width: 40px;
  height: 25px;
  margin: 10px;
  text-align: center;
}

@-webkit-keyframes bip {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

@keyframes bip {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

There is also the animation frame api

It will run your function exactly before the next "natural" repaint of the browser.

Additionally and probably useful in your case, it will pass the current time in your callback, so you can take a time detail and animate according to this. That way you can get a smooth animation of equal length for everyone, even when the frame rate is inconsistent.

Consider the following snippet, which will basically cause an infinite loop.

function draw(now) {
   requestAnimationFrame(draw)
}

requestAnimationFrame(draw)

Now you only need to remember the time and take the time delta next time the function is called. If enough time has passed, you can write another character. (Normally you would change a value divided by the time delta, but since you have characters, they will be either there or not.

let speed = 300
let timePassedSinceLastChar = 0
let then = null

function draw(now){
  now *= 0.001;
  const deltaTime = now - then;
  then = now;
  timePassedSinceLastChar += deltaTime;
  if (timePassedSinceLastChar >= speed) {
     drawChar()
     timePassedSinceLastChar = 0
  }
  requestAnimationFrame(draw)
}

requestAnimationFrame(draw)

Furthermore, the requestAnimationFrame function returns the id of the requested frame. That allows to cancel the loop.

const frameId = requestAnimationFrame(draw)
cancelAnimationFrame(frameId)

So the final code could look something like this.

 const textDB = 'We go to dominate the world.'; let charIndex = 0; let frameId = null let then = 0 let sinceDraw = 0 let speed = () => Math.random() * 0.4 + 0.05 // randomize the speed a bit let $p = document.querySelector('p') function draw(now) { // cancel on end of string if (charIndex >= textDB.length) { cancelAnimationFrame(frameId) console.log('done') return } // get the time delta now *= 0.001; // convert to seconds const deltaTime = now - then; then = now; sinceDraw += deltaTime // if its time to draw again, do so if (sinceDraw >= speed()) { let char = textDB[charIndex] $p.textContent += char charIndex++ sinceDraw = 0 } // request another frame frameId = requestAnimationFrame(draw) } // request the first frame frameId = requestAnimationFrame(draw)
 <p></p>

The issue is that in the else statement, you are returning a function that is never called.

return () => {
  setTimeout(() => {
    textShow.style.animation = '';
    textStr = '';
    count = 0;
    textShow.style.opacity = 0;
    alien.style.opacity = 1;

    setTimeout(() => {
      alien.style.opacity = 0;
      textShow.style.opacity = 1;
      textChangeHandle(count, textStr, textDB)
    }, 5000)
  }, 5000);
  clearInterval(textChangeHandle);
}

Just remove the return statment and call the setTimeout function directly.

const textChangeHandle = (count, textStr, textDB) => setInterval(() => {
  if (count < textDB.length) {
    textStr += textDB[count];
    textShow.innerHTML = textStr;
    textDiv.style.width = `${40 * count}px`;
    count++;
  } else {
    textShow.style.animation = 'bip 1s linear 1s infinite'
    setTimeout(() => {
      textShow.style.animation = '';
      textStr = '';
      count = 0;
      textShow.style.opacity = 0;
      alien.style.opacity = 1;

      setTimeout(() => {
        alien.style.opacity = 0;
        textShow.style.opacity = 1;
        textChangeHandle(count, textStr, textDB)
      }, 5000)
    }, 5000);
    clearInterval(textChangeHandle);
  }
}, speed)

I found the solution. With setInterval, the code was creating multiple instances of callbacks and overloading memory.

///script.js

const textEl = document.getElementById('text');
const inputEl = document.getElementById('input');
const alienEl = document.getElementById('alien');

const text = 'We go to dominate the world!';

let idx = 0;
let speed = 300 / inputEl.value;

writeText();

function writeText () {
  if(idx === 0) setTimeout(() => textEl.style.opacity = 1, speed)

  textEl.innerText = text.slice(0, idx);
  
  idx++;
  
  if (idx > text.length) {
    idx = 0;
    textEl.style.animation = 'bip 1s linear 1s infinite';
    setTimeout(() => {
      textEl.style.animation = ''
      textEl.style.opacity = 0; 
      setTimeout(() => {
        alienEl.style.opacity = 1;
        setTimeout(() => {
          alienEl.style.opacity = 0;
          setTimeout(writeText, speed);
        }, 5000)
      }, 1000);
    }, 5000)  
  } else {
    setTimeout(writeText, speed);
  }
}

inputEl.addEventListener('input', (e) => speed = 300 / e.target.value);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM