Appearance
JavaScript 计时事件
什么是计时事件
计时事件是 JavaScript 中用于在指定时间后执行代码或重复执行代码的机制。通过计时事件,你可以实现延迟执行、动画效果、轮询等功能。
JavaScript 提供了两种主要的计时函数:
- setTimeout():在指定的毫秒数后执行一次函数
- setInterval():每隔指定的毫秒数重复执行函数
这两个函数都是 window 对象的方法,因此可以省略 window. 前缀。
setTimeout() 函数
setTimeout() 函数用于在指定的毫秒数后执行一次函数。
语法
javascript
window.setTimeout(function, milliseconds, param1, param2, ...);参数
function:要执行的函数milliseconds:延迟的毫秒数(1000 毫秒 = 1 秒)param1, param2, ...:(可选)传递给函数的参数
返回值
- 返回一个定时器 ID,可以用于后续通过
clearTimeout()取消定时器
示例
javascript
// 基本用法
function greet() {
console.log("Hello, World!");
}
// 2 秒后执行 greet 函数
const timeoutId = setTimeout(greet, 2000);
console.log("Timeout ID:", timeoutId);
// 使用匿名函数
setTimeout(function () {
console.log("This is an anonymous function");
}, 3000);
// 使用箭头函数(ES6+)
setTimeout(() => {
console.log("This is an arrow function");
}, 4000);
// 传递参数
function greetUser(name, message) {
console.log(`Hello, ${name}! ${message}`);
}
setTimeout(greetUser, 5000, "John", "How are you?");
// 取消定时器
const cancelTimeoutId = setTimeout(() => {
console.log("This should not be executed");
}, 6000);
// 取消定时器
clearTimeout(cancelTimeoutId);
console.log("Timeout cancelled");
// 绑定到按钮点击事件
document.getElementById("timeout-btn").addEventListener("click", () => {
setTimeout(() => {
alert("Button clicked 2 seconds ago!");
}, 2000);
});注意事项
setTimeout()的延迟时间不是精确的,可能会因为浏览器的其他任务而有所延迟- 最小延迟时间通常为 4 毫秒(根据 HTML5 规范)
- 在页面被置于后台标签页时,浏览器可能会延长延迟时间以节省资源
- 如果
milliseconds参数省略或为 0,则函数会尽快执行(但不是立即执行,而是放入事件队列)
setInterval() 函数
setInterval() 函数用于每隔指定的毫秒数重复执行函数。
语法
javascript
window.setInterval(function, milliseconds, param1, param2, ...);参数
function:要执行的函数milliseconds:重复执行的间隔毫秒数param1, param2, ...:(可选)传递给函数的参数
返回值
- 返回一个定时器 ID,可以用于后续通过
clearInterval()取消定时器
示例
javascript
// 基本用法
function tick() {
console.log("Tick!");
}
// 每秒执行一次 tick 函数
const intervalId = setInterval(tick, 1000);
console.log("Interval ID:", intervalId);
// 使用匿名函数
let count = 0;
const counterIntervalId = setInterval(function () {
count++;
console.log("Count:", count);
// 执行 5 次后停止
if (count >= 5) {
clearInterval(counterIntervalId);
console.log("Counter stopped");
}
}, 1000);
// 使用箭头函数
let seconds = 0;
const timerIntervalId = setInterval(() => {
seconds++;
console.log(`Timer: ${seconds} seconds`);
}, 1000);
// 5 秒后停止定时器
setTimeout(() => {
clearInterval(timerIntervalId);
console.log("Timer stopped");
}, 5000);
// 传递参数
function greetWithInterval(name) {
console.log(`Hello, ${name}!`);
}
const greetIntervalId = setInterval(greetWithInterval, 2000, "John");
// 6 秒后停止
setTimeout(() => {
clearInterval(greetIntervalId);
console.log("Greeting stopped");
}, 6000);
// 绑定到按钮点击事件
document.getElementById("start-interval").addEventListener("click", () => {
let intervalCount = 0;
const btnIntervalId = setInterval(() => {
intervalCount++;
console.log(`Button interval: ${intervalCount}`);
}, 1000);
// 存储 interval ID 以便停止
document.getElementById("start-interval").dataset.intervalId = btnIntervalId;
});
document.getElementById("stop-interval").addEventListener("click", () => {
const intervalId =
document.getElementById("start-interval").dataset.intervalId;
if (intervalId) {
clearInterval(intervalId);
console.log("Button interval stopped");
}
});注意事项
- 与
setTimeout()类似,setInterval()的间隔时间也不是精确的 setInterval()会按照指定的间隔时间重复执行,即使前一次执行还没有完成- 如果函数执行时间超过间隔时间,可能会导致函数调用堆积
- 因此,对于可能执行时间较长的操作,建议使用
setTimeout()递归调用代替setInterval()
clearTimeout() 和 clearInterval() 函数
clearTimeout() 和 clearInterval() 函数用于取消通过 setTimeout() 和 setInterval() 创建的定时器。
语法
javascript
window.clearTimeout(timeoutId);
window.clearInterval(intervalId);参数
timeoutId:通过setTimeout()返回的定时器 IDintervalId:通过setInterval()返回的定时器 ID
示例
javascript
// 取消 setTimeout
const timeoutId = setTimeout(() => {
console.log("This should not run");
}, 2000);
// 取消定时器
clearTimeout(timeoutId);
console.log("Timeout cancelled");
// 取消 setInterval
let count = 0;
const intervalId = setInterval(() => {
count++;
console.log("Count:", count);
}, 1000);
// 3 秒后取消
setTimeout(() => {
clearInterval(intervalId);
console.log("Interval cancelled");
}, 3000);注意事项
- 传递无效的 ID 给
clearTimeout()或clearInterval()不会产生错误,只是没有效果 - 为了避免内存泄漏,应该在不需要定时器时及时取消它们
- 特别注意在组件卸载或页面关闭时取消所有活动的定时器
递归 setTimeout() 与 setInterval() 的比较
对于需要重复执行的操作,有两种方法:
- 使用
setInterval() - 使用递归的
setTimeout()
setInterval() 的问题
setInterval() 会按照固定的间隔时间重复执行函数,即使前一次执行还没有完成。这可能会导致:
- 如果函数执行时间超过间隔时间,函数调用会堆积
- 如果函数中出现错误,可能会影响后续的执行
- 无法动态调整间隔时间
递归 setTimeout() 的优势
递归的 setTimeout() 是指在函数执行完毕后,再次调用 setTimeout() 来安排下一次执行。这种方式的优势:
- 确保函数执行完毕后才会安排下一次执行,避免调用堆积
- 如果函数中出现错误,不会影响后续的执行
- 可以动态调整间隔时间
示例比较
javascript
// 使用 setInterval()
console.log("=== Using setInterval() ===");
let intervalCount = 0;
const intervalId = setInterval(() => {
intervalCount++;
console.log(`Interval count: ${intervalCount}`);
// 模拟长时间执行
if (intervalCount === 3) {
console.log("Simulating long execution...");
// 注意:在实际代码中,不应该使用同步的长时间操作
// 这里只是为了演示
}
if (intervalCount >= 5) {
clearInterval(intervalId);
console.log("Interval stopped");
}
}, 1000);
// 使用递归 setTimeout()
console.log("\n=== Using recursive setTimeout() ===");
let timeoutCount = 0;
function recursiveTimeout() {
timeoutCount++;
console.log(`Timeout count: ${timeoutCount}`);
// 模拟长时间执行
if (timeoutCount === 3) {
console.log("Simulating long execution...");
}
if (timeoutCount < 5) {
// 递归调用,安排下一次执行
setTimeout(recursiveTimeout, 1000);
} else {
console.log("Recursive timeout stopped");
}
}
// 启动递归 setTimeout
setTimeout(recursiveTimeout, 1000);计时事件的应用场景
1. 延迟执行
javascript
// 页面加载后延迟执行
window.addEventListener("load", () => {
setTimeout(() => {
console.log("Page loaded 3 seconds ago");
// 可以在这里执行需要延迟的初始化操作
}, 3000);
});
// 按钮点击后延迟执行
document.getElementById("delayed-action").addEventListener("click", () => {
// 显示加载状态
const button = document.getElementById("delayed-action");
const originalText = button.textContent;
button.textContent = "Processing...";
button.disabled = true;
// 模拟异步操作
setTimeout(() => {
// 恢复按钮状态
button.textContent = originalText;
button.disabled = false;
// 显示结果
alert("Action completed!");
}, 2000);
});2. 动画效果
javascript
// 简单的动画效果
function animateElement(elementId) {
const element = document.getElementById(elementId);
let position = 0;
const speed = 5;
const direction = 1; // 1 = right, -1 = left
function animate() {
// 更新位置
position += speed * direction;
// 检查边界
if (position >= 300) {
direction = -1; // 改变方向向左
} else if (position <= 0) {
direction = 1; // 改变方向向右
}
// 应用位置
element.style.left = position + "px";
// 继续动画
requestAnimationFrame(animate);
}
// 开始动画
animate();
}
// 注意:对于复杂的动画,建议使用 CSS 动画或 requestAnimationFrame()
// 这里只是演示 setTimeout/setInterval 的原理3. 轮询
javascript
// 轮询服务器状态
function pollServerStatus() {
console.log("Polling server status...");
// 模拟向服务器请求状态
fetch("/api/status")
.then((response) => response.json())
.then((data) => {
console.log("Server status:", data.status);
// 根据状态更新 UI
if (data.status === "online") {
document.getElementById("status").textContent = "Online";
document.getElementById("status").className = "status-online";
} else {
document.getElementById("status").textContent = "Offline";
document.getElementById("status").className = "status-offline";
}
// 继续轮询
setTimeout(pollServerStatus, 5000); // 每 5 秒轮询一次
})
.catch((error) => {
console.error("Error polling server:", error);
// 出错后仍然继续轮询
setTimeout(pollServerStatus, 5000);
});
}
// 启动轮询
setTimeout(pollServerStatus, 1000);4. 倒计时
javascript
// 倒计时功能
function startCountdown(duration, displayElementId) {
const display = document.getElementById(displayElementId);
let timeLeft = duration;
function updateCountdown() {
// 计算分和秒
const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60;
// 格式化显示
display.textContent = `${minutes.toString().padStart(2, "0")}:${seconds
.toString()
.padStart(2, "0")}`;
// 减少时间
timeLeft--;
// 检查是否结束
if (timeLeft >= 0) {
// 继续倒计时
setTimeout(updateCountdown, 1000);
} else {
// 倒计时结束
display.textContent = "00:00";
alert("Countdown finished!");
}
}
// 开始倒计时
updateCountdown();
}
// 示例:10 分钟倒计时
document.getElementById("start-countdown").addEventListener("click", () => {
startCountdown(10 * 60, "countdown-display");
});5. 防抖(Debouncing)
防抖是一种优化技术,用于限制函数在短时间内被频繁调用,例如在输入框输入时。
javascript
// 防抖函数
function debounce(func, wait) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
// 示例:搜索输入防抖
const searchInput = document.getElementById("search-input");
const searchResults = document.getElementById("search-results");
// 防抖处理的搜索函数
const debouncedSearch = debounce((query) => {
console.log("Searching for:", query);
// 模拟搜索
if (query.length > 0) {
searchResults.textContent = `Search results for "${query}"`;
} else {
searchResults.textContent = "";
}
}, 300);
// 监听输入事件
searchInput.addEventListener("input", (e) => {
debouncedSearch(e.target.value);
});6. 节流(Throttling)
节流是另一种优化技术,用于限制函数在单位时间内最多执行一次,例如在滚动事件中。
javascript
// 节流函数
function throttle(func, limit) {
let inThrottle;
return function () {
const context = this;
const args = arguments;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// 示例:滚动事件节流
const throttleScroll = throttle(() => {
console.log("Scroll position:", window.scrollY);
// 可以在这里执行滚动相关的操作
}, 100);
// 监听滚动事件
window.addEventListener("scroll", throttleScroll);requestAnimationFrame()
对于动画效果,推荐使用 requestAnimationFrame() 而不是 setTimeout() 或 setInterval()。requestAnimationFrame() 是浏览器提供的 API,用于优化动画性能。
语法
javascript
window.requestAnimationFrame(callback);参数
callback:每帧执行的函数,接收一个参数timestamp,表示当前时间戳
返回值
- 返回一个 ID,可以用于通过
cancelAnimationFrame()取消动画
示例
javascript
// 使用 requestAnimationFrame 实现动画
function animateElement(elementId) {
const element = document.getElementById(elementId);
let position = 0;
const speed = 2;
function animate(timestamp) {
// 更新位置
position += speed;
// 检查边界
if (position >= window.innerWidth - 100) {
position = 0; // 重置位置
}
// 应用位置
element.style.left = position + "px";
// 继续动画
animationId = requestAnimationFrame(animate);
}
// 开始动画
let animationId = requestAnimationFrame(animate);
// 存储 animationId 以便后续取消
element.dataset.animationId = animationId;
}
// 取消动画
function cancelAnimation(elementId) {
const element = document.getElementById(elementId);
const animationId = element.dataset.animationId;
if (animationId) {
cancelAnimationFrame(animationId);
console.log("Animation cancelled");
}
}
// 示例:启动和取消动画
document.getElementById("start-animation").addEventListener("click", () => {
animateElement("animated-box");
});
document.getElementById("stop-animation").addEventListener("click", () => {
cancelAnimation("animated-box");
});优势
requestAnimationFrame()会根据浏览器的刷新频率来调整执行时间,通常为 60 FPS- 当页面在后台标签页时,
requestAnimationFrame()会暂停执行,节省资源 requestAnimationFrame()可以与 CSS 动画更好地同步- 对于复杂的动画,
requestAnimationFrame()通常比setTimeout()或setInterval()性能更好
最佳实践
1. 始终存储定时器 ID
当创建定时器时,始终存储返回的 ID,以便后续可以取消它:
javascript
// 推荐
const timeoutId = setTimeout(() => {
// 代码
}, 1000);
// 后续可以取消
clearTimeout(timeoutId);
// 不推荐
setTimeout(() => {
// 代码
}, 1000);
// 无法取消2. 及时取消定时器
在不需要定时器时,及时取消它,以避免内存泄漏和不必要的执行:
javascript
// 组件挂载时创建定时器
let intervalId;
function componentDidMount() {
intervalId = setInterval(() => {
// 代码
}, 1000);
}
// 组件卸载时取消定时器
function componentWillUnmount() {
if (intervalId) {
clearInterval(intervalId);
}
}3. 使用递归 setTimeout() 代替 setInterval()
对于需要重复执行的操作,特别是可能执行时间较长的操作,建议使用递归的 setTimeout() 代替 setInterval():
javascript
// 推荐:递归 setTimeout
function repeatTask() {
// 执行任务
console.log("Task executed");
// 安排下一次执行
setTimeout(repeatTask, 1000);
}
// 开始执行
setTimeout(repeatTask, 1000);
// 不推荐:setInterval
setInterval(() => {
console.log("Task executed");
}, 1000);4. 对于动画使用 requestAnimationFrame()
对于动画效果,推荐使用 requestAnimationFrame() 而不是 setTimeout() 或 setInterval():
javascript
// 推荐
function animate() {
// 动画逻辑
requestAnimationFrame(animate);
}
// 开始动画
requestAnimationFrame(animate);
// 不推荐
setInterval(() => {
// 动画逻辑
}, 16); // 约 60 FPS5. 避免在定时器中使用全局状态
尽量避免在定时器回调中依赖或修改全局状态,这可能会导致竞态条件和难以调试的问题:
javascript
// 不推荐
let globalCount = 0;
setInterval(() => {
globalCount++;
console.log(globalCount);
}, 1000);
// 推荐
function createCounter() {
let count = 0;
return {
start: function () {
return setInterval(() => {
count++;
console.log(count);
}, 1000);
},
};
}
const counter = createCounter();
const intervalId = counter.start();6. 考虑使用 Promise 包装定时器
对于现代 JavaScript,可以使用 Promise 包装定时器,使代码更加清晰:
javascript
// 使用 Promise 包装 setTimeout
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// 使用 async/await
async function demo() {
console.log("Start");
await delay(2000);
console.log("After 2 seconds");
await delay(1000);
console.log("After another second");
}
// 调用
demo();
// 使用 Promise 链
delay(1000)
.then(() => {
console.log("After 1 second");
return delay(1000);
})
.then(() => {
console.log("After another second");
});总结
JavaScript 的计时事件是实现延迟执行、重复执行和动画效果的重要工具。通过 setTimeout() 和 setInterval() 函数,你可以控制代码的执行时间和频率。
在使用计时事件时,应该遵循最佳实践:
- 始终存储定时器 ID 以便取消
- 及时取消不需要的定时器
- 对于重复执行的操作,考虑使用递归的
setTimeout() - 对于动画效果,使用
requestAnimationFrame() - 避免在定时器中使用全局状态
- 考虑使用 Promise 包装定时器以提高代码可读性
通过合理使用计时事件,你可以创建更加交互性和响应性的网页应用。