Как синхронно запустить вложенный цикл? - PullRequest
0 голосов
/ 29 июня 2018

Я пишу простой скрипт, который печатает текст на экране по одному символу за раз.

Я делаю так, чтобы функция (которую я назвал slowPrint) могла получить массив строк. Каждый элемент в массиве представляет сообщение.

Это код, который у меня есть:

Однако я не получаю ожидаемый результат. Я подозреваю, что это отчасти связано с асинхронной природой кода, хотя у меня нет полного и ясного понимания того, что происходит и как это исправить.

Начнем с того, что теги <br /> печатаются перед любым сообщением, что говорит мне о том, что внешний цикл завершается еще до запуска вложенного цикла.

Однако, когда вложенные циклы начинаются, каждая строка в массиве печатается с интервалом в одну секунду, но целиком , а не символ за символом .

Чего мне не хватает?

Кроме того, кто-нибудь может объяснить, пожалуйста, следующее поведение метода setTimeout?

Сценарий 1 : Когда я устанавливаю второй аргумент в 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)
<div id="screen"></div>

Сценарий 2 : Когда я устанавливаю второй аргумент в j * 1000, вывод совершенно неожиданный: каждые вторые символы печатаются в наборах по 2, но в непонятном порядке; печатается только последнее слово из последних аргументов, как и все остальные.

Сценарий 3 : Когда я устанавливаю второй аргумент равным 1000, ВСЕ строки в массиве печатаются через одну секунду.

Что происходит?

Ответы [ 4 ]

0 голосов
/ 29 июня 2018

Есть множество способов сделать это от использования очереди до математики для определения диапазонов. Не слишком сильно изменяя свой код, вы можете просто проверить, находитесь ли вы на последнем символе, а затем добавить разрыв строки и использовать переменную, чтобы отслеживать текущее время для вывода.

 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)
<div id="screen"></div>

Лично я бы использовал больше очереди, чтобы у меня не было тонн созданных таймеров.

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)
<div id="screen"></div>
0 голосов
/ 29 июня 2018

Это видео является одним из лучших объяснений того, как js работает в браузере: здесь

По сути, все, что вы помещаете в обратный вызов setTimeout, помещается в backburner на количество мс, которое вы передаете второму аргументу. Затем он помещается в очередь обратного вызова до тех пор, пока стек вызовов не станет пустым и не станет следующим элементом в очереди

Если вы скопируете и вставите свой код в http://latentflip.com/loupe/, вы увидите, как он на самом деле работает за кадром

0 голосов
/ 29 июня 2018

Вы можете сделать это с довольно кратким кодом, просто используя 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)
<div id="screen"></div>

Причина, по которой у вашего кода проблемы, заключается в том, что цикл for не останавливается и не ожидает истечения времени ожидания. Цикл for позволяет all тайм-аутам запускаться почти одновременно, поэтому через 1000 мс они все срабатывают. setInterval обычно лучший метод, когда вам нужно, чтобы что-то происходило периодически.

Есть, конечно, много других способов сделать это. Просто пример чего-то более экзотического, вот способ сделать это с помощью простого генератора. Это немного сложнее понять, но выглядит довольно чисто, когда вы привыкли к генераторам:

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)
<div id='screen'></div>
0 голосов
/ 29 июня 2018

Используя 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)
<div id="screen"></div>
Обратный вызов

setTimeout() выполняется асинхронно , поэтому порядок выполнения всегда будет выглядеть следующим образом:

// first

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

// second

Как отмечается в комментариях, async / await поддерживается только в браузерах , которые реализуют ECMAScript 2017 .

...