Skip to content

窗口事件

窗口事件(Window Events)在浏览器窗口或页面发生状态变化时触发,包括页面加载完成、窗口大小改变、页面滚动、页面关闭等。窗口事件通常绑定在 windowdocument 对象上,是构建响应式布局、保存用户状态和优化用户体验的重要工具。

前置知识

阅读本节前,建议先了解:表单事件

基础概念

窗口事件类型

事件触发时机绑定对象说明
load页面资源加载完成window所有资源(图片、脚本等)加载后
DOMContentLoadedDOM 解析完成documentHTML 解析完毕,不等待样式和图片
beforeunload页面即将关闭window用户关闭/刷新页面时
unload页面正在卸载window页面关闭时(不可靠)
resize窗口大小改变window浏览器窗口尺寸变化时
scroll页面滚动window/element滚动位置变化时
error资源加载失败window图片、脚本等加载出错
html
<!-- HTML 属性方式 -->
<body onload="initPage()" onbeforeunload="return confirmLeave()" onresize="handleResize()">

<!-- JavaScript 方式 -->
<script>
  window.addEventListener('load', initPage);
  window.addEventListener('resize', debounce(handleResize, 200));
</script>

load vs DOMContentLoaded

特性DOMContentLoadedload
触发时机DOM 树构建完成所有资源加载完成
等待图片
等待 CSS是(阻塞渲染)
等待 JS是(阻塞解析)
推荐使用操作 DOM需要图片尺寸等

语法

HTML 事件属性

html
<body onload="init()" onunload="cleanup()" onresize="adjustLayout()">
  <img onerror="this.src='fallback.jpg'" src="photo.jpg">
</body>

JavaScript 绑定

javascript
// DOM 加载完成(推荐)
document.addEventListener('DOMContentLoaded', () => {
  console.log('DOM 已就绪,可以操作元素');
});

// 所有资源加载完成
window.addEventListener('load', () => {
  console.log('页面完全加载,包括图片和样式');
});

// 窗口大小变化
window.addEventListener('resize', () => {
  console.log('窗口尺寸:', window.innerWidth, window.innerHeight);
});

// 页面滚动
window.addEventListener('scroll', () => {
  console.log('滚动位置:', window.scrollY);
});

详细说明

DOMContentLoaded

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>DOMContentLoaded 示例</title>
  <!-- 脚本放在 head 中,使用 DOMContentLoaded 确保可以安全操作 DOM -->
  <script>
    document.addEventListener('DOMContentLoaded', () => {
      // 此时 DOM 已就绪,可以安全操作
      const title = document.getElementById('title');
      title.textContent = 'DOM 已加载完毕';
      title.style.color = '#22c55e';
    });
  </script>
</head>
<body>
  <h1 id="title">加载中...</h1>
</body>
</html>

resize 事件(防抖)

javascript
// resize 事件触发频繁,需要防抖
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

const handleResize = debounce(() => {
  const width = window.innerWidth;
  console.log('窗口宽度:', width);

  if (width < 768) {
    document.body.classList.add('mobile-layout');
  } else {
    document.body.classList.remove('mobile-layout');
  }
}, 250);

window.addEventListener('resize', handleResize);

scroll 事件(节流)

javascript
// scroll 事件需要节流处理
function throttle(fn, delay) {
  let last = 0;
  return function(...args) {
    const now = Date.now();
    if (now - last >= delay) {
      last = now;
      fn.apply(this, args);
    }
  };
}

// 回到顶部按钮
const backToTop = document.getElementById('back-to-top');

window.addEventListener('scroll', throttle(() => {
  if (window.scrollY > 300) {
    backToTop.style.display = 'block';
  } else {
    backToTop.style.display = 'none';
  }
}, 100));

backToTop.addEventListener('click', () => {
  window.scrollTo({ top: 0, behavior: 'smooth' });
});

beforeunload 事件

javascript
// 防止用户意外关闭页面
window.addEventListener('beforeunload', (e) => {
  // 如果有未保存的更改
  if (hasUnsavedChanges) {
    e.preventDefault();
    // 现代浏览器会自动显示确认对话框
    e.returnValue = ''; // Chrome 需要
  }
});

// 更安全的做法:使用 visibilitychange 保存状态
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    // 页面切换到后台时自动保存
    autoSave();
  }
});

实战示例

