JavaScript setTimeout() 方法指南:定时执行的秘密武器
一、setTimeout 是什么
setTimeout 是 JavaScript 中用于延迟执行某个函数的方法。就是"过一会儿再做某件事"。它属于 Web API,由浏览器提供(Node.js 中也有对应的实现)。
// 最简单的例子:2秒后弹出提示
setTimeout(() => {
console.log('2秒时间到了!');
}, 2000);
console.log('定时器已设置,继续执行他代码');
// 输出顺序:
// "定时器已设置,继续执行他代码"
// (2秒后)"2秒时间到了!"
二、基本语法和参数
let timerId = setTimeout(函数, 延迟时间, 参数1, 参数2, ...);
| 参数 | 说明 |
|---|---|
| 函数 | 要执行的函数(可以是函数名或箭头函数) |
| 延迟时间 | 单位毫秒,1000 = 1秒,默认0 |
| 参数1... | 传给函数的参数(可选) |
// 带参数的例子
function greet(name, greeting) {
console.log(`${greeting},${name}!`);
}
// 3秒后执行,并传递参数
setTimeout(greet, 3000, '张三', '早上好');
// 3秒后输出:"早上好,张三!"
// 用箭头函数也可以
setTimeout((msg) => {
console.log(`收到消息:${msg}`);
}, 2000, 'Hello World');
三、实际应用场景
1. 延迟提示或通知
class NotificationManager {
constructor() {
this.notifications = [];
}
showMessage(msg, duration = 3000) {
console.log(`[通知] ${msg}`);
// 延迟后自动消失
setTimeout(() => {
console.log(`[通知消失] ${msg}`);
this.notifications = this.notifications.filter(n => n !== msg);
}, duration);
this.notifications.push(msg);
}
showDelayedMessage(msg, delay = 2000, duration = 3000) {
console.log(`消息已排队:${msg},将在${delay}ms后显示`);
setTimeout(() => {
this.showMessage(msg, duration);
}, delay);
}
}
// 使用示例
const notifier = new NotificationManager();
notifier.showMessage('欢迎登录系统'); // 立即显示,3秒后消失
notifier.showDelayedMessage('您有一条新消息', 5000); // 5秒后显示
2. 搜索输入防抖
class SearchBox {
constructor(inputId) {
this.input = document.getElementById(inputId);
this.searchTimeout = null;
this.setupListener();
}
setupListener() {
this.input.addEventListener('input', (e) => {
const keyword = e.target.value;
// 清除之前的定时器
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
// 设置新的定时器:用户停止输入500ms后才搜索
this.searchTimeout = setTimeout(() => {
this.performSearch(keyword);
}, 500);
});
}
performSearch(keyword) {
if (keyword.length < 2) {
console.log('关键词太短,不搜索');
return;
}
console.log(`正在搜索:${keyword}`);
// 这里可以发AJAX请求
// fetch(`/api/searchq=${keyword}`)
}
}
// 使用(假设有id为"searchInput"的输入框)
// const search = new SearchBox('searchInput');
3. 轮询操作
class PollingManager {
constructor(interval = 5000) {
this.interval = interval;
this.timeoutId = null;
this.isPolling = false;
}
// 开始轮询
startPolling() {
if (this.isPolling) return;
this.isPolling = true;
console.log('开始轮询...');
const poll = () => {
if (!this.isPolling) return;
// 执行轮询操作
this.checkForUpdates();
// 设置下一次轮询
this.timeoutId = setTimeout(poll, this.interval);
};
// 立即执行第一次
poll();
}
// 停止轮询
stopPolling() {
this.isPolling = false;
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
console.log('轮询已停止');
}
}
checkForUpdates() {
const now = new Date();
console.log(`[${now.toLocaleTimeString()}] 检查更新...`);
// 模拟检查更新的逻辑
if (Math.random() > 0.7) {
console.log('发现新内容!');
}
}
}
// 使用示例
const poller = new PollingManager(3000); // 每3秒检查一次
// poller.startPolling();
// 10秒后停止
setTimeout(() => {
poller.stopPolling();
}, 10000);
4. 动画效果
class FadeAnimation {
constructor(element) {
this.element = element;
this.opacity = 1;
}
fadeOut(duration = 1000) {
const steps = 20; // 分20步完成
const interval = duration / steps;
const step = 1 / steps;
const fade = () => {
this.opacity -= step;
this.element.style.opacity = this.opacity;
if (this.opacity > 0) {
setTimeout(fade, interval);
} else {
this.element.style.display = 'none';
console.log('淡出完成');
}
};
fade();
}
fadeIn(duration = 1000) {
this.element.style.display = 'block';
this.opacity = 0;
this.element.style.opacity = 0;
const steps = 20;
const interval = duration / steps;
const step = 1 / steps;
const fade = () => {
this.opacity += step;
this.element.style.opacity = this.opacity;
if (this.opacity < 1) {
setTimeout(fade, interval);
} else {
console.log('淡入完成');
}
};
fade();
}
}
// 使用(假设有id为"box"的元素)
// const box = document.getElementById('box');
// const anim = new FadeAnimation(box);
// anim.fadeOut(2000);
四、clearTimeout 取消定时器
setTimeout 返回一个 ID,用 clearTimeout 可以取消执行:
console.log('启动定时器...');
const timerId = setTimeout(() => {
console.log('这行不会被执行');
}, 5000);
// 3秒后取消(在5秒之前)
setTimeout(() => {
clearTimeout(timerId);
console.log('定时器已取消');
}, 3000);
// 实际应用:取消操作
class UploadManager {
constructor() {
this.uploadTimeout = null;
this.uploadStatus = 'pending';
}
startUpload(file) {
console.log(`开始上传:${file.name}`);
this.uploadStatus = 'uploading';
// 设置超时处理
this.uploadTimeout = setTimeout(() => {
if (this.uploadStatus === 'uploading') {
this.uploadStatus = 'timeout';
console.log(`上传超时:${file.name}`);
this.cancelUpload('上传时间过长');
}
}, 30000); // 30秒超时
// 模拟上传过程
this.simulateUpload(file);
}
simulateUpload(file) {
// 假设上传需要一些时间
setTimeout(() => {
if (this.uploadStatus === 'uploading') {
this.uploadStatus = 'completed';
clearTimeout(this.uploadTimeout); // 取消超时定时器
console.log(`上传完成:${file.name}`);
}
}, 15000); // 15秒完成
}
cancelUpload(reason) {
if (this.uploadTimeout) {
clearTimeout(this.uploadTimeout);
this.uploadTimeout = null;
}
this.uploadStatus = 'cancelled';
console.log(`上传已取消:${reason}`);
}
}
// 使用示例
const uploader = new UploadManager();
const file = { name: 'document.pdf', size: 1024 };
uploader.startUpload(file);
五、setTimeout 的进阶技巧
1. 递归 setTimeout 实现 setInterval
// setInterval 存在的问题
let count = 0;
const intervalId = setInterval(() => {
console.log(`第${++count}次执行`);
// 如果这里有个耗时操作
const start = Date.now();
while (Date.now() - start < 2000) {
// 模拟耗时2秒
}
// setInterval 会在2秒后继续调用,导致重叠执行
}, 1000);
// 更好的做法:递归 setTimeout
class RecursiveTimer {
constructor() {
this.count = 0;
this.timeoutId = null;
this.isRunning = false;
}
start() {
if (this.isRunning) return;
this.isRunning = true;
this.schedule();
}
schedule() {
if (!this.isRunning) return;
this.timeoutId = setTimeout(() => {
this.execute();
this.schedule(); // 执行完后安排下一次
}, 1000);
}
execute() {
const now = new Date();
console.log(`[${now.toLocaleTimeString()}] 执行第${++this.count}次任务`);
// 即使这里有耗时操作,也不会影响下一次调用的间隔计算
// const start = Date.now();
// while (Date.now() - start < 2000) { }
}
stop() {
this.isRunning = false;
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
}
}
// 使用
const timer = new RecursiveTimer();
// timer.start();
// 10秒后停止
// setTimeout(() => timer.stop(), 10000);
2. 延迟加载和懒执行
class LazyLoader {
constructor() {
this.loadedResources = new Set();
}
// 延迟加载资源
loadResource(resource, delay = 0) {
console.log(`资源 ${resource.name} 计划在 ${delay}ms 后加载`);
setTimeout(() => {
if (!this.loadedResources.has(resource.name)) {
// 模拟加载资源
console.log(`正在加载:${resource.name}`);
this.loadedResources.add(resource.name);
// 资源加载完成后的回调
if (resource.callback) {
resource.callback(resource);
}
}
}, delay);
}
// 批量延迟加载
loadResources(resources, baseDelay = 1000) {
resources.forEach((resource, index) => {
const delay = baseDelay * (index + 1);
this.loadResource(resource, delay);
});
}
}
// 使用示例
const loader = new LazyLoader();
const resources = [
{ name: '图片1.jpg', type: 'image', size: '2MB' },
{ name: '图片2.jpg', type: 'image', size: '3MB' },
{ name: '视频.mp4', type: 'video', size: '50MB', callback: (r) => {
console.log(`${r.name} 加载完成,开始播放`);
}},
{ name: '文档.pdf', type: 'document', size: '1MB' }
];
// 每隔1秒加载一个资源
loader.loadResources(resources, 1000);
3. 实现简单的任务队列
class TaskQueue {
constructor(concurrency = 1) {
this.queue = [];
this.running = 0;
this.concurrency = concurrency;
this.delayedTasks = [];
}
addTask(task, delay = 0) {
if (delay > 0) {
// 延迟添加任务
setTimeout(() => {
this.queue.push(task);
this.processQueue();
}, delay);
} else {
this.queue.push(task);
this.processQueue();
}
}
processQueue() {
if (this.running >= this.concurrency || this.queue.length === 0) {
return;
}
const task = this.queue.shift();
this.running++;
console.log(`开始执行任务,当前并发:${this.running}`);
// 模拟异步任务
setTimeout(() => {
task();
this.running--;
console.log(`任务完成,剩余并发:${this.running}`);
this.processQueue(); // 继续处理下一个
}, Math.random() * 2000 + 1000);
}
addDelayedTask(task, executeDelay) {
console.log(`任务将在 ${executeDelay}ms 后执行`);
setTimeout(() => {
this.addTask(task);
}, executeDelay);
}
}
// 使用示例
const queue = new TaskQueue(2); // 同时执行2个任务
for (let i = 1; i <= 5; i++) {
queue.addTask(() => {
console.log(`执行任务 ${i}`);
}, i * 1000); // 每个任务延迟不同时间加入队列
}
queue.addDelayedTask(() => {
console.log('这是延迟添加的特殊任务');
}, 8000);
六、常见和注意事项
1. this 指向问题
// 错误示例
const obj = {
name: '测试对象',
greet: function() {
setTimeout(function() {
console.log(`Hello, ${this.name}`); // this 指向 window/undefined
}, 1000);
}
};
obj.greet(); // 输出: Hello, undefined
// 解决方案1:箭头函数
const obj2 = {
name: '测试对象',
greet: function() {
setTimeout(() => {
console.log(`Hello, ${this.name}`); // this 继承自外部
}, 1000);
}
};
// 解决方案2:bind
const obj3 = {
name: '测试对象',
greet: function() {
setTimeout(function() {
console.log(`Hello, ${this.name}`);
}.bind(this), 1000);
}
};
// 解决方案3:保存 this
const obj4 = {
name: '测试对象',
greet: function() {
const self = this;
setTimeout(function() {
console.log(`Hello, ${self.name}`);
}, 1000);
}
};
2. 小延迟时间
// 实际小延迟是4ms
console.time('timer1');
setTimeout(() => {
console.timeEnd('timer1'); // 显示4ms以上
}, 0);
// 嵌套超时的小延迟
let start = Date.now();
setTimeout(function nested() {
const end = Date.now();
console.log(`实际延迟:${end - start}ms`);
if (end - start < 100) {
start = end;
setTimeout(nested, 0);
}
}, 0);
3. 定时器不准的问题
// setTimeout 不保证精确的时间
class AccurateTimer {
constructor(callback, interval) {
this.callback = callback;
this.interval = interval;
this.expected = 0;
this.timeoutId = null;
}
start() {
this.expected = Date.now() + this.interval;
this.schedule();
}
schedule() {
const drift = Date.now() - this.expected;
this.timeoutId = setTimeout(() => {
this.callback();
this.expected += this.interval;
this.schedule();
}, Math.max(0, this.interval - drift));
}
stop() {
clearTimeout(this.timeoutId);
}
}
// 使用
const timer = new AccurateTimer(() => {
console.log('滴答', new Date().toLocaleTimeString());
}, 1000);
// timer.start();
// 10秒后停止
// setTimeout(() => timer.stop(), 10000);
七、性能优化和实践
1. 清理不再需要的定时器
class Component {
constructor() {
this.timers = new Set();
}
setTimeout(callback, delay) {
const id = setTimeout(() => {
this.timers.delete(id);
callback();
}, delay);
this.timers.add(id);
return id;
}
clearAllTimeouts() {
this.timers.forEach(id => clearTimeout(id));
this.timers.clear();
console.log('所有定时器已清理');
}
// 组件销毁时调用
destroy() {
this.clearAllTimeouts();
}
}
// 使用
const comp = new Component();
comp.setTimeout(() => console.log('任务1'), 1000);
comp.setTimeout(() => console.log('任务2'), 2000);
comp.setTimeout(() => console.log('任务3'), 3000);
// 组件被销毁
setTimeout(() => {
comp.destroy();
}, 2500);
// 任务3不会执行
2. 使用 requestAnimationFrame 代替 setTimeout 做动画
class SmoothAnimation {
constructor(element) {
this.element = element;
this.position = 0;
this.requestId = null;
}
// 不好的做法:用 setTimeout 做动画
badAnimate(target, duration = 1000) {
const start = this.position;
const startTime = Date.now();
const step = () => {
const elapsed = Date.now() - startTime;
this.position = start + (target - start) * (elapsed / duration);
this.element.style.transform = `translateX(${this.position}px)`;
if (elapsed < duration) {
setTimeout(step, 16); // 约60fps
}
};
step();
}
// 好的做法:用 requestAnimationFrame
goodAnimate(target, duration = 1000) {
const start = this.position;
const startTime = performance.now();
const step = (currentTime) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
this.position = start + (target - start) * progress;
this.element.style.transform = `translateX(${this.position}px)`;
if (progress < 1) {
this.requestId = requestAnimationFrame(step);
}
};
this.requestId = requestAnimationFrame(step);
}
stop() {
if (this.requestId) {
cancelAnimationFrame(this.requestId);
}
}
}
八、面试常见问题
// 1. 下面的代码输出什么?
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// 输出顺序:1, 3, 2
// 2. 如何实现 sleep 函数?
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function () {
console.log('开始');
await sleep(2000);
console.log('2秒后');
}
// 3. setTimeout 和 setInterval 的区别
function showDifference() {
// setInterval:固定间隔执行
setInterval(() => {
console.log('setInterval 每1秒执行一次');
}, 1000);
// setTimeout 递归:上一个执行完后再等1秒
function timeoutLoop() {
setTimeout(() => {
console.log('setTimeout 循环执行');
timeoutLoop();
}, 1000);
}
timeoutLoop();
}
九、学习编程的建议
学习 setTimeout 可以按照这个顺序:
-
先掌握基础用法:延迟执行、取消定时器
-
理解异步执行机制:事件循环、宏任务
-
结合实际场景练习:防抖、轮询、动画
-
注意常见:this指向、内存泄漏
-
对比他方案:requestAnimationFrame、Web Worker
// 练习:实现一个简单的倒计时
class Countdown {
constructor(seconds, onTick, onComplete) {
this.remaining = seconds;
this.onTick = onTick;
this.onComplete = onComplete;
this.timeoutId = null;
}
start() {
this.tick();
}
tick() {
if (this.remaining <= 0) {
this.onComplete.();
return;
}
this.onTick.(this.remaining);
this.remaining--;
this.timeoutId = setTimeout(() => this.tick(), 1000);
}
stop() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
}
}
new Countdown(
5,
(remaining) => console.log(`剩余:${remaining}秒`),
() => console.log('倒计时结束!')
).start();
setTimeout 看似简单,但深入理解后会发现它涉及 JavaScript 事件循环、异步编程、性能优化等多个重要概念。掌握好它,对理解整个 JavaScript 运行机制都有帮助。
记住:定时器不是精确的时间控制工具,而是"至少延迟指定时间后执行"的机制。在需要精确计时的场景(如动画、游戏),要考虑使用更合适的技术。