d35211071a
- standardize border-radius from 8px to 6px for consistent styling - adjust padding values from 1rem to 0.75rem for better visual hierarchy - remove redundant overflow-y properties for cleaner code
451 lines
11 KiB
Vue
451 lines
11 KiB
Vue
<!--
|
||
ReflowRepaintDemo.vue
|
||
重排与重绘演示
|
||
-->
|
||
<template>
|
||
<div class="reflow-demo">
|
||
<div class="demo-header">
|
||
<span class="icon">⚡</span>
|
||
<span class="title">重排与重绘</span>
|
||
<span class="subtitle">观察不同操作对性能的影响</span>
|
||
</div>
|
||
|
||
<div class="tabs">
|
||
<button
|
||
v-for="tab in tabs"
|
||
:key="tab.id"
|
||
class="tab-btn"
|
||
:class="{ active: activeTab === tab.id }"
|
||
@click="activeTab = tab.id"
|
||
>
|
||
<span class="tab-icon">{{ tab.icon }}</span>
|
||
<span class="tab-label">{{ tab.label }}</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="demo-area">
|
||
<div class="canvas-area">
|
||
<div class="box-container">
|
||
<div
|
||
v-for="box in boxes"
|
||
:key="box.id"
|
||
class="box"
|
||
:style="getBoxStyle(box)"
|
||
>
|
||
{{ box.id }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="performance-meter">
|
||
<div class="meter-label">性能影响</div>
|
||
<div class="meter-bar">
|
||
<div
|
||
class="meter-fill"
|
||
:class="performanceLevel.class"
|
||
:style="{ width: performanceImpact + '%' }"
|
||
></div>
|
||
</div>
|
||
<div class="meter-value" :class="performanceLevel.class">
|
||
{{ performanceLevel.text }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stats">
|
||
<div class="stat-item">
|
||
<div class="stat-label">操作类型</div>
|
||
<div class="stat-value">{{ currentOperation }}</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-label">影响范围</div>
|
||
<div class="stat-value">{{ affectedElements }} 个元素</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<div v-if="activeTab === 'reflow'" class="control-group">
|
||
<button @click="changeWidth" class="btn high-impact">改变宽度</button>
|
||
<button @click="changePosition" class="btn high-impact">改变位置</button>
|
||
<button @click="addBox" class="btn high-impact">添加元素</button>
|
||
</div>
|
||
|
||
<div v-if="activeTab === 'repaint'" class="control-group">
|
||
<button @click="changeColor" class="btn medium-impact">改变颜色</button>
|
||
<button @click="changeBackground" class="btn medium-impact">
|
||
改变背景
|
||
</button>
|
||
<button @click="toggleBorder" class="btn medium-impact">切换边框</button>
|
||
</div>
|
||
|
||
<div v-if="activeTab === 'composite'" class="control-group">
|
||
<button @click="transformTranslate" class="btn low-impact">
|
||
Transform 位移
|
||
</button>
|
||
<button @click="transformRotate" class="btn low-impact">
|
||
Transform 旋转
|
||
</button>
|
||
<button @click="changeOpacity" class="btn low-impact">
|
||
改变透明度
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<Transition name="fade">
|
||
<div v-if="activeTab" class="tab-info">
|
||
<div v-if="activeTab === 'reflow'" class="info-content">
|
||
<p>
|
||
<strong>重排 (Reflow)</strong>:当元素的位置、尺寸发生变化时,浏览器需要重新计算布局。重排开销最大,因为要重新计算所有受影响元素的位置。
|
||
</p>
|
||
</div>
|
||
<div v-if="activeTab === 'repaint'" class="info-content">
|
||
<p>
|
||
<strong>重绘 (Repaint)</strong>:当元素的外观(颜色、背景)发生变化,但位置不变时,浏览器只需要重新绘制像素。比重排快,但仍有开销。
|
||
</p>
|
||
</div>
|
||
<div v-if="activeTab === 'composite'" class="info-content">
|
||
<p>
|
||
<strong>合成 (Composite)</strong>:使用 transform 和 opacity 等属性,浏览器可以在合成层上完成变化,完全不触发布局和绘制。性能最佳,推荐优先使用。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
|
||
<div class="info-box">
|
||
<span class="icon">💡</span>
|
||
<strong>性能优化原则:</strong>优先使用 transform 和 opacity 进行动画,避免频繁触发布局计算(如 width、height、top、left),可以大幅提升页面性能。
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed } from 'vue'
|
||
|
||
const activeTab = ref('reflow')
|
||
|
||
const tabs = [
|
||
{ id: 'reflow', icon: '🔴', label: '重排' },
|
||
{ id: 'repaint', icon: '🟡', label: '重绘' },
|
||
{ id: 'composite', icon: '🟢', label: '合成' }
|
||
]
|
||
|
||
const boxes = ref([
|
||
{ id: 1, x: 20, y: 20, width: 80, height: 80, bg: 'var(--vp-c-brand-1)', rotation: 0, opacity: 1 },
|
||
{ id: 2, x: 120, y: 20, width: 80, height: 80, bg: 'var(--vp-c-brand-2)', rotation: 0, opacity: 1 },
|
||
{ id: 3, x: 20, y: 120, width: 80, height: 80, bg: 'var(--vp-c-brand-3)', rotation: 0, opacity: 1 }
|
||
])
|
||
|
||
const currentOperation = ref('无')
|
||
const performanceImpact = ref(0)
|
||
const affectedElements = ref(0)
|
||
|
||
const performanceLevel = computed(() => {
|
||
if (performanceImpact.value <= 33) return { class: 'good', text: '低' }
|
||
if (performanceImpact.value <= 66) return { class: 'medium', text: '中' }
|
||
return { class: 'high', text: '高' }
|
||
})
|
||
|
||
function getBoxStyle(box) {
|
||
return {
|
||
left: box.x + 'px',
|
||
top: box.y + 'px',
|
||
width: box.width + 'px',
|
||
height: box.height + 'px',
|
||
backgroundColor: box.bg,
|
||
transform: `rotate(${box.rotation}deg)`,
|
||
opacity: box.opacity
|
||
}
|
||
}
|
||
|
||
function updateMetrics(operation, impact, affected) {
|
||
currentOperation.value = operation
|
||
performanceImpact.value = impact
|
||
affectedElements.value = affected
|
||
}
|
||
|
||
function changeWidth() {
|
||
boxes.value.forEach((box) => { box.width = 60 + Math.random() * 60 })
|
||
updateMetrics('改变宽度', 90, boxes.value.length)
|
||
}
|
||
|
||
function changePosition() {
|
||
boxes.value.forEach((box) => {
|
||
box.x = Math.random() * 150
|
||
box.y = Math.random() * 150
|
||
})
|
||
updateMetrics('改变位置', 85, boxes.value.length)
|
||
}
|
||
|
||
function addBox() {
|
||
const newId = boxes.value.length + 1
|
||
boxes.value.push({
|
||
id: newId,
|
||
x: Math.random() * 100,
|
||
y: Math.random() * 100,
|
||
width: 80,
|
||
height: 80,
|
||
bg: 'var(--vp-c-brand)',
|
||
rotation: 0,
|
||
opacity: 1
|
||
})
|
||
updateMetrics('添加元素', 95, boxes.value.length)
|
||
}
|
||
|
||
function changeColor() {
|
||
const colors = ['var(--vp-c-brand-1)', 'var(--vp-c-brand-2)', 'var(--vp-c-brand-3)']
|
||
boxes.value.forEach((box) => { box.bg = colors[Math.floor(Math.random() * colors.length)] })
|
||
updateMetrics('改变颜色', 50, boxes.value.length)
|
||
}
|
||
|
||
function changeBackground() {
|
||
const bgs = ['var(--vp-c-brand-1)', 'var(--vp-c-brand-2)', 'var(--vp-c-brand-3)']
|
||
boxes.value.forEach((box) => { box.bg = bgs[Math.floor(Math.random() * bgs.length)] })
|
||
updateMetrics('改变背景', 45, boxes.value.length)
|
||
}
|
||
|
||
function toggleBorder() {
|
||
updateMetrics('切换边框', 55, boxes.value.length)
|
||
}
|
||
|
||
function transformTranslate() {
|
||
boxes.value.forEach((box) => { box.x += Math.random() * 20 - 10 })
|
||
updateMetrics('Transform 位移', 10, boxes.value.length)
|
||
}
|
||
|
||
function transformRotate() {
|
||
boxes.value.forEach((box) => { box.rotation += Math.random() * 30 - 15 })
|
||
updateMetrics('Transform 旋转', 10, boxes.value.length)
|
||
}
|
||
|
||
function changeOpacity() {
|
||
boxes.value.forEach((box) => { box.opacity = 0.5 + Math.random() * 0.5 })
|
||
updateMetrics('改变透明度', 10, boxes.value.length)
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.reflow-demo {
|
||
border: 1px solid var(--vp-c-divider);
|
||
border-radius: 6px;
|
||
background: var(--vp-c-bg-soft);
|
||
padding: 0.75rem;
|
||
margin: 0.5rem 0;
|
||
}
|
||
|
||
.demo-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.demo-header .icon { font-size: 1.25rem; }
|
||
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
||
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
|
||
|
||
.tabs {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.tab-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.35rem;
|
||
padding: 0.5rem 0.75rem;
|
||
border: 1px solid var(--vp-c-divider);
|
||
background: var(--vp-c-bg);
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 0.85rem;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.tab-btn:hover {
|
||
background: var(--vp-c-bg-soft);
|
||
}
|
||
|
||
.tab-btn.active {
|
||
background: var(--vp-c-brand);
|
||
color: var(--vp-c-bg-inverse);
|
||
border-color: var(--vp-c-brand);
|
||
}
|
||
|
||
.tab-icon { font-size: 1rem; }
|
||
.tab-label { font-weight: 500; }
|
||
|
||
.demo-area {
|
||
display: grid;
|
||
grid-template-columns: 1fr 200px;
|
||
gap: 1rem;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.demo-area { grid-template-columns: 1fr; }
|
||
}
|
||
|
||
.canvas-area {
|
||
background: var(--vp-c-bg);
|
||
border: 1px solid var(--vp-c-divider);
|
||
border-radius: 6px;
|
||
padding: 0.75rem;
|
||
}
|
||
|
||
.box-container {
|
||
position: relative;
|
||
height: 200px;
|
||
margin-bottom: 1rem;
|
||
border: 2px dashed var(--vp-c-divider);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.box {
|
||
position: absolute;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 700;
|
||
font-size: 1.1rem;
|
||
color: var(--vp-c-bg-inverse);
|
||
border-radius: 6px;
|
||
transition: all 0.3s ease;
|
||
user-select: none;
|
||
}
|
||
|
||
.performance-meter {
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.meter-label {
|
||
font-size: 0.8rem;
|
||
color: var(--vp-c-text-2);
|
||
margin-bottom: 0.35rem;
|
||
}
|
||
|
||
.meter-bar {
|
||
height: 10px;
|
||
background: var(--vp-c-bg-soft);
|
||
border-radius: 5px;
|
||
overflow: hidden;
|
||
margin-bottom: 0.35rem;
|
||
}
|
||
|
||
.meter-fill {
|
||
height: 100%;
|
||
transition: all 0.5s ease;
|
||
border-radius: 5px;
|
||
}
|
||
|
||
.meter-fill.good { background: var(--vp-c-success-1); }
|
||
.meter-fill.medium { background: var(--vp-c-warning-1); }
|
||
.meter-fill.high { background: var(--vp-c-error-1); }
|
||
|
||
.meter-value {
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
text-align: right;
|
||
}
|
||
|
||
.meter-value.good { color: var(--vp-c-success-1); }
|
||
.meter-value.medium { color: var(--vp-c-warning-1); }
|
||
.meter-value.high { color: var(--vp-c-error-1); }
|
||
|
||
.stats {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.stat-item {
|
||
flex: 1;
|
||
background: var(--vp-c-bg-soft);
|
||
border-radius: 6px;
|
||
padding: 0.6rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 0.7rem;
|
||
color: var(--vp-c-text-2);
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
color: var(--vp-c-text-1);
|
||
}
|
||
|
||
.controls {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.control-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.btn {
|
||
padding: 0.6rem;
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-size: 0.85rem;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
color: var(--vp-c-bg-inverse);
|
||
}
|
||
|
||
.btn.high-impact { background: var(--vp-c-error-1); }
|
||
.btn.high-impact:hover { opacity: 0.9; }
|
||
|
||
.btn.medium-impact { background: var(--vp-c-warning-1); }
|
||
.btn.medium-impact:hover { opacity: 0.9; }
|
||
|
||
.btn.low-impact { background: var(--vp-c-success-1); }
|
||
.btn.low-impact:hover { opacity: 0.9; }
|
||
|
||
.tab-info {
|
||
background: var(--vp-c-bg);
|
||
border-radius: 6px;
|
||
padding: 0.75rem;
|
||
margin-top: 0.75rem;
|
||
border: 1px solid var(--vp-c-divider);
|
||
}
|
||
|
||
.info-content {
|
||
font-size: 0.85rem;
|
||
color: var(--vp-c-text-2);
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.info-content strong {
|
||
color: var(--vp-c-text-1);
|
||
}
|
||
|
||
.fade-enter-active,
|
||
.fade-leave-active {
|
||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||
}
|
||
|
||
.fade-enter-from,
|
||
.fade-leave-to {
|
||
opacity: 0;
|
||
transform: translateY(-10px);
|
||
}
|
||
|
||
.info-box {
|
||
background: var(--vp-c-bg-alt);
|
||
padding: 0.75rem;
|
||
border-radius: 6px;
|
||
font-size: 0.85rem;
|
||
color: var(--vp-c-text-2);
|
||
margin-top: 0.75rem;
|
||
}
|
||
|
||
.info-box .icon { margin-right: 0.25rem; }
|
||
</style>
|