一、先搞清楚你想干什么
其实这个问题拆开来很简单,就是三个东西:
- 一个按钮,点一下就开始
- 一个定时器,每秒跑一次
- 一个请求,每秒发一次,直到某种条件触发才停
很多新手一上来就 setInterval 写满屏代码,结果发现停不下来,或者请求堆积成山。咱们一步步来,把逻辑讲清楚。
二、最基础的思路
先用一个最简单的例子让你理解核心机制,不加任何花哨的东西。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>每秒请求示例</title>
</head>
<body>
<button id="startBtn">开始</button>
<p id="status">未开始</p>
<script>
let timerId = null; // 用来存定时器的ID
// 发送请求的函数
function sendRequest() {
console.log('第' + (Date.now() % 10000 / 1000 | 0) + '秒,发送请求...');
// 这里放你的实际请求逻辑,比如 fetch()
}
// 开始按钮点击事件
document.getElementById('startBtn').addEventListener('click', function () {
// 如果定时器还没开,就开启
if (!timerId) {
sendRequest(); // 先立即执行一次
timerId = setInterval(sendRequest, 1000); // 每秒执行一次
document.getElementById('status').textContent = '运行中...';
}
});
</script>
</body>
</html>
注意几个关键点:
timerId变量必须放在click事件外面,否则每次点击都会重置它,定时器永远停不了。- 先执行一次,再设定时器,这样用户点按钮的瞬间就能拿到第一次响应,不用等一秒。
三、加上”停止”逻辑
光会开不够,还得会停。通常停止的条件是:
- 用户再次点击同一个按钮(切换开始/停止)
- 请求返回了某个特定结果
- 达到最大次数
最常见的做法是用一个开关变量配合按钮文字切换。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>开始/停止定时请求</title>
</head>
<body>
<button id="toggleBtn">开始</button>
<p id="status">状态:未开始</p>
<p id="count">已发送请求次数:0</p>
<script>
let timerId = null;
let isRunning = false;
let requestCount = 0;
function sendRequest() {
requestCount++;
document.getElementById('count').textContent = '已发送请求次数:' + requestCount;
// 模拟发送请求(实际项目换成你的 fetch 或 $.ajax)
console.log('第' + requestCount + '次请求已发送,时间戳:' + Date.now());
// 模拟请求完成后,收到"停止"信号,比如这里用第5次停止演示
if (requestCount >= 5) {
stopTimer();
document.getElementById('status').textContent = '状态:已停止(达到最大次数)';
return;
}
}
function startTimer() {
sendRequest(); // 立即执行第一次
timerId = setInterval(sendRequest, 1000);
isRunning = true;
document.getElementById('toggleBtn').textContent = '停止';
document.getElementById('status').textContent = '状态:运行中';
}
function stopTimer() {
if (timerId) {
clearInterval(timerId); // 关键:用同一个ID清除
timerId = null;
}
isRunning = false;
document.getElementById('toggleBtn').textContent = '开始';
}
document.getElementById('toggleBtn').addEventListener('click', function () {
if (!isRunning) {
startTimer();
} else {
stopTimer();
document.getElementById('status').textContent = '状态:已手动停止';
}
});
</script>
</body>
</html>
这段代码里最重要的是 clearInterval(timerId),必须传当初 setInterval 返回的那个ID,否则停不掉。
四、真实的业务场景:用 jQuery 发送 AJAX 请求
既然你标题里写了 “jq”,咱们就用 jQuery 的真实场景来演示。比如每秒轮询服务器,检查某个任务是否完成,完成后自动停止。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>jQuery 每秒轮询请求</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<button id="pollBtn">开始轮询</button>
<p id="status">未开始轮询</p>
<p id="result"></p>
<script>
let pollTimer = null;
let isPolling = false;
// 发送轮询请求
function pollServer() {
$.ajax({
url: '/api/check-task-status', // 替换成你的接口地址
type: 'GET',
dataType: 'json',
success: function (res) {
// 假设返回格式:{ status: 'pending' | 'completed' }
$('#status').text('轮询中... 最新状态:' + (res.status || '未知'));
if (res.status === 'completed') {
stopPolling();
$('#result').text('任务已完成!').css('color', 'green');
}
},
error: function (xhr, status, err) {
console.error('轮询请求失败:', err);
// 错误不立即停止,可以继续重试,也可以根据需求处理
}
});
}
function startPolling() {
pollServer(); // 立即发一次
pollTimer = setInterval(pollServer, 1000); // 每秒一次
isPolling = true;
$('#pollBtn').text('停止轮询');
$('#status').text('正在轮询服务器...');
}
function stopPolling() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
isPolling = false;
$('#pollBtn').text('开始轮询');
}
// 按钮点击切换
$('#pollBtn').on('click', function () {
if (!isPolling) {
startPolling();
} else {
stopPolling();
$('#status').text('轮询已手动停止');
}
});
</script>
</body>
</html>
这里有个细节值得注意:轮询型请求最怕”请求堆积”。如果网络慢,上一次请求还没回来,下一次又发出去了,服务器压力会很大。所以实际项目中,通常会用 $.ajax 的 beforeSend 或 start 回调做一个简单防重,或者改用 setTimeout 链式调用(等上一次请求完成后再发下一次),这样更稳妥。
五、进阶:用 setTimeout 替代 setInterval(更可控)
setInterval 有个隐藏坑:如果某次请求卡住了,定时器不会等,下一秒照样触发,容易导致请求风暴。改成 setTimeout 递归写法,就能保证”上一次完成 → 再等一秒 → 下一次”。
let timeoutId = null;
function pollWithTimeout() {
$.ajax({
url: '/api/check-task-status',
type: 'GET',
success: function (res) {
console.log('收到响应:', res);
if (res.status === 'completed') {
$('#status').text('任务完成,停止轮询');
return; // 不继续递归,自然停止
}
// 请求成功,1秒后再发下一次
timeoutId = setTimeout(pollWithTimeout, 1000);
},
error: function () {
// 出错也继续,3秒后重试(给网络恢复时间)
timeoutId = setTimeout(pollWithTimeout, 3000);
}
});
}
// 开始
function startPolling() {
if (!timeoutId) {
pollWithTimeout();
}
}
// 停止
function stopPolling() {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
}
这种写法在真实项目里更常见,尤其是需要”请求串行”的场景。
六、把逻辑封装成可复用的组件
如果你要在多个页面用,别每次都抄一遍代码,封装一下。
/**
* 简单的轮询工具
* @param {Object} options
* @param {string} options.url 请求地址
* @param {Function} options.onSuccess 成功回调,参数是响应数据
* @param {Function} options.onError 失败回调,参数是错误信息
* @param {Function} options.onComplete 完成回调(手动或自动停止时触发)
* @param {number} options.interval 间隔毫秒数,默认1000
*/
function createPoller(options) {
let timerId = null;
let isRunning = false;
function stop() {
if (timerId) {
clearInterval(timerId);
timerId = null;
}
isRunning = false;
if (options.onComplete) options.onComplete();
}
function tick() {
$.ajax({
url: options.url,
type: 'GET',
success: function (res) {
if (options.onSuccess) options.onSuccess(res);
},
error: function (xhr, status, err) {
if (options.onError) options.onError(err);
}
});
}
function start() {
if (isRunning) return;
isRunning = true;
tick(); // 立即执行一次
timerId = setInterval(tick, options.interval || 1000);
}
return {
start: start,
stop: stop,
isRunning: function () { return isRunning; }
};
}
// 使用示例
const myPoller = createPoller({
url: '/api/check-task-status',
interval: 1000,
onSuccess: function (res) {
console.log('收到数据:', res);
$('#status').text('最新状态:' + (res.status || '未知'));
if (res.status === 'completed') {
myPoller.stop();
$('#result').text('任务已完成').css('color', 'green');
}
},
onError: function (err) {
console.error('轮询出错:', err);
},
onComplete: function () {
console.log('轮询已停止');
}
});
// 按钮绑定
$('#pollBtn').on('click', function () {
if (myPoller.isRunning()) {
myPoller.stop();
$(this).text('开始轮询');
} else {
myPoller.start();
$(this).text('停止轮询');
}
});
这样写的好处是:每次调用 createPoller 都会生成一个独立的定时器实例,互不干扰。你页面里就算有好几个轮询任务,也不会乱。
七、常见问题排查
问题1:点了按钮,定时器没停?
检查 clearInterval(timerId) 里的 timerId 是不是和 setInterval 里存的是同一个变量。很多人把 timerId 写在 click 函数里面,每次点击都是新变量,当然停不掉。
问题2:请求发出去了,但响应晚了,导致请求堆积?
换用前面说的 setTimeout 递归写法,或者在请求开始时禁用按钮、请求完成后再启用。
问题3:页面切换后定时器还在跑,浪费资源?
在 window.onbeforeunload 或路由切换钩子里调用 stop(),养成好习惯。
问题4:想限制最大轮询次数?
加个计数器,达到上限就 stop(),逻辑和上面示例里的 requestCount >= 5 一样。
八、小结
核心就三句话:
- 开启:
timerId = setInterval(fn, 1000),先立即执行一次再设定时器。 - 停止:
clearInterval(timerId),传当初返回的那个ID。 - 切换:用一个布尔变量记录状态,按钮点击时判断是开还是关。
把这三步写清楚,其他花哨的东西都是在此基础上加的条件判断而已。希望这个讲解能帮你把思路理清楚,真正动手写的时候就不会慌了。
