pin_drop当前位置:知识文库 ❯ 图文
JS集成:HTML onload事件:w - 详细教程与实战指南
教程简介
页面加载是Web应用生命周期的起点,理解各种加载事件的触发时机和区别,对于正确初始化应用、优化加载性能至关重要。onload事件是最常用的页面加载事件,但它只是众多加载事件之一。DOMContentLoaded在DOM树构建完成时触发,远早于load事件;readystatechange提供了更细粒度的加载状态追踪;图片和子资源的加载也有独立的事件机制。本教程将全面讲解HTML onload及相关加载事件,帮助开发者掌握页面加载的完整生命周期,实现高效的资源加载策略。
核心概念
页面加载的完整生命周期
浏览器加载一个HTML页面的过程如下:
代码示例
1. 开始解析HTML
|
2. 遇到<link>、<script>等资源,开始下载
|
3. HTML解析完成,DOM树构建完毕
|
4. DOMContentLoaded 事件触发(此时CSS和图片可能还未加载完成)
|
5. CSS加载完成,样式应用
|
6. 图片等子资源加载完成
|
7. load 事件触发(页面及所有资源全部加载完成)
|
8. 用户交互开始三种核心加载事件对比
document.readyState的值
语法与用法
window.onload
代码示例
// 方式1:属性赋值(后者覆盖前者)
window.onload = function() {
console.log('页面加载完成');
};
// 方式2:addEventListener(推荐,可添加多个)
window.addEventListener('load', function() {
console.log('页面加载完成');
});DOMContentLoaded
代码示例
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM构建完成,可以操作DOM了');
});readystatechange
代码示例
document.addEventListener('readystatechange', function() {
console.log('readyState:', document.readyState);
if (document.readyState === 'interactive') {
console.log('文档解析完成');
}
if (document.readyState === 'complete') {
console.log('全部加载完成');
}
});图片和资源加载事件
代码示例
// 图片加载
const img = new Image();
img.onload = function() { console.log('图片加载完成', img.naturalWidth, img.naturalHeight); };
img.onerror = function() { console.log('图片加载失败'); };
img.src = 'image.jpg';
// script加载
const script = document.createElement('script');
script.onload = function() { console.log('脚本加载完成'); };
script.src = 'app.js';
// link样式加载
const link = document.createElement('link');
link.rel = 'stylesheet';
link.onload = function() { console.log('样式加载完成'); };
link.href = 'style.css';代码示例
示例1:DOMContentLoaded与load事件对比
通过可视化时间线,直观观察各加载事件的实际触发顺序和时间差:
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DOMContentLoaded与load事件对比</title>
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 20px;
}
.timeline {
position: relative;
padding: 20px 0;
margin: 20px 0;
}
.timeline::before {
content: '';
position: absolute;
left: 30px;
top: 0;
bottom: 0;
width: 3px;
background: #e0e0e0;
}
.timeline-item {
position: relative;
padding: 12px 20px 12px 70px;
margin: 8px 0;
border-radius: 8px;
transition: all 0.3s;
}
.timeline-item::before {
content: '';
position: absolute;
left: 22px;
top: 16px;
width: 18px;
height: 18px;
border-radius: 50%;
background: #e0e0e0;
border: 3px solid white;
box-shadow: 0 0 0 2px #e0e0e0;
transition: all 0.3s;
}
.timeline-item.fired {
background: #f0f7ff;
}
.timeline-item.fired::before {
background: #2196F3;
box-shadow: 0 0 0 2px #2196F3;
}
.timeline-item .event-name {
font-weight: bold;
color: #333;
}
.timeline-item .event-time {
font-size: 13px;
color: #666;
font-family: 'Consolas', monospace;
}
.timeline-item .event-desc {
font-size: 13px;
color: #888;
margin-top: 4px;
}
.info-box {
background: white;
border-radius: 10px;
padding: 20px;
margin: 20px 0;
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
}
.compare-table {
width: 100%;
border-collapse: collapse;
margin: 12px 0;
}
.compare-table th, .compare-table td {
padding: 10px 14px;
text-align: left;
border-bottom: 1px solid #eee;
}
.compare-table th {
background: #f5f5f5;
font-weight: bold;
}
.tag-yes { color: #4CAF50; font-weight: bold; }
.tag-no { color: #f44336; }
</style>
</head>
<body>
<h1>DOMContentLoaded 与 load 事件对比</h1>
<div class="info-box">
<h3>事件对比表</h3>
<table class="compare-table">
<tr>
<th>特性</th>
<th>DOMContentLoaded</th>
<th>load</th>
</tr>
<tr>
<td>触发时机</td>
<td>DOM树构建完成</td>
<td>所有资源加载完成</td>
</tr>
<tr>
<td>等待CSS</td>
<td class="tag-no">不等待</td>
<td class="tag-yes">等待</td>
</tr>
<tr>
<td>等待图片</td>
<td class="tag-no">不等待</td>
<td class="tag-yes">等待</td>
</tr>
<tr>
<td>等待iframe</td>
<td class="tag-no">不等待</td>
<td class="tag-yes">等待</td>
</tr>
<tr>
<td>等待async脚本</td>
<td class="tag-no">不等待</td>
<td class="tag-yes">等待</td>
</tr>
<tr>
<td>绑定对象</td>
<td>document</td>
<td>window</td>
</tr>
<tr>
<td>推荐用途</td>
<td>DOM操作初始化</td>
<td>依赖资源尺寸的操作</td>
</tr>
</table>
</div>
<h2>事件触发时间线</h2>
<p>观察以下事件的实际触发顺序和时间差</p>
<div class="timeline" id="timeline">
<div class="timeline-item" id="item-start">
<div class="event-name">页面开始加载</div>
<div class="event-time" id="time-start">-</div>
<div class="event-desc">浏览器开始解析HTML</div>
</div>
<div class="timeline-item" id="item-dom">
<div class="event-name">DOMContentLoaded</div>
<div class="event-time" id="time-dom">-</div>
<div class="event-desc">DOM树构建完成,可以安全操作DOM</div>
</div>
<div class="timeline-item" id="item-interactive">
<div class="event-name">readystatechange (interactive)</div>
<div class="event-time" id="time-interactive">-</div>
<div class="event-desc">document.readyState变为interactive</div>
</div>
<div class="timeline-item" id="item-load">
<div class="event-name">load</div>
<div class="event-time" id="time-load">-</div>
<div class="event-desc">页面及所有子资源加载完成</div>
</div>
<div class="timeline-item" id="item-complete">
<div class="event-name">readystatechange (complete)</div>
<div class="event-time" id="time-complete">-</div>
<div class="event-desc">document.readyState变为complete</div>
</div>
</div>
<!-- 使用一张较大的图片来制造加载延迟 -->
<img src="https://picsum.photos/2000/1500?random=1" style="display:none;" id="testImage">
<script>
const pageStart = performance.timeOrigin || Date.now();
document.getElementById('time-start').textContent = '0ms (页面开始)';
document.getElementById('item-start').classList.add('fired');
// readystatechange - interactive
document.addEventListener('readystatechange', function() {
if (document.readyState === 'interactive') {
const elapsed = Math.round(performance.now());
document.getElementById('time-interactive').textContent = elapsed + 'ms';
document.getElementById('item-interactive').classList.add('fired');
}
});
// DOMContentLoaded
document.addEventListener('DOMContentLoaded', function() {
const elapsed = Math.round(performance.now());
document.getElementById('time-dom').textContent = elapsed + 'ms';
document.getElementById('item-dom').classList.add('fired');
});
// readystatechange - complete
document.addEventListener('readystatechange', function() {
if (document.readyState === 'complete') {
const elapsed = Math.round(performance.now());
document.getElementById('time-complete').textContent = elapsed + 'ms';
document.getElementById('item-complete').classList.add('fired');
}
});
// load
window.addEventListener('load', function() {
const elapsed = Math.round(performance.now());
document.getElementById('time-load').textContent = elapsed + 'ms';
document.getElementById('item-load').classList.add('fired');
});
</script>
</body>
</html>示例2:图片和资源加载事件
追踪图片加载状态,并演示动态加载CSS、JS和图片资源的事件处理:
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片和资源加载事件</title>
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 20px;
}
.image-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin: 20px 0;
}
.image-card {
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
}
.image-card .img-wrapper {
height: 160px;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.image-card img {
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.3s;
}
.image-card img.loaded { opacity: 1; }
.image-card .loading-indicator {
position: absolute;
color: #999;
font-size: 14px;
}
.image-card .status-bar {
padding: 10px 14px;
font-size: 13px;
}
.status-loading { color: #FF9800; }
.status-loaded { color: #4CAF50; }
.status-error { color: #f44336; }
.progress-bar {
height: 4px;
background: #e0e0e0;
border-radius: 2px;
overflow: hidden;
margin-top: 6px;
}
.progress-bar .fill {
height: 100%;
background: #4CAF50;
width: 0;
transition: width 0.3s;
}
.resource-panel {
background: white;
border-radius: 10px;
padding: 20px;
margin: 20px 0;
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
}
.resource-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 0;
border-bottom: 1px solid #f0f0f0;
}
.resource-icon {
width: 36px;
height: 36px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.icon-css { background: #E3F2FD; }
.icon-js { background: #FFF3E0; }
.icon-img { background: #E8F5E9; }
.resource-info { flex: 1; }
.resource-name { font-size: 14px; font-weight: bold; }
.resource-status { font-size: 12px; color: #999; }
</style>
</head>
<body>
<h1>图片和资源加载事件</h1>
<h2>图片加载状态追踪</h2>
<div class="image-grid" id="imageGrid">
<!-- 动态生成 -->
</div>
<h2>动态资源加载</h2>
<div class="resource-panel">
<button onclick="loadCSS()" style="padding:8px 16px;border:none;border-radius:6px;cursor:pointer;background:#2196F3;color:white;margin:4px;">加载CSS</button>
<button onclick="loadJS()" style="padding:8px 16px;border:none;border-radius:6px;cursor:pointer;background:#FF9800;color:white;margin:4px;">加载JS</button>
<button onclick="loadImage()" style="padding:8px 16px;border:none;border-radius:6px;cursor:pointer;background:#4CAF50;color:white;margin:4px;">加载图片</button>
<div id="resourceList" style="margin-top:16px;"></div>
</div>
<script>
// ========== 图片加载追踪 ==========
const imageGrid = document.getElementById('imageGrid');
const images = [
{ url: 'https://picsum.photos/400/300?random=10', name: '风景图' },
{ url: 'https://picsum.photos/400/300?random=20', name: '建筑图' },
{ url: 'https://picsum.photos/400/300?random=30', name: '自然图' },
{ url: 'https://httpbin.org/image/notfound', name: '错误图(404)' },
{ url: 'https://picsum.photos/400/300?random=50', name: '城市图' },
{ url: 'https://picsum.photos/400/300?random=60', name: '动物图' }
];
images.forEach(function(imgData, index) {
const card = document.createElement('div');
card.className = 'image-card';
card.innerHTML = '<div class="img-wrapper">' +
'<span class="loading-indicator" id="indicator-' + index + '">加载中...</span>' +
'<img id="img-' + index + '" alt="' + imgData.name + '">' +
'</div>' +
'<div class="status-bar">' +
'<div id="status-' + index + '" class="status-loading">' + imgData.name + ' - 等待加载</div>' +
'<div class="progress-bar"><div class="fill" id="progress-' + index + '"></div></div>' +
'</div>';
imageGrid.appendChild(card);
const img = card.querySelector('#img-' + index);
const startTime = performance.now();
// 模拟进度
let progressInterval = setInterval(function() {
const elapsed = performance.now() - startTime;
const fakeProgress = Math.min(90, elapsed / 20);
document.getElementById('progress-' + index).style.width = fakeProgress + '%';
}, 100);
img.onload = function() {
clearInterval(progressInterval);
const loadTime = Math.round(performance.now() - startTime);
img.classList.add('loaded');
document.getElementById('indicator-' + index).style.display = 'none';
document.getElementById('status-' + index).className = 'status-loaded';
document.getElementById('status-' + index).textContent = imgData.name + ' - 加载完成 (' + loadTime + 'ms, ' + img.naturalWidth + 'x' + img.naturalHeight + ')';
document.getElementById('progress-' + index).style.width = '100%';
};
img.onerror = function() {
clearInterval(progressInterval);
document.getElementById('indicator-' + index).textContent = '加载失败';
document.getElementById('indicator-' + index).style.color = '#f44336';
document.getElementById('status-' + index).className = 'status-error';
document.getElementById('status-' + index).textContent = imgData.name + ' - 加载失败';
document.getElementById('progress-' + index).style.width = '100%';
document.getElementById('progress-' + index).style.background = '#f44336';
};
// 延迟加载,便于观察
setTimeout(function() {
img.src = imgData.url;
document.getElementById('status-' + index).textContent = imgData.name + ' - 加载中...';
}, index * 300);
});
// ========== 动态资源加载 ==========
const resourceList = document.getElementById('resourceList');
function addResourceEntry(type, name, status) {
const icons = { css: '[CSS]', js: '[JS]', img: '[IMG]' };
const entry = document.createElement('div');
entry.className = 'resource-item';
entry.id = 'res-' + name.replace(/[^a-z0-9]/gi, '');
entry.innerHTML = '<div class="resource-icon icon-' + type + '">' + icons[type] + '</div>' +
'<div class="resource-info">' +
'<div class="resource-name">' + name + '</div>' +
'<div class="resource-status">' + status + '</div>' +
'</div>';
resourceList.appendChild(entry);
return entry;
}
function loadCSS() {
const entry = addResourceEntry('css', '动态CSS', '加载中...');
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://cdn.jsdelivr.net/npm/water.css@2/out/light.min.css';
link.onload = function() {
entry.querySelector('.resource-status').textContent = '加载完成';
entry.querySelector('.resource-status').style.color = '#4CAF50';
};
link.onerror = function() {
entry.querySelector('.resource-status').textContent = '加载失败';
entry.querySelector('.resource-status').style.color = '#f44336';
};
document.head.appendChild(link);
}
function loadJS() {
const entry = addResourceEntry('js', '动态JS', '加载中...');
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js';
script.onload = function() {
entry.querySelector('.resource-status').textContent = '加载完成 (lodash ' + _.VERSION + ')';
entry.querySelector('.resource-status').style.color = '#4CAF50';
};
script.onerror = function() {
entry.querySelector('.resource-status').textContent = '加载失败';
entry.querySelector('.resource-status').style.color = '#f44336';
};
document.head.appendChild(script);
}
function loadImage() {
const entry = addResourceEntry('img', '动态图片', '加载中...');
const img = new Image();
img.src = 'https://picsum.photos/800/600?random=' + Date.now();
img.onload = function() {
entry.querySelector('.resource-status').textContent = '加载完成 (' + img.naturalWidth + 'x' + img.naturalHeight + ')';
entry.querySelector('.resource-status').style.color = '#4CAF50';
};
img.onerror = function() {
entry.querySelector('.resource-status').textContent = '加载失败';
entry.querySelector('.resource-status').style.color = '#f44336';
};
}
</script>
</body>
</html>示例3:加载顺序与性能优化
使用Performance API获取页面加载性能指标,并展示script标签的不同加载策略:
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>加载顺序与性能优化</title>
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 20px;
}
.perf-card {
background: white;
border-radius: 10px;
padding: 20px;
margin: 16px 0;
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
}
.metric-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
margin: 16px 0;
}
.metric-item {
background: #f8f9fa;
padding: 14px;
border-radius: 8px;
text-align: center;
}
.metric-item .value {
font-size: 24px;
font-weight: bold;
color: #1565C0;
font-family: 'Consolas', monospace;
}
.metric-item .label {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.tip-box {
background: #E3F2FD;
border-left: 4px solid #2196F3;
padding: 14px 18px;
border-radius: 0 8px 8px 0;
margin: 12px 0;
font-size: 14px;
}
.tip-box code {
background: rgba(0,0,0,0.06);
padding: 2px 6px;
border-radius: 3px;
font-size: 13px;
}
.code-block {
background: #263238;
color: #ECEFF1;
padding: 16px;
border-radius: 8px;
font-family: 'Consolas', monospace;
font-size: 13px;
overflow-x: auto;
margin: 12px 0;
line-height: 1.6;
}
.code-block .comment { color: #6A9955; }
.code-block .keyword { color: #569CD6; }
.code-block .string { color: #CE9178; }
</style>
</head>
<body>
<h1>加载顺序与性能优化</h1>
<div class="perf-card">
<h2>当前页面性能指标</h2>
<div class="metric-grid" id="metrics">
<div class="metric-item">
<div class="value" id="metricDNS">-</div>
<div class="label">DNS查询 (ms)</div>
</div>
<div class="metric-item">
<div class="value" id="metricTCP">-</div>
<div class="label">TCP连接 (ms)</div>
</div>
<div class="metric-item">
<div class="value" id="metricTTFB">-</div>
<div class="label">首字节时间 (ms)</div>
</div>
<div class="metric-item">
<div class="value" id="metricDOM">-</div>
<div class="label">DOM解析 (ms)</div>
</div>
<div class="metric-item">
<div class="value" id="metricLoad">-</div>
<div class="label">页面加载 (ms)</div>
</div>
<div class="metric-item">
<div class="value" id="metricDOMReady">-</div>
<div class="label">DOM Ready (ms)</div>
</div>
</div>
</div>
<div class="perf-card">
<h2>script标签加载策略</h2>
<div class="tip-box">
<strong>默认行为:</strong> <script> 标签会阻塞HTML解析,直到脚本下载并执行完毕
</div>
<div class="code-block">
<span class="comment">// 方式1:默认 - 阻塞解析</span>
<<span class="keyword">script</span> src=<span class="string">"app.js"</span>></<span class="keyword">script</span>>
<span class="comment">// 方式2:defer - 不阻塞解析,DOM构建完成后按顺序执行</span>
<<span class="keyword">script</span> <span class="keyword">defer</span> src=<span class="string">"app.js"</span>></<span class="keyword">script</span>>
<span class="comment">// 方式3:async - 不阻塞解析,下载完立即执行(无序)</span>
<<span class="keyword">script</span> <span class="keyword">async</span> src=<span class="string">"analytics.js"</span>></<span class="keyword">script</span>>
<span class="comment">// 方式4:动态创建 - 类似async,可控制执行时机</span>
<span class="keyword">const</span> script = document.createElement(<span class="string">'script'</span>);
script.src = <span class="string">'app.js'</span>;
document.head.appendChild(script);
</div>
<table style="width:100%;border-collapse:collapse;margin:12px 0;">
<tr style="background:#f5f5f5;">
<th style="padding:10px;text-align:left;">属性</th>
<th style="padding:10px;text-align:left;">阻塞解析</th>
<th style="padding:10px;text-align:left;">执行顺序</th>
<th style="padding:10px;text-align:left;">触发DOMContentLoaded</th>
</tr>
<tr>
<td style="padding:10px;">默认</td>
<td style="padding:10px;color:#f44336;">阻塞</td>
<td style="padding:10px;">按文档顺序</td>
<td style="padding:10px;">等待执行完</td>
</tr>
<tr style="background:#fafafa;">
<td style="padding:10px;">defer</td>
<td style="padding:10px;color:#4CAF50;">不阻塞</td>
<td style="padding:10px;">按文档顺序</td>
<td style="padding:10px;">等待执行完</td>
</tr>
<tr>
<td style="padding:10px;">async</td>
<td style="padding:10px;color:#4CAF50;">不阻塞</td>
<td style="padding:10px;">下载完即执行(无序)</td>
<td style="padding:10px;">不等待</td>
</tr>
</table>
</div>
<div class="perf-card">
<h2>优化建议</h2>
<div class="tip-box">
<strong>1. CSS放在head中:</strong> CSS会阻塞渲染,尽早加载可避免闪烁(FOUC)
</div>
<div class="tip-box">
<strong>2. JS放在body底部或使用defer:</strong> 避免阻塞DOM解析
</div>
<div class="tip-box">
<strong>3. 使用preload预加载关键资源:</strong><br>
<code><link rel="preload" href="critical.js" as="script"></code>
</div>
<div class="tip-box">
<strong>4. 使用prefetch预获取下一页资源:</strong><br>
<code><link rel="prefetch" href="next-page.js"></code>
</div>
<div class="tip-box">
<strong>5. 图片使用loading="lazy"懒加载:</strong><br>
<code><img src="image.jpg" loading="lazy"></code>
</div>
</div>
<script>
// 使用Performance API获取加载指标
window.addEventListener('load', function() {
setTimeout(function() {
const timing = performance.timing;
if (timing) {
const dns = timing.domainLookupEnd - timing.domainLookupStart;
const tcp = timing.connectEnd - timing.connectStart;
const ttfb = timing.responseStart - timing.requestStart;
const domParsing = timing.domInteractive - timing.domLoading;
const pageLoad = timing.loadEventEnd - timing.navigationStart;
const domReady = timing.domContentLoadedEventEnd - timing.navigationStart;
document.getElementById('metricDNS').textContent = dns;
document.getElementById('metricTCP').textContent = tcp;
document.getElementById('metricTTFB').textContent = ttfb;
document.getElementById('metricDOM').textContent = domParsing;
document.getElementById('metricLoad').textContent = pageLoad;
document.getElementById('metricDOMReady').textContent = domReady;
}
}, 100);
});
</script>
</body>
</html>示例4:图片懒加载实现
展示三种不同的图片懒加载实现方式:原生loading="lazy"、IntersectionObserver和scroll事件:
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片懒加载实现</title>
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 20px;
}
.lazy-image {
background: #f0f0f0;
border-radius: 10px;
margin: 20px 0;
overflow: hidden;
position: relative;
}
.lazy-image img {
width: 100%;
height: 300px;
object-fit: cover;
display: block;
opacity: 0;
transition: opacity 0.5s ease;
}
.lazy-image img.loaded { opacity: 1; }
.lazy-image .placeholder {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #999;
font-size: 14px;
}
.lazy-image .load-info {
position: absolute;
bottom: 10px;
right: 10px;
background: rgba(0,0,0,0.6);
color: white;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-family: 'Consolas', monospace;
}
.method-section {
background: white;
border-radius: 10px;
padding: 20px;
margin: 20px 0;
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
}
.method-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.badge-native { background: #E8F5E9; color: #2E7D32; }
.badge-io { background: #E3F2FD; color: #1565C0; }
.badge-scroll { background: #FFF3E0; color: #E65100; }
</style>
</head>
<body>
<h1>图片懒加载实现</h1>
<!-- 方法1:原生 loading="lazy" -->
<div class="method-section">
<h2>方法1:原生 loading="lazy" <span class="method-badge badge-native">推荐</span></h2>
<p>最简单的懒加载方式,浏览器原生支持</p>
</div>
<div class="lazy-image">
<img src="https://picsum.photos/800/300?random=101"
loading="lazy"
alt="懒加载图片1"
onload="this.classList.add('loaded'); this.parentElement.querySelector('.load-info').textContent='原生lazy加载完成'">
<span class="placeholder">等待滚动到可视区域...</span>
<span class="load-info">未加载</span>
</div>
<div class="lazy-image">
<img src="https://picsum.photos/800/300?random=102"
loading="lazy"
alt="懒加载图片2"
onload="this.classList.add('loaded'); this.parentElement.querySelector('.load-info').textContent='原生lazy加载完成'">
<span class="placeholder">等待滚动到可视区域...</span>
<span class="load-info">未加载</span>
</div>
<div class="lazy-image">
<img src="https://picsum.photos/800/300?random=103"
loading="lazy"
alt="懒加载图片3"
onload="this.classList.add('loaded'); this.parentElement.querySelector('.load-info').textContent='原生lazy加载完成'">
<span class="placeholder">等待滚动到可视区域...</span>
<span class="load-info">未加载</span>
</div>
<!-- 方法2:IntersectionObserver -->
<div class="method-section">
<h2>方法2:IntersectionObserver <span class="method-badge badge-io">灵活</span></h2>
<p>更灵活的控制,可自定义触发阈值和根边距</p>
</div>
<div class="lazy-image" data-lazy="io">
<img data-src="https://picsum.photos/800/300?random=201" alt="IO懒加载1">
<span class="placeholder">IntersectionObserver 等待中...</span>
<span class="load-info">未加载</span>
</div>
<div class="lazy-image" data-lazy="io">
<img data-src="https://picsum.photos/800/300?random=202" alt="IO懒加载2">
<span class="placeholder">IntersectionObserver 等待中...</span>
<span class="load-info">未加载</span>
</div>
<div class="lazy-image" data-lazy="io">
<img data-src="https://picsum.photos/800/300?random=203" alt="IO懒加载3">
<span class="placeholder">IntersectionObserver 等待中...</span>
<span class="load-info">未加载</span>
</div>
<!-- 方法3:scroll事件 -->
<div class="method-section">
<h2>方法3:scroll事件 + getBoundingClientRect <span class="method-badge badge-scroll">兼容</span></h2>
<p>传统方式,兼容性最好但性能较差</p>
</div>
<div class="lazy-image" data-lazy="scroll">
<img data-src="https://picsum.photos/800/300?random=301" alt="Scroll懒加载1">
<span class="placeholder">Scroll事件 等待中...</span>
<span class="load-info">未加载</span>
</div>
<div class="lazy-image" data-lazy="scroll">
<img data-src="https://picsum.photos/800/300?random=302" alt="Scroll懒加载2">
<span class="placeholder">Scroll事件 等待中...</span>
<span class="load-info">未加载</span>
</div>
<div class="lazy-image" data-lazy="scroll">
<img data-src="https://picsum.photos/800/300?random=303" alt="Scroll懒加载3">
<span class="placeholder">Scroll事件 等待中...</span>
<span class="load-info">未加载</span>
</div>
<script>
// ========== 方法2:IntersectionObserver ==========
const ioObserver = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
const container = entry.target;
const img = container.querySelector('img');
const src = img.dataset.src;
if (src) {
img.src = src;
img.onload = function() {
img.classList.add('loaded');
container.querySelector('.placeholder').style.display = 'none';
container.querySelector('.load-info').textContent = 'IO加载完成';
};
img.removeAttribute('data-src');
}
ioObserver.unobserve(container);
}
});
}, {
rootMargin: '100px', // 提前100px开始加载
threshold: 0.1 // 10%可见时触发
});
document.querySelectorAll('[data-lazy="io"]').forEach(function(el) {
ioObserver.observe(el);
});
// ========== 方法3:scroll事件 ==========
function checkScrollLazyImages() {
const images = document.querySelectorAll('[data-lazy="scroll"]');
images.forEach(function(container) {
const img = container.querySelector('img');
if (img.dataset.src) {
const rect = container.getBoundingClientRect();
const isVisible = rect.top < window.innerHeight + 100 && rect.bottom > -100;
if (isVisible) {
img.src = img.dataset.src;
img.onload = function() {
img.classList.add('loaded');
container.querySelector('.placeholder').style.display = 'none';
container.querySelector('.load-info').textContent = 'Scroll加载完成';
};
img.removeAttribute('data-src');
}
}
});
}
// 节流处理
let scrollTimer = null;
window.addEventListener('scroll', function() {
if (scrollTimer) return;
scrollTimer = setTimeout(function() {
checkScrollLazyImages();
scrollTimer = null;
}, 200);
}, { passive: true });
// 初始检查
checkScrollLazyImages();
</script>
</body>
</html>示例5:readystatechange与加载状态追踪
通过可视化仪表板实时追踪document.readyState的状态变化:
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>readystatechange与加载状态追踪</title>
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 20px;
}
.state-dashboard {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
margin: 20px 0;
}
.state-indicator {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
margin: 8px 0;
border-radius: 8px;
background: #f8f9fa;
transition: all 0.3s;
}
.state-indicator.active {
background: #E8F5E9;
border-left: 4px solid #4CAF50;
}
.state-indicator.current {
background: #E3F2FD;
border-left: 4px solid #2196F3;
box-shadow: 0 2px 8px rgba(33,150,243,0.2);
}
.state-dot {
width: 16px;
height: 16px;
border-radius: 50%;
background: #e0e0e0;
transition: background 0.3s;
}
.state-indicator.active .state-dot { background: #4CAF50; }
.state-indicator.current .state-dot {
background: #2196F3;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(33,150,243,0.4); }
50% { box-shadow: 0 0 0 8px rgba(33,150,243,0); }
}
.state-name { font-weight: bold; min-width: 120px; }
.state-desc { color: #666; font-size: 14px; }
.state-time { margin-left: auto; font-family: 'Consolas', monospace; color: #1565C0; }
.event-log {
background: #263238;
color: #ECEFF1;
padding: 14px;
border-radius: 8px;
font-family: 'Consolas', monospace;
font-size: 13px;
max-height: 300px;
overflow-y: auto;
margin: 16px 0;
}
.event-log .loading { color: #FFA726; }
.event-log .interactive { color: #66BB6A; }
.event-log .complete { color: #42A5F5; }
</style>
</head>
<body>
<h1>readystatechange 与加载状态追踪</h1>
<div class="state-dashboard">
<h2>document.readyState 状态仪表板</h2>
<div class="state-indicator" id="state-loading">
<div class="state-dot"></div>
<span class="state-name">loading</span>
<span class="state-desc">文档正在加载中</span>
<span class="state-time" id="time-loading">-</span>
</div>
<div class="state-indicator" id="state-interactive">
<div class="state-dot"></div>
<span class="state-name">interactive</span>
<span class="state-desc">文档解析完成,子资源可能仍在加载</span>
<span class="state-time" id="time-interactive">-</span>
</div>
<div class="state-indicator" id="state-complete">
<div class="state-dot"></div>
<span class="state-name">complete</span>
<span class="state-desc">文档和所有子资源加载完成</span>
<span class="state-time" id="time-complete">-</span>
</div>
</div>
<h2>事件触发日志</h2>
<div class="event-log" id="eventLog"></div>
<script>
const eventLog = document.getElementById('eventLog');
const startTime = performance.timeOrigin || 0;
function logEvent(msg, type) {
const elapsed = Math.round(performance.now());
eventLog.innerHTML += '<div class="' + type + '">[' + elapsed + 'ms] ' + msg + '</div>';
eventLog.scrollTop = eventLog.scrollHeight;
}
function updateState(stateName) {
// 更新状态指示器
document.querySelectorAll('.state-indicator').forEach(function(el) {
el.classList.remove('current');
});
const stateEl = document.getElementById('state-' + stateName);
if (stateEl) {
stateEl.classList.add('active', 'current');
document.getElementById('time-' + stateName).textContent =
Math.round(performance.now()) + 'ms';
}
// 之前的状态设为active但不current
const states = ['loading', 'interactive', 'complete'];
const currentIndex = states.indexOf(stateName);
for (let i = 0; i < currentIndex; i++) {
document.getElementById('state-' + states[i]).classList.add('active');
document.getElementById('state-' + states[i]).classList.remove('current');
}
}
// 检查当前状态(可能脚本执行时已经过了某个状态)
logEvent('初始readyState: ' + document.readyState, document.readyState);
if (document.readyState === 'loading') {
updateState('loading');
} else if (document.readyState === 'interactive') {
updateState('loading');
updateState('interactive');
} else if (document.readyState === 'complete') {
updateState('loading');
updateState('interactive');
updateState('complete');
}
// 监听readystatechange事件
document.addEventListener('readystatechange', function() {
const state = document.readyState;
logEvent('readystatechange - ' + state, state);
updateState(state);
});
// 监听DOMContentLoaded
document.addEventListener('DOMContentLoaded', function() {
logEvent('DOMContentLoaded 触发', 'interactive');
});
// 监听load
window.addEventListener('load', function() {
logEvent('window load 触发', 'complete');
});
// 监听beforeunload
window.addEventListener('beforeunload', function(event) {
logEvent('beforeunload 触发 - 页面即将卸载', 'loading');
});
</script>
</body>
</html>浏览器兼容性
IE兼容性说明:
• IE8及以下不支持DOMContentLoaded,可用readystatechange模拟
• IE8及以下script.onload不触发,需使用script.onreadystatechange
• IE9开始支持标准的DOMContentLoaded
注意事项与最佳实践
1. 优先使用DOMContentLoaded而非load
代码示例
// 不推荐:等待所有资源加载完成
window.onload = function() {
initApp(); // 图片、CSS等都加载完才执行,可能很慢
};
// 推荐:DOM构建完成即可操作
document.addEventListener('DOMContentLoaded', function() {
initApp(); // DOM可用就执行,更快响应
});2. 处理DOMContentLoaded已触发的情况
代码示例
// 问题:脚本异步加载时,DOMContentLoaded可能已经触发
function onReady(callback) {
if (document.readyState !== 'loading') {
callback(); // DOM已就绪,直接执行
} else {
document.addEventListener('DOMContentLoaded', callback);
}
}
onReady(function() {
console.log('DOM已就绪,安全操作DOM');
});3. 避免多个window.onload覆盖
代码示例
// 问题:后者覆盖前者
window.onload = handler1;
window.onload = handler2; // handler1被覆盖
// 解决方案:使用addEventListener
window.addEventListener('load', handler1);
window.addEventListener('load', handler2); // 两个都执行4. 图片加载的错误处理
代码示例
const img = new Image();
img.onload = function() {
console.log('图片加载成功');
};
img.onerror = function() {
console.log('图片加载失败,使用占位图');
img.src = 'placeholder.png';
};
img.src = 'photo.jpg';5. 懒加载的最佳实践
代码示例
<!-- 推荐:原生loading="lazy" + 回退方案 -->
<img src="image.jpg" loading="lazy" alt="描述">
<!-- 带回退的完整方案 -->
<img src="image.jpg"
loading="lazy"
data-fallback="placeholder.jpg"
onerror="if(this.dataset.fallback){this.src=this.dataset.fallback;delete this.dataset.fallback;}"
alt="描述">6. script标签放置位置
代码示例
<!-- 推荐:CSS在head,JS在body底部 -->
<head>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- 页面内容 -->
<script src="app.js" defer></script>
</body>代码规范示例
代码示例
/**
* 页面加载管理器 - 统一管理加载生命周期
*/
class PageLoadManager {
constructor() {
this.callbacks = {
domReady: [],
loaded: [],
beforeUnload: []
};
this.isDomReady = false;
this.isLoaded = false;
this._init();
}
_init() {
// DOMContentLoaded
if (document.readyState !== 'loading') {
this._handleDomReady();
} else {
document.addEventListener('DOMContentLoaded', () => this._handleDomReady());
}
// load
if (document.readyState === 'complete') {
this._handleLoad();
} else {
window.addEventListener('load', () => this._handleLoad());
}
// beforeunload
window.addEventListener('beforeunload', () => this._handleBeforeUnload());
}
_handleDomReady() {
this.isDomReady = true;
this.callbacks.domReady.forEach(cb => {
try { cb(); } catch (e) { console.error('domReady callback error:', e); }
});
this.callbacks.domReady = [];
}
_handleLoad() {
this.isLoaded = true;
this.callbacks.loaded.forEach(cb => {
try { cb(); } catch (e) { console.error('loaded callback error:', e); }
});
this.callbacks.loaded = [];
}
_handleBeforeUnload() {
this.callbacks.beforeUnload.forEach(cb => {
try { cb(); } catch (e) { console.error('beforeUnload callback error:', e); }
});
}
/**
* DOM构建完成后执行
* @param {Function} callback
*/
onDomReady(callback) {
if (this.isDomReady) {
callback();
} else {
this.callbacks.domReady.push(callback);
}
return this;
}
/**
* 页面及所有资源加载完成后执行
* @param {Function} callback
*/
onLoaded(callback) {
if (this.isLoaded) {
callback();
} else {
this.callbacks.loaded.push(callback);
}
return this;
}
/**
* 页面卸载前执行
* @param {Function} callback
*/
onBeforeUnload(callback) {
this.callbacks.beforeUnload.push(callback);
return this;
}
}
// 使用示例
const pageLoad = new PageLoadManager();
pageLoad
.onDomReady(function() {
console.log('DOM已就绪,初始化应用');
})
.onLoaded(function() {
console.log('页面完全加载,执行依赖资源的操作');
})
.onBeforeUnload(function() {
console.log('页面即将卸载,保存数据');
});常见问题与解决方案
问题1:DOMContentLoaded不触发
代码示例
// 问题:使用了async脚本且脚本执行时间很长
// <script async src="slow-script.js"></script>
// async脚本不阻塞DOMContentLoaded,但如果脚本在DOMContentLoaded之前开始执行,
// 会阻塞DOM解析
// 解决方案:确保没有阻塞DOM解析的同步脚本
// 使用defer代替async(defer脚本在DOMContentLoaded之前按顺序执行)问题2:图片onload不触发(缓存问题)
代码示例
// 问题:IE中已缓存的图片不触发onload
const img = new Image();
img.onload = function() { console.log('loaded'); };
img.src = 'cached-image.jpg'; // 如果已缓存,IE可能不触发onload
// 解决方案:先设置src再绑定onload,或检查complete属性
const img = new Image();
img.src = 'cached-image.jpg';
if (img.complete) {
// 图片已缓存,直接处理
handleImageLoad(img);
} else {
img.onload = function() { handleImageLoad(img); };
}问题3:iframe的load事件影响主页面
代码示例
// 问题:window.load等待所有iframe加载完成
// 如果iframe加载很慢,主页面load事件会延迟
// 解决方案1:动态加载iframe
// 不在HTML中写iframe,在load后动态创建
// 解决方案2:使用DOMContentLoaded代替load
document.addEventListener('DOMContentLoaded', function() {
// 不等待iframe,DOM就绪即可
});问题4:动态创建的script/link的加载检测
代码示例
// 动态创建的script标签
function loadScript(url) {
return new Promise(function(resolve, reject) {
const script = document.createElement('script');
script.src = url;
script.onload = function() { resolve(); };
script.onerror = function() { reject(new Error('Failed to load: ' + url)); };
document.head.appendChild(script);
});
}
// 使用
loadScript('https://cdn.example.com/lib.js')
.then(function() { console.log('脚本加载完成'); })
.catch(function(err) { console.error(err); });问题5:SPA中的load事件只触发一次
代码示例
// 问题:单页应用中路由切换不会触发load事件
// load事件只在页面首次加载时触发
// 解决方案:使用自定义事件或路由钩子
// Vue Router
router.afterEach(function() {
window.dispatchEvent(new CustomEvent('routeChange'));
});
// 监听路由变化
window.addEventListener('routeChange', function() {
console.log('路由变化,重新初始化');
});总结
页面加载事件是Web应用生命周期的基石,掌握其细节对于构建高性能应用至关重要:
-
DOMContentLoaded vs load:
DOMContentLoaded在DOM树构建完成时触发,不等待CSS和图片;load在所有资源加载完成时触发。大多数初始化操作应使用DOMContentLoaded。 -
readystatechange:提供更细粒度的加载状态追踪,
interactive对应DOMContentLoaded,complete对应load。可用于兼容不支持DOMContentLoaded的旧浏览器。 -
资源加载事件:图片、脚本、样式表都有独立的
onload和onerror事件,可用于追踪单个资源的加载状态,实现加载进度指示和错误处理。 -
性能优化:CSS放head、JS用defer、使用preload/prefetch预加载关键资源、图片使用
loading="lazy"懒加载,这些策略可以显著提升页面加载性能。 -
懒加载:原生
loading="lazy"是最简单的方案,IntersectionObserver提供更灵活的控制,scroll事件+getBoundingClientRect是兼容性最好的回退方案。 -
加载状态检测:始终检查
document.readyState以处理脚本异步加载时DOMContentLoaded已触发的情况,避免回调永远不会执行的问题。
常见问题
1. DOMContentLoaded不触发怎么办?
如果使用了async脚本且脚本执行时间很长,可能会阻塞DOM解析。解决方案是确保没有阻塞DOM解析的同步脚本,使用defer代替async(defer脚本在DOMContentLoaded之前按顺序执行)。
2. 图片onload不触发(缓存问题)如何解决?
在IE中,已缓存的图片可能不触发onload事件。解决方案是先设置src再绑定onload,或检查complete属性:if (img.complete) { handleImageLoad(img); } else { img.onload = function() { handleImageLoad(img); }; }
3. iframe的load事件影响主页面怎么办?
window.load等待所有iframe加载完成,如果iframe加载很慢,主页面load事件会延迟。解决方案有两种:一是动态加载iframe(不在HTML中写iframe,在load后动态创建);二是使用DOMContentLoaded代替load,不等待iframe,DOM就绪即可执行。
4. 如何检测动态创建的script/link是否加载完成?
动态创建的script标签可以使用Promise包装加载检测:function loadScript(url) { return new Promise(function(resolve, reject) { const script = document.createElement('script'); script.src = url; script.onload = function() { resolve(); }; script.onerror = function() { reject(new Error('Failed to load: ' + url)); }; document.head.appendChild(script); }); }
5. SPA中load事件只触发一次如何处理?
单页应用中路由切换不会触发load事件,load事件只在页面首次加载时触发。解决方案是使用自定义事件或路由钩子,例如Vue Router的router.afterEach钩子中派发routeChange自定义事件,然后监听该事件来重新初始化页面。
本文涉及AI创作
内容由AI创作,请仔细甄别