完整的响应式布局

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>窗口事件响应式示例</title>
  <style>
    .layout-info {
      position: fixed;
      top: 16px;
      right: 16px;
      background: rgba(0, 0, 0, 0.8);
      color: white;
      padding: 12px 16px;
      border-radius: 8px;
      font-family: monospace;
      font-size: 13px;
      z-index: 9999;
    }
    .scroll-indicator {
      position: fixed;
      top: 0;
      left: 0;
      height: 3px;
      background: #3b82f6;
      z-index: 9999;
      transition: width 0.1s;
    }
    .back-to-top {
      position: fixed;
      bottom: 24px;
      right: 24px;
      width: 48px;
      height: 48px;
      border-radius: 50%;
      background: #3b82f6;
      color: white;
      border: none;
      cursor: pointer;
      display: none;
      font-size: 20px;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
    }
    .content {
      height: 300vh;
      padding: 40px;
    }
  </style>
</head>
<body>
  <div class="scroll-indicator" id="scroll-bar"></div>
  <div class="layout-info" id="layout-info"></div>
  <button class="back-to-top" id="back-top">&uarr;</button>
  <div class="content" id="content"></div>

  <script>
    // DOM 加载后初始化
    document.addEventListener('DOMContentLoaded', () => {
      const info = document.getElementById('layout-info');
      const scrollBar = document.getElementById('scroll-bar');
      const backTop = document.getElementById('back-top');
      const content = document.getElementById('content');

      // 生成内容
      for (let i = 1; i <= 30; i++) {
        content.innerHTML += '<p>这是第 ' + i + ' 段内容。</p>';
      }

      // 更新布局信息
      function updateLayoutInfo() {
        info.innerHTML =
          '窗口: ' + window.innerWidth + 'x' + window.innerHeight +
          '<br>滚动: ' + Math.round(window.scrollY) + 'px' +
          '<br>设备: ' + (window.innerWidth < 768 ? '移动端' : '桌面端');
      }

      // resize 防抖
      let resizeTimer;
      window.addEventListener('resize', () => {
        clearTimeout(resizeTimer);
        resizeTimer = setTimeout(updateLayoutInfo, 200);
      });

      // scroll 节流
      let lastScroll = 0;
      window.addEventListener('scroll', () => {
        const scrollY = window.scrollY;
        const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
        const progress = (scrollY / maxScroll) * 100;

        scrollBar.style.width = progress + '%';
        backTop.style.display = scrollY > 300 ? 'block' : 'none';
        lastScroll = scrollY;
        updateLayoutInfo();
      });

      // 回到顶部
      backTop.addEventListener('click', () => {
        window.scrollTo({ top: 0, behavior: 'smooth' });
      });

      updateLayoutInfo();
    });
  </script>
</body>
</html>

注意事项

避免在 scroll/resize 中执行重计算

javascript
// 错误:每次滚动都读取布局属性(触发重排)
window.addEventListener('scroll', () => {
  const height = document.body.scrollHeight; // 强制重排
  // 处理逻辑...
});

// 正确:缓存值,使用 requestAnimationFrame
let scrollHeight = document.body.scrollHeight;
window.addEventListener('scroll', () => {
  requestAnimationFrame(() => {
    scrollHeight = document.body.scrollHeight;
    // 处理逻辑...
  });
});

unload 事件不可靠

javascript
// 不推荐:unload 事件在现代浏览器中不可靠
//(特别是移动端浏览器)
window.addEventListener('unload', () => {
  // 可能不会执行
  saveData();
});

// 推荐:使用 beforeunload + visibilitychange + IndexedDB
window.addEventListener('beforeunload', (e) => {
  if (hasUnsavedChanges) {
    e.preventDefault();
    e.returnValue = '';
  }
});

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    saveDataToIndexedDB();
  }
});

最佳实践

1. 使用 DOMContentLoaded 代替 body.onload

javascript
// 推荐:DOMContentLoaded 更早触发
document.addEventListener('DOMContentLoaded', init);

// 不推荐:等待所有资源
window.addEventListener('load', init);

2. 使用 IntersectionObserver 代替 scroll 监听

javascript
// 推荐:IntersectionObserver 检测元素可见性
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('visible');
    }
  });
});

document.querySelectorAll('.animate-on-scroll').forEach(el => {
  observer.observe(el);
});

// 不推荐:在 scroll 事件中手动计算
// window.addEventListener('scroll', checkVisibility);

下一节

继续学习:拖放事件

参考链接