窗口事件
窗口事件(Window Events)在浏览器窗口或页面发生状态变化时触发,包括页面加载完成、窗口大小改变、页面滚动、页面关闭等。窗口事件通常绑定在 window 或 document 对象上,是构建响应式布局、保存用户状态和优化用户体验的重要工具。
前置知识
阅读本节前,建议先了解:表单事件
基础概念
窗口事件类型
| 事件 | 触发时机 | 绑定对象 | 说明 |
|---|---|---|---|
load | 页面资源加载完成 | window | 所有资源(图片、脚本等)加载后 |
DOMContentLoaded | DOM 解析完成 | document | HTML 解析完毕,不等待样式和图片 |
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
| 特性 | DOMContentLoaded | load |
|---|---|---|
| 触发时机 | 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">↑</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);下一节
继续学习:拖放事件