Как приостановить и возобновить выполнение функций в javascript

#javascript

Вопрос:

У меня есть эта функция:

 const BFS = (graph, start) => {
  let queue = []
  queue.push(start)

  let visited = []
  visited[start] = true

  while (queue.lenght > 0) {
    let node = queue.shift()
    for (var i=1; i<graph[node].length; i  ) {
      if (graph[node][i] amp;amp; !visited[i]) {
        visited[i] = true
        queue.push(i)
      }
    }
  }
}
 

Я хочу, чтобы у меня была кнопка, при нажатии на которую она останавливает выполнение функции, а если я нажму ее снова, она возобновит выполнение с того же места, на котором остановилась.

Возможно ли это ? и если да, то как это сделать ?

Комментарии:

1. Я не думаю, что существует такой способ сделать это, кроме использования точек останова. Я предполагаю, что вы пытаетесь отладить код? Если это так, убедитесь, что слово length правильно написано в строке while (queue.lenght > 0) { .

2. То, чего вы хотите, невозможно. Самое близкое-прикрепить событие щелчка к кнопке, чтобы запустить некоторые JS. Когда вы запускаете JS-код, все остальное в браузере блокируется, пока JS не будет выполнен, невозможно обнаружить нажатие кнопки во время выполнения JS.

3. @Adnan Я не пытаюсь отлаживать код .. дело в том, что эта функция является частью процесса визуализации, и я хочу, чтобы пользователь мог остановить визуализацию, когда захочет, а затем возобновить ее снова.

4. @MikeM Можно внести изменения в функцию BFS, если это изменение решит проблему

5. @Teemu, это возможно — с помощью функций генератора проверьте мое решение.

Ответ №1:

Другое решение с функциями генератора, читайте об этой функции на MDN

Обратите внимание, что здесь мы можем продвигаться дальше по одному шагу за раз!

Общая идея:

  1. Поместите yield операторы в свой метод в тех местах, где вы хотите его приостановить.
  2. Создайте экземпляр своего генератора и напишите код, который вызовет его .next() метод и будет обрабатывать повторные вызовы.
  3. Обратите внимание, что вы можете получать значения из генератора, а также передавать их с .next() помощью метода.
 // generator function that we want to stop/continue in the middle
function* stoppableMethod() {
  // here is the implementation of the algorithm
  // that we want to control
  let i = 0;
  while (true) {
    // note that yield is inside infinite loop!
    yield i;
    i  = 1;
  }
}

const generatorInstance = stoppableMethod();

// tick generator and perform update of the indicator
const nextStep = () => {
  const { value } = generatorInstance.next();
  document.getElementById("indicator").innerHTML = value;
}

// state to keep track of the setInterval id
const state = {
  timeoutId: 0,
}

// start method progression
const start = () => {
  // do not start interval if there is already an interval
  // running
  if (state.timeoutId === 0) {
    state.timeoutId = setInterval(() => nextStep(), 1000)
  }
}

// clear timeout to stop auto porgress
const stop = () => { 
  clearTimeout(state.timeoutId);
  state.timeoutId = 0;
}

// tick further one step
const stepForward = () => nextStep() 
 <button onclick="start()">start</button>
<button onclick="stop()">pause</button>
<button onclick="nextStep()">one step forward</button>
<div id="indicator">0</div> 

Комментарии:

1. Если вы нажмете на Start кнопку два или более раз, следовательно, она активирует ваш счетчик по сравнению с предыдущими. Кроме того, использование генераторов-хорошая идея.

Ответ №2:

Вы можете использовать это, но я не уверен, поможет ли это вам
, насколько я знаю, кроме этого, вы не можете остановить запуск функции извне

 document.getElementById("start").addEventListener("click", startInterval);
document.getElementById("stop").addEventListener("click", stopInterval);

// You'll need a variable to store a reference to the timer
var timer = null;

function startInterval() {
  // Then you initilize the variable
  timer = setInterval(function() {
    console.log("Foo Executed!");
  }, 800);
}

function stopInterval() {
  // To cancel an interval, pass the timer to clearInterval()
  clearInterval(timer);
} 
 <button type="button" id="start">Start</button>
<button type="button" id="stop">Stop</button> 

Ответ №3:

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

В данном конкретном случае ваше состояние может быть;

 var state = { active: false
            , graph : myGraph
            , node  : currentNode
            , index : i
            }
 

Поскольку вы в основном делаете это в while цикле, я бы добавил такое утверждение, как;

 while (queue.lenght > 0 amp;amp; state.active)
 

и как только кто-то нажимает кнопку и переключается state.active на false while цикл, он завершается, и в этот момент, я имею в виду сразу после while цикла, вы сохраняете свой новый state . Поэтому очевидно, что в следующий раз, когда вы активируете эту функцию, ваш код должен быть в состоянии начать с того места, где он был оставлен.

Вот простой макет, чтобы показать, как это может быть реализовано;

 var state = { active: false
            , count : "0"
            };

document.querySelector("#b")
        .onclick = function(){
                     let run = n => setTimeout( n => ( this.textContent = n.toString()
                                                                           .padStart(6,"0")
                                                     , state.active amp;amp; run(  n)
                                                     )
                                              , 100
                                              , n
                                              );
                     state.active ? ( state.active = false
                                    , state.count  = this.textContent
                                    )
                                  : ( state.active = true
                                    , run(state.count)
                                    );
                  }; 
 <button id="b">000000</button> 

Ответ №4:

Чтобы приостановить и возобновить выполнение функции в определенном месте одним нажатием кнопки, мы можем использовать либо yield функцию генератора, либо await Обещание в асинхронной функции.

В любом случае, нам нужно уступить или дождаться асинхронной функции , например setTimeout , setInterval или requestIdleCallback предоставить одному потоку выполнения JavaScript возможность выполнять любые обратные вызовы обработчика событий, которые затем могут управлять возобновлением приостановленной функции.

Смотрите цикл событий JavaScript, чтобы понять это подробнее.

Предположим, у нас есть кнопка и функция f , которые мы хотим приостановить на показанной строке.

 function f() {
  let i = 0;
  while (true) {
    // pause here when button clicked
    button.textContent =   i;
  }
}
 

Если использовать yield , то f можно было бы внести изменения в

 function* f() {
  let i = 0;
  while (true) {
    yield;
    button.textContent =   i;
  }
}
 

Затем мы создадим итератор из этой функции генератора и будем использовать iterator.next() в асинхронно выполняемом блоке вызовов для возобновления выполнения.

Если использовать await , то f вместо этого можно было бы изменить на

 async function f() {
  let i = 0;
  while (true) {
    await new Promise(executor);
    button.textContent =   i;
  }
}
 

где executor функция, вызывающая асинхронную функцию, которая разрешает исполнителю возобновить выполнение из await .


Некоторые примеры:

Используя функцию генератора и setTimeout .

 const button = document.querySelector('button');

let started = false;
let iterator = f();

const nextTick = () => {
  if (started) {
    iterator.next();
    setTimeout(nextTick);
  }
};

button.addEventListener('click', () => {
  started = !started;
  nextTick();
});

function* f() {
  let i = 0;
  while (true) {
    yield;
    button.textContent =   i;
  }
} 
 button { 
  text-align: center; padding: .5rem; width: 16rem;
  font-size: 2rem; border-radius: .5rem; margin-left: .25rem;
} 
 <button>Click me</button> 

Использование async и await :

 const button = document.querySelector('button');

let started = false;
let resolve;

const nextTick = () => new Promise(res => {
  resolve = res;
  setTimeout(() => {
    if (started) resolve();
  });
});

button.addEventListener('click', () => {
  started = !started;
  if (started) resolve();
});

async function f() {
  let i = 0;
  while (true) {
    await nextTick();
    button.textContent =   i;
  }
}

f(); 
 button { 
  text-align: center; padding: .5rem; width: 16rem;
  font-size: 2rem; border-radius: .5rem; margin-left: .25rem;
} 
 <button>Click me</button> 

Использование setInterval , которое в основном аналогично использованию setTimeout , но менее гибко:

 const button = document.querySelector('button');

let id = 0;
let started = false;
const iterator = f();
const next = iterator.next.bind(iterator);

button.addEventListener('click', () => {
  started = !started;
  if (started) {
    if (id === 0) id = setInterval(next, 0);
  } else if (id !== 0) {
    clearInterval(id);
    id = 0;
  }
});

function* f() {
  let i = 0;
  while (true) {
    yield;
    button.textContent =   i;
  }
} 
 button { 
  text-align: center; padding: .5rem; width: 16rem;
  font-size: 2rem; border-radius: .5rem; margin-left: .25rem;
} 
 <button>Click me</button> 

Проблема с использованием асинхронных функций, таких как setTimeout или setInterval здесь, заключается в том, что они подвержены минимальной задержке в несколько миллисекунд, что видно по тому, как медленно увеличивается число в приведенных выше примерах.

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

 const button = document.querySelector('button');

let nextTick;

button.addEventListener('click', (() => {
  let started = false;
  let resolve;
  const { port1, port2 } = new MessageChannel();
  port2.onmessage = () => {
    if (started) resolve();
  };
  nextTick = () => new Promise(res => {
    resolve = res;
    port1.postMessage(null);
  });
  return () => {
    started = !started;
    if (started) resolve();
  };
})());

async function f() {
  let i = 0;
  while (true) {
    await nextTick();
    button.textContent =   i;
  }
}

f(); 
 button { 
  text-align: center; padding: .5rem; width: 16rem;
  font-size: 2rem; border-radius: .5rem; margin-left: .25rem;
} 
 <button>Click me</button> 

Обратите внимание, что в этих примерах nextTick могло быть названо что угодно, это не то же самое process.nextTick , что Node.js. См. раздел setImmediate при использовании Node.js.

Если вы анимируете, вы можете использовать requestAnimationFrame , и это также выполняет свой обратный вызов асинхронно, поэтому его можно использовать здесь.

Ниже показана относительная скорость различных асинхронных функций:

 const button = document.querySelector('button');

button.addEventListener('click', (() => {
  const setImmediateAnalogues = [
    setTimeout,
    requestAnimationFrame,
    cb => requestIdleCallback(cb, { timeout: 0 }),
    cb => {
      window.onmessage = event => {
        // event.origin should be validated here
        if (event.source === window) cb();  
      };
      window.postMessage('', window.location);
    },
    (() => {
      const { port1, port2 } = new MessageChannel();
      return cb => {
        port2.onmessage = cb;
        port1.postMessage('');
      };
    })(),
  ];
  
  let setImmediate = setTimeout;
  for (const rb of document.querySelectorAll('input[name="rb"]')) {
    const analog = setImmediateAnalogues.shift();
    rb.addEventListener('click', () => setImmediate = analog);
  }
  
  const iterator = f();
  let started = false;
  const nextTick = () => {
    if (started) {
      iterator.next();
      setImmediate(nextTick);
    }
  };
  
  return () => {
    started = !started;
    nextTick();
  };
})());


function* f() {
  let i = 0;
  while (true) {
    yield;
    button.textContent =   i;
  }
} 
 button { text-align: center; padding: .5rem; width: 16rem;
font-size: 2rem; border-radius: .5rem; margin-left: .25rem; }
label { padding: .1rem; display: block; font-family: monospace; } 
 <label><input type='radio' name='rb' value='' checked> setTimeout</label>
<label><input type='radio' name='rb' value=''> requestAnimationFrame</label>
<label><input type='radio' name='rb' value=''> requestIdleCallback</label>
<label><input type='radio' name='rb' value=''> window.postMessage</label>
<label><input type='radio' name='rb' value=''> MessageChannel</label>
<br>
<button>Click me</button>