Skip to content

JavaScript 计时事件

什么是计时事件

计时事件是 JavaScript 中用于在指定时间后执行代码或重复执行代码的机制。通过计时事件,你可以实现延迟执行、动画效果、轮询等功能。

JavaScript 提供了两种主要的计时函数:

  1. setTimeout():在指定的毫秒数后执行一次函数
  2. 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() 返回的定时器 ID
  • intervalId:通过 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() 的比较

对于需要重复执行的操作,有两种方法:

  1. 使用 setInterval()
  2. 使用递归的 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 FPS

5. 避免在定时器中使用全局状态

尽量避免在定时器回调中依赖或修改全局状态,这可能会导致竞态条件和难以调试的问题:

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 包装定时器以提高代码可读性

通过合理使用计时事件,你可以创建更加交互性和响应性的网页应用。