У меня есть функция JavaScript расширения Chrome, которую мне нужно запускать с точными 5-минутными интервалами, синхронизируя с текущим временем. Например, он должен запускаться в 5:00, 5:05, 5:10 и т. д. независимо от того, когда запускается сценарий.
Вот подход, который я пытаюсь реализовать:
Calculate the delay until the next 5-minute mark.
Use setTimeout to call the function at the next 5-minute mark.
Set up a setInterval to call the function every 5 minutes after the initial timeout.
Here is a simplified version of the code to demonstrate the issue:
function myFunction() {
console.info("Function executed at:", new Date().toLocaleTimeString());
}
function checkTimeAndRunMainFunction() {
let now = new Date();
let minutes = now.getMinutes();
let seconds = now.getSeconds();
if (seconds === 0 && minutes % 5 === 0) {
// Run the main function immediately
myFunction();
}
}
function calculateDelayToNextFiveMinuteMark() {
let now = new Date();
let millisecondsUntilNextFiveMinuteMark = (5 - (now.getMinutes() % 5)) * 60 * 1000 - now.getSeconds() * 1000 - now.getMilliseconds();
return millisecondsUntilNextFiveMinuteMark;
}
// Set an initial timeout to synchronize with the next 5-minute mark
setTimeout(function() {
checkTimeAndRunMainFunction();
// Set an interval to check the time every 5 minutes after the initial call
setInterval(checkTimeAndRunMainFunction, 5 * 60 * 1000);
}, calculateDelayToNextFiveMinuteMark());
Проблема, с которой я столкнулся, заключается в том, что setInterval(checkTimeAndRunMainFunction, 1000); отсчитывает каждую секунду с момента запуска пользователем кода и не синхронизируется с фактическими секундами часов, в результате чего условие if (секунды === 0 && минуты % 5 === 0) не выполняется точно.
Как я могу гарантировать, что моя функция будет выполняться точно на 5-минутных отметках (например, 5:00, 5:05, 5:10) независимо от того, когда запускается скрипт?
@woxxom использует setTimeout для определения первого запуска.
@Hamed, вы передаете неправильное количество миллисекунд (время, прошедшее с момента последнего запуска) на первоначальный вызов. Используйте 300000 - (Date.now() % 300000) для продолжительности setTimeout.
@Christopher, я хочу сказать, что в вопросе упоминается часть кода, которой не существует.
@woxxom, его задача заключается в том, что когда сценарий выполняется за 0,995 секунды (или первый запуск выполняется, когда Date.now() заканчивается через 99N миллисекунд), проверка (мин = 0 и секунды = 0) может завершиться неудачей. Потому что результаты getSeconds() и getMilli Seconds() внутри обратного вызова могут быть 1 и >=0 во время выполнения.
Я не комментировал вызов. Мой комментарий был по поводу несоответствия текста вопроса и размещенного кода.



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Вот простой скрипт, который запускается каждые 5 минут и корректирует время следующего запуска при каждом запуске.
(function loop() {
const date = new Date();
const wait = 300000 - date % 300000
console.info(date, wait);
setTimeout(() => {
console.info(new Date())
loop();
// your code here
}, wait);
})();Ошибка, если оставшееся время превышает 1 минуту. - Максимум, который вы вычтете из 300 000, равен 59 999. Используйте timespan - (Date.now() % timespan) для определения продолжительности.
Вы правы, исправили - переместили «ваш код» в план следующего запуска. На самом деле вы вычитаете только миллисекунды - это корректировки для планирования следующего цикла через следующие 5 минут. И только после того, как вы спланируете следующий запуск, вы выполняете свой основной код.
Я отредактировал ответ. Должно работать правильно.
Я протестировал его, и он работает нормально каждые 5 минут, но проблема в том, что это влияет на работу основной функции. Вероятно, это связано с конфликтом параметра задержки внутри основной функции. Как я сказал в своем вопросе, основная функция имеет свой собственный период, и ваш код привел к уменьшению и неточному его периоду.
Вот более безопасный сценарий. Он будет выполняться каждый раз, когда интервал достигает 5-минутной отметки, а затем перемещает запись в конец массива. Это означает, что есть место для некоторого колебания
Вы можете добавить checkTimeAndRunMainFunction() перед setInterval, чтобы запустить его немедленно, если пользователь запустит его ровно через 5 минут.
const myFunction = () => {
console.info("Function executed at:", new Date().toLocaleTimeString());
}
const getNearestFiveMinuteMark = () => {
const now = new Date();
const minutes = now.getMinutes();
return Math.ceil(minutes / 5) * 5;
}
const initializeFiveMinuteArray = () => {
const nearestFiveMinuteMark = getNearestFiveMinuteMark();
const intervals = [];
for (let i = 0; i < 12; i++) { // 12 entries for every 5 minutes in an hour
intervals.push((nearestFiveMinuteMark + i * 5) % 60);
}
return intervals;
}
const checkTimeAndRunMainFunction = () => {
const now = new Date();
const currentMinute = now.getMinutes();
if (currentMinute === fiveMinuteIntervals[0]) {
// Run the main function and move the entry to the end
myFunction();
const executedEntry = fiveMinuteIntervals.shift();
fiveMinuteIntervals.push(executedEntry);
}
time.textContent = `next run will be at :${String(fiveMinuteIntervals[0]).padStart(2,'0')}`;
}
const fiveMinuteIntervals = initializeFiveMinuteArray();
console.info("Initial 5-minute intervals array:", fiveMinuteIntervals);
// Set an interval to check the time every second
setInterval(checkTimeAndRunMainFunction, 1000);<span id = "time"></span>Проблема ОП должна быть решена...
... в общее решение для функции, которая вычисляет количество миллисекунд до следующего доступного слота для запуска интервала запуска/выполнения основной задачи OP.
... и полностью переписать функцию checkTimeAndRunMainFunction ОП.
Что касается (1), пример кода, который предоставляется после объяснения, переименовывает функцию calculateDelayToNextFiveMinuteMark OP в getTimeToNextAvailableSchedulerSlot и реализует расчет задержки общим способом на основе точности передаваемого слота в секундах и возвращает задержку. значение в миллисекундах, чтобы быть максимально точным.
Что касается (2), checkTimeAndRunMainFunction необходимо полностью переопределить как scheduleTask, где первый параметр новой функции — это любая функция, которую нужно запустить/выполнить в запланированном режиме на основе временного интервала; его второй параметр равен точности передаваемого слота в секундах, которая будет использоваться как для расчета задержки до запуска расписания, так и для установки интервала расписания/задачи.
scheduleTask дополнительно возвращает объект, который имеет два свойства timeout и interval, оба из которых являются идентификаторами таймера. Таким образом, можно по-прежнему предотвратить запуск расписания, очистив тайм-аут с помощью timeout, и можно завершить уже работающее расписание/интервал, очистив интервал с помощью interval.
function getTimeToNextAvailableSchedulerSlot(slotPrecisionInSeconds) {
const current = new Date;
const precisionInMilliseconds = Number
.isFinite(slotPrecisionInSeconds = parseInt(slotPrecisionInSeconds, 10))
? (slotPrecisionInSeconds * 1000)
: 60_000; // 1 minute slot default.
const currentMilliseconds = (
current.getHours() * 60 * 60 * 1000 +
current.getMinutes() * 60 * 1000 +
current.getSeconds() * 1000 +
current.getMilliseconds()
);
return (
precisionInMilliseconds -
(currentMilliseconds % precisionInMilliseconds)
);
}
function scheduleTask(task, slotPrecisionInSeconds) {
const timerIds = {
timeout: null, interval: null,
};
const delay = getTimeToNextAvailableSchedulerSlot(slotPrecisionInSeconds);
timerIds.timeout = setTimeout(() => {
timerIds.interval = setInterval(task, slotPrecisionInSeconds * 1000);
task();
}, delay);
return timerIds;
}
function chron_job_5_seconds_slot() {
console.info('5 seconds chron job executed at ...', new Date().toLocaleTimeString());
}
function chron_job_15_seconds_slot() {
console.info('15 seconds chron job executed at ...', new Date().toLocaleTimeString());
}
function chron_job_1_minute_slot() {
console.info('1 minute chron job executed at ...', new Date().toLocaleTimeString());
}
function chron_job_5_minutes_slot() {
console.info('5 minutes chron job executed at ...', new Date().toLocaleTimeString());
}
console.info('initialization started at ... ', new Date);
// execution at the next available 5 seconds slot continued with a 5 seconds interval.
const timer_ids_5_seconds_slot = scheduleTask(chron_job_5_seconds_slot, 5);
// execution at the next available 15 seconds slot continued with a 15 seconds interval.
const timer_ids_15_seconds_slot = scheduleTask(chron_job_15_seconds_slot, 15);
// execution at the next available 1 minute slot continued with a 1 minute interval.
const timer_ids_1_minute_slot = scheduleTask(chron_job_1_minute_slot, 60);
// execution at the next available 5 minutes slot continued with a 5 minutes interval.
const timer_ids_5_minutes_slot = scheduleTask(chron_job_5_minutes_slot, 5 * 60);
// terminate the 5 seconds based task immediately.
clearTimeout(timer_ids_5_seconds_slot.timeout);
// terminate all other tasks after 15 minutes.
setTimeout(() => {
clearInterval(timer_ids_15_seconds_slot.interval);
clearInterval(timer_ids_1_minute_slot.interval);
clearInterval(timer_ids_5_minutes_slot.interval);
}, 15 * 60 * 1000);.as-console-wrapper { min-height: 100%!important; top: 0; }Совет профессионала
Проблему синхронизируемых функций/методов можно решить с помощью общего подхода. Еще лучше реализовать общий подход как clocked метод, например. Function.prototype.
Применение/использование последнего для решения проблемы ФП было бы так же просто, как...
// the OP's function.
function myFunction() {
console.info("Function executed at:", new Date().toLocaleTimeString());
}
// - create a variant of the OP's above function
// with an interval clocked at (or set to) 5 minutes.
const clockedMyFct = myFunction.clocked(5 * 60 * 1000);
// // - whether to trigger an immediate or
// // initial execution of `myFunction`.
// myFunction();
// - invoke/call the clocked variant of the OP's `myFunction`.
// - the latter is going to be invoked every 5 minutes and infinitely.
clockedMyFct();
Но реализация метода clocked способна на гораздо большее. Можно настроить поток управления любой тактируемой функцией, предоставив дополнительную функцию обратного вызова, которая действует как controller.
Предоставив такую функцию контроллера, последняя вызывается вместо функции, которая будет синхронизироваться, но тактируется с тем же интервалом. Единственным параметром контроллера является (тактируемый) объект data, который содержит все необходимое для управления потоком управления синхронизируемой функции.
Например, можно получить доступ к значению count и функции terminate, которая позволяет завершить/очистить интервал после определенного количества выполненных интервалов. Простой пример демонстрирует сказанное только что...
// the OP's function.
function myFunction() {
console.info("Function executed at:", new Date().toLocaleTimeString());
}
// the controller function with a custom implemented control flow.
function controller(data) {
const { clock: { count }, proceed, terminate } = data;
if (count > 5) {
terminate();
} else {
// - calling the function that needs
// to be executed in a clocked way.
proceed();
}
}
// - create a variant of the OP's above function with
// an interval clocked at (or set to) 2 seconds which
// terminates itself after having been invoked 5 times.
const clockedMyFct = myFunction.clocked(2_000, null, controller);
// // - whether to trigger an immediate or
// // initial execution of `myFunction`.
// myFunction();
// - invoke/call the clocked variant of the OP's `myFunction`.
// - the latter is going to be invoked every 2 seconds and
// to be terminated after having been invoked 5 times.
clockedMyFct();
И возможная реализация прототипного метода clocked и некоторого исполняемого примера кода, который создает и вызывает различные clocked версии в соответствии с проблемой ОП, выглядит следующим образом...
function loggerOne() {
console.info(
`\nlogger ONE executed at ... ${ new Date().toLocaleTimeString() }\n`
);
}
function loggerTwo() {
console.info(
`logger TWO executed at ... ${ new Date().toLocaleTimeString() }`
);
}
function controller(clockedData) {
const {
clock: { interval, startTime, timestamp, count },
target, args, proceed, terminate,
} =
clockedData;
const passedTime = timestamp - startTime;
const passedSeconds = passedTime / 1000;
// - implement controller logic as you like/need.
if (count > 10) {
terminate();
} else {
console.info({
interval,
startTime,
timestamp,
passedSeconds,
count,
target,
args,
proceed,
});
// - calling the function that needs
// to be executed in a clocked way.
proceed.apply(target, args);
}
}
const clocked_at_20_seconds_cycling_infinitely =
loggerOne.clocked(20_000);
const clocked_at_07_seconds_limited_to_10_cycles =
loggerTwo.clocked(7_000, null, controller);
console.info(
`... test started at ... ${ new Date().toLocaleTimeString() }`
);
clocked_at_20_seconds_cycling_infinitely();
clocked_at_07_seconds_limited_to_10_cycles();body { margin: 0; }
.as-console-wrapper { min-height: 100%!important; top: 0; }<script>
(function (Reflect, Math, Number, Array, Function) {
'use strict';
/**
* @typedef {Object} clockedControllerClockData
* @param {number} interval - interval-value in milliseconds.
* @param {number} startTime - time of first invocation in milliseconds.
* @param {number} timestamp - time of current invocation in milliseconds.
* @param {number} count - current invocation-count since first invocation; `count` value of 1st invocation is `1`.
*/
/**
* A data object which holds all the necessary references
* in order to properly run any controller logic.
*
* @typedef {Object} clockedControllerData
* @param {clockedControllerClockData} clock
* @param {(Object|null)} target
* @param {Array} args
* @param {Function} proceed
* @param {Function} terminate
*/
/**
* A callback function which acts like a pre-processor which e.g. can decide
* whether to proceed with or terminate the original functions `clocked` version.
*
* @callback clockedController
* @param {clockedControllerData} data
* A data object which holds all the necessary references
* in order to properly run any controller logic.
*/
const DEFAULT_INTERVAL = 200;
const { isFinite, parseInt } = Number;
function isFunction(value) {
return (
typeof value === 'function' &&
typeof value.call === 'function' &&
typeof value.apply === 'function'
);
}
function getSanitizedInteger(value) {
value = parseInt(value, 10);
return isFinite(value) ? value : 0;
}
function getSanitizedPositiveInteger(value) {
return Math.max(getSanitizedInteger(value), 0);
}
/**
* Returns a function, that, invoked initially once, will continuously auto-invoke
* the original function every amount of milliseconds, as it was parametrized by
* this methods `interval` argument. It does so until the additional `terminate`
* method of the returned wrapper function gets called.
* This methods second parameter - `target` - provides the object/context a `clocked`
* method can act upon.
* This methods third parameter - `controller` - enables the even more precise
* customized handling of how to run a `clocked` function/method.
*
* @param {number=} interval - The optional interval value; expected to be grater than zero and assuming milliseconds.
* @param {*=} target - The function's/method's optional target object.
* @param {clockedController=} controller
* The optional callback function which acts like a pre-processor where additionally
* provided program logic might e.g. decide whether to proceed with or terminate
* the original functions `clocked` version.
* @returns {Function}
* The original function's/method's `clocked` version.
*/
function createClockedFunction(interval/*, delay*/, target, controller) {
const proceed = this;
let thisArg;
let argsArr;
let clockCount = null;
let clockStart = null;
let timerId = null;
target = target ?? null; // `null` instead of an `undefined` default.
interval = getSanitizedPositiveInteger(interval) || DEFAULT_INTERVAL;
function triggerController() {
controller({
clock: {
interval,
startTime: clockStart,
timestamp: Date.now(),
count: ++clockCount,
},
target: thisArg,
args: [...argsArr],
proceed,
terminate,
});
}
function triggerProceed() {
proceed.apply(thisArg, argsArr);
}
function terminate() {
clearInterval(timerId);
timerId = null;
clockStart = null;
clockCount = null;
}
function isActive() {
return (timerId !== null);
}
function clocked(...argumentsArray) {
// - a `clocked` method's target can be delegated at call time, thus
// it overrules the target which was provided at composition time.
thisArg = (this ?? null) ?? target;
argsArr = argumentsArray;
if (isActive()) {
terminate();
}
clockCount = 0;
clockStart = Date.now();
const trigger = isFunction(controller)
? triggerController
: triggerProceed;
timerId = setInterval(trigger, interval);
}
clocked.terminate = terminate;
clocked.isActive = isActive;
return (isFunction(proceed) && clocked) || proceed;
}
Reflect.defineProperty(Function.prototype, 'clocked', {
configurable: true,
writable: true,
value: createClockedFunction,
});
}(Reflect, Math, Number, Array, Function));
</script>@Hamed ... Я отредактировал свой (первый) ответ таким образом, чтобы он впервые предоставил решение вашей проблемы, заключающейся в том, как точно запускать задания хрона на основе временных интервалов и интервалов. Моим первым ответом, который не решил реальную проблему, стал раздел «Совет профессионала» в обоих приведенных выше решениях.
В опубликованном коде нет
setInterval(checkTimeAndRunMainFunction, 1000). Может быть, у вас на странице работает старая версия сценария содержимого? Перезагрузите расширение на странице chrome://extensions и перезагрузите вкладку. См. также Как удалить потерянный скрипт после обновления расширения Chrome