pin_drop当前位置:知识文库 ❯ 图文

JS集成:HTML onresize 事 - 详细教程与实战指南

教程简介

onresize 事件在浏览器窗口大小发生变化时触发,是实现响应式布局和自适应界面的关键事件。无论是用户手动调整窗口大小、旋转移动设备,还是全屏/退出全屏操作,都会触发 resize 事件。与 scroll 事件类似,resize 事件在调整过程中也会高频触发,如果不加以优化,会导致严重的性能问题。

本教程将深入讲解 onresize 事件的核心概念、性能优化策略、防抖处理、响应式布局检测、ResizeObserver API、元素尺寸变化监听以及布局重排优化。


核心概念

onresize 事件定义

resize 事件在以下情况触发:

  • 用户拖拽浏览器窗口边框调整大小

  • 用户最大化/还原/最小化窗口

  • 移动设备旋转屏幕方向

  • 全屏/退出全屏切换

  • JavaScript 代码修改窗口大小

resize 事件的关键特性

特性 说明
触发对象 window 对象
触发频率 调整过程中持续高频触发
冒泡行为 不冒泡
默认行为 不可取消
event 对象 无特殊属性,需通过 window 获取尺寸
性能影响 每次触发可能导致重排重绘

获取窗口尺寸的方法

方法 说明 包含滚动条
window.innerWidth 视口宽度 是
window.innerHeight 视口高度 是
document.documentElement.clientWidth 文档可视宽度 否
document.documentElement.clientHeight 文档可视高度 否
window.outerWidth 浏览器窗口宽度 是
window.outerHeight 浏览器窗口高度 是
screen.width 屏幕宽度 -
screen.height 屏幕高度 -

ResizeObserver API

ResizeObserver 是监听元素尺寸变化的现代 API,相比 resize 事件有以下优势:

特性 window.resize ResizeObserver
监听对象 仅 window 任意元素
触发频率 高频 高频
回调参数 无尺寸信息 包含尺寸信息
性能 需手动优化 浏览器优化
兼容性 全部 现代浏览器

语法与用法

HTML 属性方式

代码示例

<body onresize="handleResize()">内容</body>

DOM 属性方式

代码示例

window.onresize = function() {
    // 处理逻辑
};

addEventListener 方式(推荐)

代码示例

window.addEventListener('resize', function() {
    // 处理逻辑
});

ResizeObserver 用法

代码示例

const observer = new ResizeObserver(entries => {
    for (const entry of entries) {
        const width = entry.contentRect.width;
        const height = entry.contentRect.height;
        // 处理尺寸变化
    }
});

observer.observe(element);

ResizeObserver 回调参数

属性 说明
entry.target 被观察的元素
entry.contentRect 内容区域的 DOMRectReadOnly
entry.contentRect.width 内容宽度(不含 padding/border)
entry.contentRect.height 内容高度(不含 padding/border)
entry.borderBoxSize 包含 border 的尺寸
entry.contentBoxSize 内容框尺寸

代码示例

示例1:基础窗口大小监听

代码示例

