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 中响应特定断点时,使用 matchMediaresize 事件更高效。

代码示例

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 回调中读取 offsetWidthgetBoundingClientRect() 等触发布局计算的属性,应缓存这些值。

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 中响应断点变化时,使用 matchMediaresize 事件更高效。

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

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

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

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

  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相关推荐