<!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;
            margin: 0;
            padding: 20px;
            background: #f5f5f5;
        }
        .info-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
            gap: 16px;
            margin-bottom: 30px;
        }
        .info-card {
            background: white;
            padding: 20px;
            border-radius: 12px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
            text-align: center;
        }
        .info-card .label {
            font-size: 13px;
            color: #888;
            margin-bottom: 8px;
        }
        .info-card .value {
            font-size: 28px;
            font-weight: bold;
            color: #2196F3;
        }
        .info-card .unit {
            font-size: 14px;
            color: #aaa;
        }
        .breakpoint-bar {
            background: white;
            padding: 20px;
            border-radius: 12px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
            margin-bottom: 20px;
        }
        .breakpoint-bar h3 { margin-top: 0; }
        .bp-indicators {
            display: flex;
            gap: 8px;
            flex-wrap: wrap;
        }
        .bp-tag {
            padding: 6px 16px;
            border-radius: 20px;
            font-size: 13px;
            font-weight: bold;
            opacity: 0.3;
            transition: opacity 0.3s;
        }
        .bp-tag.active { opacity: 1; }
        .bp-xs { background: #ffebee; color: #c62828; }
        .bp-sm { background: #fff3e0; color: #e65100; }
        .bp-md { background: #fff8e1; color: #f57f17; }
        .bp-lg { background: #e8f5e9; color: #2e7d32; }
        .bp-xl { background: #e3f2fd; color: #1565C0; }
        .size-history {
            background: white;
            padding: 20px;
            border-radius: 12px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        .size-history h3 { margin-top: 0; }
        .history-log {
            max-height: 150px;
            overflow-y: auto;
            font-size: 13px;
            font-family: monospace;
        }
    </style>
</head>
<body>
    <h1>窗口大小监听</h1>
    <p>调整浏览器窗口大小,观察尺寸变化。</p>

    <div class="info-grid">
        <div class="info-card">
            <div class="label">视口宽度</div>
            <div class="value" id="vpWidth">0</div>
            <div class="unit">px</div>
        </div>
        <div class="info-card">
            <div class="label">视口高度</div>
            <div class="value" id="vpHeight">0</div>
            <div class="unit">px</div>
        </div>
        <div class="info-card">
            <div class="label">屏幕宽度</div>
            <div class="value" id="screenW">0</div>
            <div class="unit">px</div>
        </div>
        <div class="info-card">
            <div class="label">屏幕高度</div>
            <div class="value" id="screenH">0</div>
            <div class="unit">px</div>
        </div>
        <div class="info-card">
            <div class="label">设备像素比</div>
            <div class="value" id="dpr">0</div>
            <div class="unit">x</div>
        </div>
        <div class="info-card">
            <div class="label">方向</div>
            <div class="value" id="orientation">-</div>
            <div class="unit"></div>
        </div>
    </div>

    <div class="breakpoint-bar">
        <h3>当前断点</h3>
        <div class="bp-indicators">
            <span class="bp-tag bp-xs" id="bp-xs">XS (<576px)</span>
            <span class="bp-tag bp-sm" id="bp-sm">SM (576-767px)</span>
            <span class="bp-tag bp-md" id="bp-md">MD (768-991px)</span>
            <span class="bp-tag bp-lg" id="bp-lg">LG (992-1199px)</span>
            <span class="bp-tag bp-xl" id="bp-xl">XL (≥1200px)</span>
        </div>
    </div>

    <div class="size-history">
        <h3>尺寸变化记录</h3>
        <div class="history-log" id="historyLog"></div>
    </div>

    <script>
        const vpWidthEl = document.getElementById('vpWidth');
        const vpHeightEl = document.getElementById('vpHeight');
        const screenWEl = document.getElementById('screenW');
        const screenHEl = document.getElementById('screenH');
        const dprEl = document.getElementById('dpr');
        const orientationEl = document.getElementById('orientation');
        const historyLog = document.getElementById('historyLog');

        // 防抖处理
        let resizeTimer = null;
        const DEBOUNCE_DELAY = 150;

        window.addEventListener('resize', function() {
            clearTimeout(resizeTimer);
            resizeTimer = setTimeout(updateSizeInfo, DEBOUNCE_DELAY);
        });

        function updateSizeInfo() {
            const w = window.innerWidth;
            const h = window.innerHeight;

            vpWidthEl.textContent = w;
            vpHeightEl.textContent = h;
            screenWEl.textContent = screen.width;
            screenHEl.textContent = screen.height;
            dprEl.textContent = window.devicePixelRatio || 1;
            orientationEl.textContent = w > h ? '横屏' : '竖屏';

            // 更新断点
            updateBreakpoint(w);

            // 记录变化
            addHistory(w, h);
        }

        function updateBreakpoint(w) {
            const bps = ['xs', 'sm', 'md', 'lg', 'xl'];
            bps.forEach(bp => {
                document.getElementById('bp-' + bp).classList.remove('active');
            });

            if (w < 576) document.getElementById('bp-xs').classList.add('active');
            else if (w < 768) document.getElementById('bp-sm').classList.add('active');
            else if (w < 992) document.getElementById('bp-md').classList.add('active');
            else if (w < 1200) document.getElementById('bp-lg').classList.add('active');
            else document.getElementById('bp-xl').classList.add('active');
        }

        function addHistory(w, h) {
            const time = new Date().toLocaleTimeString('zh-CN', { hour12: false });
            historyLog.innerHTML += `[${time}] ${w} x ${h}<br>`;
            historyLog.scrollTop = historyLog.scrollHeight;
        }

        // 初始化
        updateSizeInfo();
    </script>
</body>
</html>

示例2:防抖处理对比

代码示例

<!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;
            margin: 0;
            padding: 20px;
            background: #f5f5f5;
        }
        .comparison {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
        }
        .panel {
            background: white;
            padding: 20px;
            border-radius: 12px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        .panel h3 { margin-top: 0; }
        .counter {
            font-size: 48px;
            font-weight: bold;
            margin: 10px 0;
        }
        .panel:nth-child(1) .counter { color: #f44336; }
        .panel:nth-child(2) .counter { color: #4CAF50; }
        .desc {
            font-size: 13px;
            color: #666;
        }
        .timeline {
            height: 60px;
            background: #f5f5f5;
            border-radius: 4px;
            position: relative;
            overflow: hidden;
            margin-top: 10px;
        }
        .timeline-dot {
            position: absolute;
            width: 4px;
            height: 100%;
            background: currentColor;
            opacity: 0.6;
        }
        .panel:nth-child(1) .timeline-dot { color: #f44336; }
        .panel:nth-child(2) .timeline-dot { color: #4CAF50; }
        .current-size {
            font-size: 18px;
            font-weight: bold;
            color: #333;
        }
    </style>
</head>
<body>
    <h1>防抖处理对比</h1>
    <p>调整浏览器窗口大小,观察两种处理方式的触发次数差异。</p>

    <div class="comparison">
        <div class="panel">
            <h3>无优化</h3>
            <div class="counter" id="rawCount">0</div>
            <div class="current-size" id="rawSize">0 x 0</div>
            <div class="desc">每次 resize 都触发</div>
            <div class="timeline" id="rawTimeline"></div>
        </div>
        <div class="panel">
            <h3>防抖 (200ms)</h3>
            <div class="counter" id="debounceCount">0</div>
            <div class="current-size" id="debounceSize">0 x 0</div>
            <div class="desc">停止调整 200ms 后触发</div>
            <div class="timeline" id="debounceTimeline"></div>
        </div>
    </div>

    <script>
        let rawCount = 0;
        let debounceCount = 0;
        let debounceTimer = null;
        const DEBOUNCE_DELAY = 200;

        // 无优化
        window.addEventListener('resize', function() {
            rawCount++;
            document.getElementById('rawCount').textContent = rawCount;
            document.getElementById('rawSize').textContent =
                window.innerWidth + ' x ' + window.innerHeight;
            addDot('rawTimeline', rawCount);
        });

        // 防抖
        window.addEventListener('resize', function() {
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(function() {
                debounceCount++;
                document.getElementById('debounceCount').textContent = debounceCount;
                document.getElementById('debounceSize').textContent =
                    window.innerWidth + ' x ' + window.innerHeight;
                addDot('debounceTimeline', debounceCount);
            }, DEBOUNCE_DELAY);
        });

        function addDot(timelineId, count) {
            const timeline = document.getElementById(timelineId);
            const dot = document.createElement('div');
            dot.className = 'timeline-dot';
            const position = Math.min((count - 1) * 8, 100);
            dot.style.left = position + '%';
            timeline.appendChild(dot);

            // 限制点数
            while (timeline.children.length > 12) {
                timeline.removeChild(timeline.firstChild);
            }
        }
    </script>
</body>
</html>

示例3:响应式布局检测

代码示例

<!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>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: "Microsoft YaHei", sans-serif;
            background: #f5f5f5;
            transition: background 0.3s;
        }
        body.bp-xs { background: #ffebee; }
        body.bp-sm { background: #fff3e0; }
        body.bp-md { background: #fff8e1; }
        body.bp-lg { background: #e8f5e9; }
        body.bp-xl { background: #e3f2fd; }

        .header {
            padding: 20px;
            background: white;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 10px;
        }
        .header h1 { font-size: 20px; }
        .bp-badge {
            padding: 6px 16px;
            border-radius: 20px;
            font-size: 14px;
            font-weight: bold;
        }
        .layout-demo {
            padding: 20px;
            max-width: 1200px;
            margin: 20px auto;
        }
        .grid-layout {
            display: grid;
            gap: 16px;
            transition: all 0.3s;
        }
        .grid-layout.cols-1 { grid-template-columns: 1fr; }
        .grid-layout.cols-2 { grid-template-columns: repeat(2, 1fr); }
        .grid-layout.cols-3 { grid-template-columns: repeat(3, 1fr); }
        .grid-layout.cols-4 { grid-template-columns: repeat(4, 1fr); }
        .grid-item {
            background: white;
            padding: 30px;
            border-radius: 12px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
            text-align: center;
            font-size: 16px;
            font-weight: bold;
            color: #333;
        }
        .info-bar {
            max-width: 1200px;
            margin: 20px auto;
            padding: 15px 20px;
            background: white;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 10px;
        }
        .layout-info {
            font-size: 14px;
            color: #666;
        }
        .layout-info strong {
            color: #2196F3;
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>响应式布局检测</h1>
        <span class="bp-badge" id="bpBadge">-</span>
    </div>

    <div class="info-bar">
        <div class="layout-info">
            当前断点:<strong id="currentBP">-</strong>
        </div>
        <div class="layout-info">
            列数:<strong id="currentCols">-</strong>
        </div>
        <div class="layout-info">
            视口:<strong id="currentSize">-</strong>
        </div>
    </div>

    <div class="layout-demo">
        <div class="grid-layout" id="gridLayout">
            <div class="grid-item">卡片 1</div>
            <div class="grid-item">卡片 2</div>
            <div class="grid-item">卡片 3</div>
            <div class="grid-item">卡片 4</div>
            <div class="grid-item">卡片 5</div>
            <div class="grid-item">卡片 6</div>
            <div class="grid-item">卡片 7</div>
            <div class="grid-item">卡片 8</div>
        </div>
    </div>

    <script>
        const gridLayout = document.getElementById('gridLayout');
        const bpBadge = document.getElementById('bpBadge');
        const currentBP = document.getElementById('currentBP');
        const currentCols = document.getElementById('currentCols');
        const currentSize = document.getElementById('currentSize');

        const breakpoints = [
            { name: 'XS', minWidth: 0, cols: 1, color: '#c62828', bg: '#ffebee' },
            { name: 'SM', minWidth: 576, cols: 2, color: '#e65100', bg: '#fff3e0' },
            { name: 'MD', minWidth: 768, cols: 2, color: '#f57f17', bg: '#fff8e1' },
            { name: 'LG', minWidth: 992, cols: 3, color: '#2e7d32', bg: '#e8f5e9' },
            { name: 'XL', minWidth: 1200, cols: 4, color: '#1565C0', bg: '#e3f2fd' }
        ];

        let resizeTimer = null;

        window.addEventListener('resize', function() {
            clearTimeout(resizeTimer);
            resizeTimer = setTimeout(updateLayout, 100);
        });

        function updateLayout() {
            const w = window.innerWidth;
            let current = breakpoints[0];

            for (const bp of breakpoints) {
                if (w >= bp.minWidth) {
                    current = bp;
                }
            }

            // 更新网格列数
            gridLayout.className = 'grid-layout cols-' + current.cols;

            // 更新信息
            bpBadge.textContent = current.name;
            bpBadge.style.background = current.bg;
            bpBadge.style.color = current.color;
            currentBP.textContent = current.name;
            currentCols.textContent = current.cols;
            currentSize.textContent = w + 'px';

            // 更新背景
            document.body.className = 'bp-' + current.name.toLowerCase();
        }

        // 初始化
        updateLayout();
    </script>
</body>
</html>

示例4:ResizeObserver 监听元素尺寸

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ResizeObserver 监听元素尺寸</title>
    <style>
        body {
            font-family: "Microsoft YaHei", sans-serif;
            padding: 20px;
            background: #f5f5f5;
        }
        .demo-area {
            display: flex;
            gap: 20px;
            margin-bottom: 20px;
        }
        .resizable-box {
            min-width: 100px;
            min-height: 100px;
            max-width: 600px;
            max-height: 400px;
            padding: 20px;
            background: white;
            border: 2px solid #2196F3;
            border-radius: 12px;
            resize: both;
            overflow: hidden;
            position: relative;
        }
        .resizable-box .size-label {
            font-size: 14px;
            color: #2196F3;
            font-weight: bold;
        }
        .resizable-box .resize-hint {
            position: absolute;
            bottom: 5px;
            right: 10px;
            font-size: 12px;
            color: #aaa;
        }
        .observer-log {
            background: white;
            padding: 20px;
            border-radius: 12px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        .observer-log h3 { margin-top: 0; }
        .log-entries {
            max-height: 300px;
            overflow-y: auto;
            font-size: 13px;
            font-family: monospace;
        }
        .log-entry {
            padding: 8px;
            border-bottom: 1px solid #f0f0f0;
        }
        .log-entry .element-name {
            font-weight: bold;
            color: #2196F3;
        }
        .log-entry .dimensions {
            color: #333;
        }
        .log-entry .timestamp {
            color: #aaa;
            font-size: 11px;
        }
        .flex-container {
            display: flex;
            gap: 20px;
            flex-wrap: wrap;
            margin-bottom: 20px;
        }
        .flex-item {
            flex: 1;
            min-width: 150px;
            padding: 20px;
            background: white;
            border: 2px solid #4CAF50;
            border-radius: 8px;
            text-align: center;
        }
        .flex-item .size {
            font-size: 18px;
            font-weight: bold;
            color: #4CAF50;
        }
    </style>
</head>
<body>
    <h1>ResizeObserver 监听元素尺寸</h1>
    <p>拖拽右下角调整盒子大小,观察 ResizeObserver 的回调触发。</p>

    <div class="demo-area">
        <div class="resizable-box" id="box1" data-name="盒子A">
            <div class="size-label" id="box1Label">0 x 0</div>
            <p>拖拽右下角调整大小</p>
            <div class="resize-hint">⇵</div>
        </div>
        <div class="resizable-box" id="box2" data-name="盒子B" style="border-color: #E91E63;">
            <div class="size-label" id="box2Label" style="color: #E91E63;">0 x 0</div>
            <p>拖拽右下角调整大小</p>
            <div class="resize-hint">⇵</div>
        </div>
    </div>

    <h2>Flex 布局元素监听</h2>
    <p>调整窗口大小,观察 flex 子元素的尺寸变化。</p>
    <div class="flex-container" id="flexContainer">
        <div class="flex-item" data-name="Flex-1">
            <div>项目 1</div>
            <div class="size" id="flex1Size">0 x 0</div>
        </div>
        <div class="flex-item" data-name="Flex-2">
            <div>项目 2</div>
            <div class="size" id="flex2Size">0 x 0</div>
        </div>
        <div class="flex-item" data-name="Flex-3">
            <div>项目 3</div>
            <div class="size" id="flex3Size">0 x 0</div>
        </div>
    </div>

    <div class="observer-log">
        <h3>ResizeObserver 日志</h3>
        <div class="log-entries" id="logEntries"></div>
    </div>

    <script>
        const logEntries = document.getElementById('logEntries');

        // 创建 ResizeObserver
        const observer = new ResizeObserver(entries => {
            for (const entry of entries) {
                const name = entry.target.dataset.name;
                const width = Math.round(entry.contentRect.width);
                const height = Math.round(entry.contentRect.height);

                // 更新尺寸显示
                if (entry.target.id === 'box1') {
                    document.getElementById('box1Label').textContent = width + ' x ' + height;
                } else if (entry.target.id === 'box2') {
                    document.getElementById('box2Label').textContent = width + ' x ' + height;
                }

                // 更新 flex 项目尺寸
                if (entry.target.classList.contains('flex-item')) {
                    const sizeEl = entry.target.querySelector('.size');
                    if (sizeEl) sizeEl.textContent = width + ' x ' + height;
                }

                // 记录日志
                addLog(name, width, height);
            }
        });

        // 观察可调整大小的盒子
        observer.observe(document.getElementById('box1'));
        observer.observe(document.getElementById('box2'));

        // 观察 flex 子元素
        document.querySelectorAll('.flex-item').forEach(item => {
            observer.observe(item);
        });

        function addLog(name, width, height) {
            const time = new Date().toLocaleTimeString('zh-CN', { hour12: false, fractionalSecondDigits: 1 });
            const entry = document.createElement('div');
            entry.className = 'log-entry';
            entry.innerHTML = `
                <span class="timestamp">${time}</span>
                <span class="element-name">[${name}]</span>
                <span class="dimensions">${width} x ${height}</span>
            `;
            logEntries.insertBefore(entry, logEntries.firstChild);

            // 限制日志数量
            while (logEntries.children.length > 50) {
                logEntries.removeChild(logEntries.lastChild);
            }
        }
    </script>
</body>
</html>

示例5:布局重排优化

代码示例

<!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;
            padding: 20px;
            background: #f5f5f5;
        }
        .demo-section {
            margin-bottom: 30px;
            padding: 20px;
            background: white;
            border-radius: 12px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        .demo-section h3 { margin-top: 0; }
        .chart-container {
            position: relative;
            width: 100%;
            height: 200px;
            background: #fafafa;
            border: 1px solid #eee;
            border-radius: 8px;
            overflow: hidden;
        }
        .bar-chart {
            display: flex;
            align-items: flex-end;
            justify-content: space-around;
            height: 100%;
            padding: 20px;
        }
        .bar {
            flex: 1;
            max-width: 60px;
            background: linear-gradient(to top, #2196F3, #64B5F6);
            border-radius: 4px 4px 0 0;
            transition: height 0.3s ease;
            position: relative;
        }
        .bar-label {
            position: absolute;
            bottom: -25px;
            left: 50%;
            transform: translateX(-50%);
            font-size: 12px;
            color: #666;
            white-space: nowrap;
        }
        .sidebar-layout {
            display: flex;
            gap: 20px;
            min-height: 300px;
        }
        .sidebar {
            width: 200px;
            background: #e3f2fd;
            border-radius: 8px;
            padding: 15px;
            flex-shrink: 0;
            transition: width 0.3s;
        }
        .sidebar.collapsed {
            width: 60px;
        }
        .sidebar h4 { margin-top: 0; font-size: 14px; }
        .sidebar ul {
            list-style: none;
            padding: 0;
        }
        .sidebar li {
            padding: 8px;
            font-size: 13px;
            cursor: pointer;
            border-radius: 4px;
            transition: background 0.2s;
        }
        .sidebar li:hover { background: rgba(33, 150, 243, 0.1); }
        .main-area {
            flex: 1;
            background: #f5f5f5;
            border-radius: 8px;
            padding: 20px;
        }
        .toggle-btn {
            padding: 8px 16px;
            background: #2196F3;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin-bottom: 10px;
        }
        .perf-tip {
            padding: 12px 16px;
            background: #fff3e0;
            border-left: 4px solid #FF9800;
            border-radius: 4px;
            font-size: 13px;
            line-height: 1.6;
            margin-top: 15px;
        }
    </style>
</head>
<body>
    <h1>布局重排优化</h1>

    <div class="demo-section">
        <h3>使用 CSS transform 替代布局属性变化</h3>
        <p>调整窗口大小,柱状图高度使用 CSS transition 平滑过渡,避免 JavaScript 直接操作布局属性。</p>
        <div class="chart-container">
            <div class="bar-chart" id="barChart">
                <div class="bar" style="height: 60%;"><span class="bar-label">一月</span></div>
                <div class="bar" style="height: 80%;"><span class="bar-label">二月</span></div>
                <div class="bar" style="height: 45%;"><span class="bar-label">三月</span></div>
                <div class="bar" style="height: 90%;"><span class="bar-label">四月</span></div>
                <div class="bar" style="height: 70%;"><span class="bar-label">五月</span></div>
                <div class="bar" style="height: 55%;"><span class="bar-label">六月</span></div>
            </div>
        </div>
        <div class="perf-tip">
            优化提示:使用 CSS transition 和 transform 进行动画,利用 GPU 加速,避免触发 CPU 端的布局重排。
        </div>
    </div>

    <div class="demo-section">
        <h3>使用 will-change 提示浏览器优化</h3>
        <button class="toggle-btn" id="toggleSidebar">切换侧边栏</button>
        <div class="sidebar-layout" id="sidebarLayout">
            <div class="sidebar" id="sidebar">
                <h4>导航菜单</h4>
                <ul>
                    <li>首页</li>
                    <li>产品</li>
                    <li>关于</li>
                    <li>联系</li>
                </ul>
            </div>
            <div class="main-area">
                <h3>主内容区域</h3>
                <p>侧边栏使用 CSS transition 实现宽度变化动画,并使用 will-change 提示浏览器提前优化。</p>
            </div>
        </div>
        <div class="perf-tip">
            优化提示:为频繁变化的元素添加 will-change 属性,但不要滥用,否则会消耗额外内存。
        </div>
    </div>

    <script>
        // 侧边栏切换
        const sidebar = document.getElementById('sidebar');
        const toggleBtn = document.getElementById('toggleSidebar');
        let isCollapsed = false;

        // 使用 will-change 优化
        sidebar.style.willChange = 'width';

        toggleBtn.addEventListener('click', function() {
            isCollapsed = !isCollapsed;
            sidebar.classList.toggle('collapsed', isCollapsed);
        });

        // resize 时使用 requestAnimationFrame 批量更新
        let resizeTicking = false;

        window.addEventListener('resize', function() {
            if (resizeTicking) return;
            resizeTicking = true;

            requestAnimationFrame(() => {
                // 在这里执行需要响应窗口变化的逻辑
                resizeTicking = false;
            });
        }, { passive: true });
    </script>
</body>
</html>

示例6:综合应用 - 自适应仪表盘

代码示例

<!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>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: "Microsoft YaHei", sans-serif;
            background: #0f0f23;
            color: white;
            min-height: 100vh;
        }
        .dashboard {
            padding: 20px;
            max-width: 1400px;
            margin: 0 auto;
        }
        .dashboard-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
            flex-wrap: wrap;
            gap: 10px;
        }
        .dashboard-header h1 { font-size: 24px; }
        .viewport-info {
            font-size: 13px;
            color: #888;
            background: #1a1a3e;
            padding: 6px 14px;
            border-radius: 20px;
        }
        .stats-row {
            display: grid;
            grid-template-columns: repeat(4, 1fr);
            gap: 16px;
            margin-bottom: 20px;
        }
        .stat-card {
            background: #1a1a3e;
            border-radius: 12px;
            padding: 20px;
            position: relative;
            overflow: hidden;
        }
        .stat-card::before {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            height: 3px;
        }
        .stat-card:nth-child(1)::before { background: #2196F3; }
        .stat-card:nth-child(2)::before { background: #4CAF50; }
        .stat-card:nth-child(3)::before { background: #FF9800; }
        .stat-card:nth-child(4)::before { background: #E91E63; }
        .stat-label {
            font-size: 13px;
            color: #888;
            margin-bottom: 8px;
        }
        .stat-value {
            font-size: 28px;
            font-weight: bold;
        }
        .stat-change {
            font-size: 12px;
            margin-top: 5px;
        }
        .stat-change.up { color: #4CAF50; }
        .stat-change.down { color: #f44336; }
        .charts-row {
            display: grid;
            grid-template-columns: 2fr 1fr;
            gap: 16px;
            margin-bottom: 20px;
        }
        .chart-card {
            background: #1a1a3e;
            border-radius: 12px;
            padding: 20px;
        }
        .chart-card h3 {
            margin-top: 0;
            margin-bottom: 15px;
            font-size: 16px;
        }
        .mini-chart {
            height: 150px;
            display: flex;
            align-items: flex-end;
            gap: 4px;
        }
        .mini-bar {
            flex: 1;
            background: linear-gradient(to top, #667eea, #764ba2);
            border-radius: 3px 3px 0 0;
            min-height: 10px;
            transition: height 0.5s ease;
        }
        .list-items {
            list-style: none;
        }
        .list-items li {
            padding: 10px 0;
            border-bottom: 1px solid #2a2a4a;
            display: flex;
            justify-content: space-between;
            font-size: 14px;
        }
        .list-items li:last-child { border-bottom: none; }

        /* 响应式 */
        @media (max-width: 992px) {
            .stats-row { grid-template-columns: repeat(2, 1fr); }
            .charts-row { grid-template-columns: 1fr; }
        }
        @media (max-width: 576px) {
            .stats-row { grid-template-columns: 1fr; }
            .dashboard { padding: 10px; }
        }
    </style>
</head>
<body>
    <div class="dashboard">
        <div class="dashboard-header">
            <h1>数据仪表盘</h1>
            <span class="viewport-info" id="viewportInfo">0 x 0</span>
        </div>

        <div class="stats-row" id="statsRow">
            <div class="stat-card">
                <div class="stat-label">总访问量</div>
                <div class="stat-value">12,847</div>
                <div class="stat-change up">+12.5%</div>
            </div>
            <div class="stat-card">
                <div class="stat-label">活跃用户</div>
                <div class="stat-value">3,426</div>
                <div class="stat-change up">+8.3%</div>
            </div>
            <div class="stat-card">
                <div class="stat-label">转化率</div>
                <div class="stat-value">4.7%</div>
                <div class="stat-change down">-0.5%</div>
            </div>
            <div class="stat-card">
                <div class="stat-label">收入</div>
                <div class="stat-value">¥89,230</div>
                <div class="stat-change up">+15.2%</div>
            </div>
        </div>

        <div class="charts-row">
            <div class="chart-card">
                <h3>访问趋势</h3>
                <div class="mini-chart" id="trendChart"></div>
            </div>
            <div class="chart-card">
                <h3>热门页面</h3>
                <ul class="list-items">
                    <li><span>首页</span><span>4,230</span></li>
                    <li><span>产品页</span><span>2,847</span></li>
                    <li><span>博客</span><span>1,923</span></li>
                    <li><span>关于我们</span><span>1,205</span></li>
                    <li><span>联系方式</span><span>876</span></li>
                </ul>
            </div>
        </div>
    </div>

    <script>
        const viewportInfo = document.getElementById('viewportInfo');
        const trendChart = document.getElementById('trendChart');

        // 生成趋势图
        function generateTrendChart() {
            trendChart.innerHTML = '';
            const data = Array.from({ length: 20 }, () => Math.random() * 80 + 20);
            data.forEach(value => {
                const bar = document.createElement('div');
                bar.className = 'mini-bar';
                bar.style.height = value + '%';
                trendChart.appendChild(bar);
            });
        }

        generateTrendChart();

        // 使用 ResizeObserver 监听仪表盘容器
        const dashboard = document.querySelector('.dashboard');
        let resizeTimer = null;

        const resizeObserver = new ResizeObserver(entries => {
            for (const entry of entries) {
                const w = Math.round(entry.contentRect.width);
                const h = Math.round(entry.contentRect.height);
                viewportInfo.textContent = `${window.innerWidth} x ${window.innerHeight} (容器: ${w}px)`;
            }
        });

        resizeObserver.observe(dashboard);

        // 窗口 resize 时更新视口信息
        window.addEventListener('resize', function() {
            clearTimeout(resizeTimer);
            resizeTimer = setTimeout(() => {
                viewportInfo.textContent = `${window.innerWidth} x ${window.innerHeight}`;
            }, 100);
        });
    </script>
</body>
</html>

浏览器兼容性

API/特性 Chrome Firefox Safari Edge IE
window.resize 事件 全部 全部 全部 全部 5+
window.innerWidth/Height 全部 全部 全部 全部 9+
ResizeObserver 64+ 69+ 13.1+ 79+ 不支持
ResizeObserver borderBoxSize 84+ 75+ 15.4+ 84+ 不支持
CSS resize 属性 4+ 5+ 3+ 79+ 不支持
devicePixelRatio 全部 全部 全部 全部 11+
matchMedia 9+ 6+ 5.1+ 12+ 10+

注意事项与最佳实践

1. 始终使用防抖处理

resize 事件在调整窗口过程中高频触发,必须使用防抖处理。

代码示例

let resizeTimer;
window.addEventListener('resize', function() {
    clearTimeout(resizeTimer);
    resizeTimer = setTimeout(handleResize, 200);
});

2. 优先使用 CSS 媒体查询

对于纯视觉的响应式变化,优先使用 CSS 媒体查询而非 JavaScript resize 事件。

代码示例

/* 推荐:CSS 方式 */
@media (max-width: 768px) {
    .sidebar { display: none; }
}

/* 不推荐:JavaScript 方式 */
// window.addEventListener('resize', function() {
//     if (window.innerWidth < 768) sidebar.style.display = 'none';
// });

3. 使用 matchMedia 替代 resize 断点检测

当需要在 JavaScript 中响应特定断点时,使用 matchMedia 比 resize 事件更高效。

代码示例

const mq = window.matchMedia('(max-width: 768px)');
mq.addEventListener('change', function(e) {
    if (e.matches) {
        // 小屏幕逻辑
    } else {
        // 大屏幕逻辑
    }
});

4. 使用 ResizeObserver 监听元素尺寸

当需要监听特定元素的尺寸变化时,使用 ResizeObserver 而非在 resize 事件中手动计算。

代码示例

const observer = new ResizeObserver(entries => {
    for (const entry of entries) {
        const { width, height } = entry.contentRect;
        // 处理尺寸变化
    }
});
observer.observe(targetElement);

5. 避免在 resize 回调中触发布局重排

不要在 resize 回调中读取 offsetWidth、getBoundingClientRect() 等触发布局计算的属性,应缓存这些值。

6. 使用 will-change 优化频繁变化的元素

对于 resize 过程中频繁变化的元素,可以添加 will-change 提示浏览器优化。

代码示例

.resizable-element {
    will-change: width, height;
}

但注意不要滥用,只在确实需要的元素上使用。


代码规范示例

规范的响应式处理模块

代码示例

class ResponsiveHandler {
    constructor(options = {}) {
        this.debounceDelay = options.debounceDelay || 200;
        this.breakpoints = options.breakpoints || {
            xs: 0, sm: 576, md: 768, lg: 992, xl: 1200
        };
        this.currentBreakpoint = null;
        this.callbacks = {};
        this.resizeTimer = null;
        this.boundHandleResize = this._handleResize.bind(this);
    }

    start() {
        window.addEventListener('resize', this.boundHandleResize);
        this._handleResize(); // 初始化
        return this;
    }

    stop() {
        window.removeEventListener('resize', this.boundHandleResize);
        clearTimeout(this.resizeTimer);
        return this;
    }

    on(breakpoint, callback) {
        if (!this.callbacks[breakpoint]) {
            this.callbacks[breakpoint] = [];
        }
        this.callbacks[breakpoint].push(callback);
        return this;
    }

    _handleResize() {
        clearTimeout(this.resizeTimer);
        this.resizeTimer = setTimeout(() => {
            const bp = this._getCurrentBreakpoint();
            if (bp !== this.currentBreakpoint) {
                const prev = this.currentBreakpoint;
                this.currentBreakpoint = bp;
                this._triggerCallbacks(bp, prev);
            }
        }, this.debounceDelay);
    }

    _getCurrentBreakpoint() {
        const w = window.innerWidth;
        let current = 'xs';
        for (const [name, minWidth] of Object.entries(this.breakpoints)) {
            if (w >= minWidth) current = name;
        }
        return current;
    }

    _triggerCallbacks(current, previous) {
        const callbacks = this.callbacks[current] || [];
        callbacks.forEach(cb => cb(current, previous));
    }

    getCurrentBreakpoint() {
        return this.currentBreakpoint;
    }

    is(breakpoint) {
        return this.currentBreakpoint === breakpoint;
    }

    isAbove(breakpoint) {
        return window.innerWidth >= this.breakpoints[breakpoint];
    }

    isBelow(breakpoint) {
        return window.innerWidth < this.breakpoints[breakpoint];
    }
}

常见问题与解决方案

问题1:resize 事件导致页面卡顿

原因:resize 事件高频触发,处理程序中执行了 DOM 操作或布局读取。

解决方案:使用防抖处理,并避免在回调中触发布局重排。

问题2:移动端旋转屏幕时布局异常

原因:移动端旋转时 resize 事件触发时机不一致,部分浏览器在旋转动画完成前触发。

解决方案:使用 orientationchange 事件配合 matchMedia,或增加防抖延迟。

代码示例

const mq = window.matchMedia('(orientation: portrait)');
mq.addEventListener('change', function() {
    setTimeout(handleOrientationChange, 300);
});

问题3:ResizeObserver 回调中修改被观察元素的尺寸导致循环

原因:在 ResizeObserver 回调中修改被观察元素的尺寸,会再次触发回调。

解决方案:在回调中检查尺寸是否真的变化,或使用 requestAnimationFrame 延迟修改。

代码示例

const observer = new ResizeObserver(entries => {
    requestAnimationFrame(() => {
        for (const entry of entries) {
            const { width } = entry.contentRect;
            // 修改元素尺寸
        }
    });
});

问题4:IE 不支持 ResizeObserver

解决方案:使用 polyfill 或回退到 resize 事件 + MutationObserver。

代码示例

// 检测支持
if (typeof ResizeObserver !== 'undefined') {
    const observer = new ResizeObserver(callback);
    observer.observe(element);
} else {
    // 回退方案
    window.addEventListener('resize', debounce(callback, 200));
}

问题5:resize 与 CSS 媒体查询不同步

原因:JavaScript 的 resize 事件和 CSS 媒体查询使用不同的触发机制。

解决方案:使用 matchMedia 确保 JavaScript 和 CSS 使用相同的断点逻辑。

代码示例

const mq = window.matchMedia('(max-width: 768px)');
function handleMQChange(e) {
    document.body.classList.toggle('mobile', e.matches);
}
mq.addEventListener('change', handleMQChange);
handleMQChange(mq);

总结

onresize 事件是实现响应式布局的重要工具,但其高频触发特性需要谨慎处理。以下是核心要点:

  1. 防抖处理:始终对 resize 事件使用防抖处理,避免高频触发导致的性能问题。

  2. CSS 优先:对于纯视觉的响应式变化,优先使用 CSS 媒体查询而非 JavaScript。

  3. matchMedia:在 JavaScript 中响应断点变化时,使用 matchMedia 比 resize 事件更高效。

  4. ResizeObserver:监听元素尺寸变化时,优先使用 ResizeObserver,它提供了更精确的尺寸信息和更好的性能。

  5. 避免重排:不在 resize 回调中读取触发布局计算的属性,缓存布局信息。

  6. will-change:对频繁变化的元素使用 will-change 提示浏览器优化,但不要滥用。

  7. 移动端适配:注意移动端旋转屏幕的特殊处理,使用 orientationchange 和 matchMedia 配合。

  8. 兼容性:为不支持 ResizeObserver 的浏览器提供回退方案。

正确使用 resize 事件和相关 API,能够在保证性能的前提下实现流畅的响应式体验。

常见问题

问题1:resize 事件导致页面卡顿?

resize 事件高频触发,处理程序中执行了 DOM 操作或布局读取。 使用防抖处理,并避免在回调中触发布局重排。

问题2:移动端旋转屏幕时布局异常?

移动端旋转时 resize 事件触发时机不一致,部分浏览器在旋转动画完成前触发。 使用 orientationchange 事件配合 matchMedia,或增加防抖延迟。

问题3:ResizeObserver 回调中修改被观察元素的尺寸导致循环?

在 ResizeObserver 回调中修改被观察元素的尺寸,会再次触发回调。 在回调中检查尺寸是否真的变化,或使用 requestAnimationFrame 延迟修改。

问题4:IE 不支持 ResizeObserver?

使用 polyfill 或回退到 resize 事件 + MutationObserver。

问题5:resize 与 CSS 媒体查询不同步?

JavaScript 的 resize 事件和 CSS 媒体查询使用不同的触发机制。 使用 matchMedia 确保 JavaScript 和 CSS 使用相同的断点逻辑。

标签: Script标签 JavaScript DOM HTML CSS 事件 onchange onresize

本文由小确幸生活整理发布,转载请注明出处

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: JS集成:HTML onscroll 事 - 详细教程与实战指南 下一篇: JS集成:HTML ondrag 事件详 - 详细教程与实战指南

poll相关推荐