feat(docs): enhance JavaScript runtime and browser-as-os content

refactor(demos): improve variable box, scope, and type annotation demos
style(demos): update visual styles and animations for better UX
docs(browser-as-os): restructure content with tables and practical examples
feat(demos): add new TypeScript and runtime environment demos
This commit is contained in:
sanbuphy
2026-02-17 01:39:59 +08:00
parent 9ee3312569
commit 47377646df
20 changed files with 8097 additions and 1310 deletions
@@ -0,0 +1,542 @@
<script setup>
import { ref } from 'vue'
const isAnimating = ref(false)
const currentStep = ref(0)
const callStack = ref([])
const output = ref([])
const codeSteps = [
{ action: 'push', function: 'main', description: '调用 main()', code: 'main()' },
{ action: 'push', function: 'a', description: 'main() 调用 a()', code: 'function a() {' },
{ action: 'push', function: 'b', description: 'a() 调用 b()', code: 'function b() {' },
{ action: 'push', function: 'c', description: 'b() 调用 c()', code: 'function c() {' },
{ action: 'log', function: 'c', description: 'c() 执行 console.log', code: 'console.log("执行完毕")', output: '执行完毕' },
{ action: 'pop', function: 'c', description: 'c() 执行完成,从栈中弹出', code: '}' },
{ action: 'pop', function: 'b', description: 'b() 执行完成,从栈中弹出', code: '}' },
{ action: 'pop', function: 'a', description: 'a() 执行完成,从栈中弹出', code: '}' },
{ action: 'pop', function: 'main', description: 'main() 执行完成,从栈中弹出', code: '}' }
]
const reset = () => {
currentStep.value = 0
callStack.value = []
output.value = []
isAnimating.value = false
}
const nextStep = () => {
if (currentStep.value >= codeSteps.length) return
const step = codeSteps[currentStep.value]
if (step.action === 'push') {
callStack.value.push({
function: step.function,
code: step.code,
active: true
})
// 标记之前的为非活动
callStack.value.forEach((item, index) => {
if (index < callStack.value.length - 1) {
item.active = false
}
})
} else if (step.action === 'pop') {
callStack.value.pop()
// 标记新的顶部为活动
if (callStack.value.length > 0) {
callStack.value[callStack.value.length - 1].active = true
}
} else if (step.action === 'log') {
output.value.push(step.output)
}
currentStep.value++
}
const play = async () => {
if (isAnimating.value) return
isAnimating.value = true
reset()
while (currentStep.value < codeSteps.length && isAnimating.value) {
nextStep()
await new Promise(resolve => setTimeout(resolve, 1200))
}
isAnimating.value = false
}
const stop = () => {
isAnimating.value = false
}
</script>
<template>
<div class="call-stack-demo">
<h3>调用栈:函数执行的足迹</h3>
<div class="demo-layout">
<!-- 代码显示 -->
<div class="code-section">
<h4>代码</h4>
<div class="code-display">
<div
v-for="(step, index) in codeSteps"
:key="index"
class="code-line"
:class="{
'current': currentStep === index,
'executed': currentStep > index
}"
>
<span class="line-number">{{ index + 1 }}</span>
<span class="line-code">{{ step.code }}</span>
</div>
</div>
</div>
<!-- 调用栈可视化 -->
<div class="stack-section">
<h4>调用栈</h4>
<div class="stack-container">
<div class="stack-base">
<div class="stack-label">栈底</div>
</div>
<div class="stack-frames">
<transition-group name="stack-frame">
<div
v-for="(frame, index) in callStack"
:key="`${frame.function}-${index}`"
class="stack-frame"
:class="{ 'active': frame.active }"
:style="{ bottom: `${index * 60}px` }"
>
<div class="frame-function">{{ frame.function }}()</div>
<div class="frame-code">{{ frame.code }}</div>
</div>
</transition-group>
<div v-if="callStack.length === 0" class="empty-stack">
栈为空
</div>
</div>
<div class="stack-top">
<div class="stack-label">栈顶</div>
</div>
</div>
<div class="stack-explanation">
<p><strong>当前状态:</strong></p>
<p v-if="currentStep < codeSteps.length">
{{ codeSteps[currentStep]?.description }}
</p>
<p v-else>
执行完成
</p>
</div>
</div>
</div>
<!-- 输出显示 -->
<div class="output-section">
<h4>输出</h4>
<div class="output-container">
<div v-if="output.length === 0" class="empty-output">
等待输出...
</div>
<transition-group name="output">
<div
v-for="(log, index) in output"
:key="`log-${index}`"
class="output-line"
>
{{ log }}
</div>
</transition-group>
</div>
</div>
<!-- 控制按钮 -->
<div class="controls">
<button @click="play" :disabled="isAnimating" class="btn-play">
{{ isAnimating ? '执行中...' : '▶ 自动演示' }}
</button>
<button @click="nextStep" :disabled="isAnimating || currentStep >= codeSteps.length" class="btn-step">
单步执行
</button>
<button @click="stop" :disabled="!isAnimating" class="btn-stop">
停止
</button>
<button @click="reset" :disabled="isAnimating" class="btn-reset">
🔄 重置
</button>
</div>
<!-- 说明 -->
<div class="explanation-box">
<p><strong>调用栈工作原理:</strong></p>
<ul>
<li>每次调用函数,就会在栈上"压入"一个新的"栈帧"</li>
<li>栈帧记录了函数的执行状态局部变量等信息</li>
<li>函数执行完毕,栈帧就会从栈上"弹出"</li>
<li>栈是"后进先出"(LIFO)的数据结构</li>
<li>如果递归太深,会导致"栈溢出"错误</li>
</ul>
<p class="highlight">
调用栈就像一摞盘子:最后放上去的盘子最先被取走每个函数就是一个盘子,执行完就取走,然后继续执行下面的函数
</p>
</div>
</div>
</template>
<style scoped>
.call-stack-demo {
border: 1px solid var(--vp-c-border);
border-radius: 12px;
padding: 24px;
margin: 24px 0;
background: var(--vp-c-bg);
}
h3 {
margin: 0 0 20px 0;
font-size: 18px;
font-weight: 600;
color: var(--vp-c-text-1);
}
h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
}
.demo-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-bottom: 20px;
}
@media (max-width: 1024px) {
.demo-layout {
grid-template-columns: 1fr;
}
}
.code-section,
.stack-section {
border: 2px solid var(--vp-c-border);
border-radius: 8px;
padding: 16px;
background: var(--vp-c-bg-soft);
}
.code-display {
background: #1e1e1e;
border-radius: 6px;
padding: 12px;
font-family: 'Courier New', monospace;
}
.code-line {
display: flex;
gap: 12px;
padding: 6px 8px;
border-radius: 4px;
transition: all 0.3s ease;
}
.code-line.current {
background: rgba(62, 175, 124, 0.2);
border-left: 3px solid var(--vp-c-brand-1);
}
.code-line.executed {
opacity: 0.5;
}
.line-number {
color: #858585;
font-size: 12px;
min-width: 20px;
text-align: right;
user-select: none;
}
.line-code {
color: #d4d4d4;
font-size: 13px;
}
.stack-container {
position: relative;
height: 350px;
background: var(--vp-c-bg);
border-radius: 8px;
padding: 12px;
margin-top: 12px;
}
.stack-base,
.stack-top {
display: flex;
justify-content: center;
padding: 8px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
margin-bottom: 8px;
}
.stack-top {
margin-top: 8px;
margin-bottom: 0;
}
.stack-label {
font-size: 12px;
font-weight: 600;
color: var(--vp-c-text-2);
}
.stack-frames {
position: relative;
flex: 1;
}
.stack-frame {
position: absolute;
left: 12px;
right: 12px;
padding: 12px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-border);
border-radius: 6px;
transition: all 0.4s ease;
}
.stack-frame.active {
border-color: var(--vp-c-brand-1);
background: rgba(62, 175, 124, 0.1);
box-shadow: 0 0 0 3px rgba(62, 175, 124, 0.1);
}
.stack-frame-enter-active,
.stack-frame-leave-active {
transition: all 0.4s ease;
}
.stack-frame-enter-from {
opacity: 0;
transform: translateY(-20px);
}
.stack-frame-leave-to {
opacity: 0;
transform: translateY(20px);
}
.frame-function {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-brand-1);
margin-bottom: 4px;
font-family: 'Courier New', monospace;
}
.frame-code {
font-size: 11px;
color: var(--vp-c-text-2);
font-family: 'Courier New', monospace;
}
.empty-stack {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--vp-c-text-3);
font-size: 14px;
}
.stack-explanation {
margin-top: 12px;
padding: 12px;
background: rgba(62, 175, 124, 0.1);
border-radius: 6px;
}
.stack-explanation p {
margin: 0;
font-size: 13px;
line-height: 1.6;
color: var(--vp-c-text-1);
}
.stack-explanation strong {
color: var(--vp-c-brand-1);
}
.output-section {
margin-bottom: 20px;
}
.output-container {
min-height: 60px;
padding: 12px;
border: 2px solid var(--vp-c-border);
border-radius: 8px;
background: var(--vp-c-bg);
}
.empty-output {
color: var(--vp-c-text-3);
font-size: 14px;
}
.output-line {
padding: 8px 12px;
margin-bottom: 8px;
background: var(--vp-c-brand-1);
color: white;
border-radius: 6px;
font-size: 14px;
font-family: 'Courier New', monospace;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.output-enter-active,
.output-leave-active {
transition: all 0.3s ease;
}
.output-enter-from {
opacity: 0;
transform: translateY(-10px);
}
.output-leave-to {
opacity: 0;
transform: translateY(10px);
}
.controls {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
button:active {
transform: scale(0.95);
}
.btn-play {
background: var(--vp-c-brand-1);
color: white;
}
.btn-play:hover:not(:disabled) {
background: var(--vp-c-brand-2);
}
.btn-step {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.btn-step:hover:not(:disabled) {
background: var(--vp-c-bg-soft-hover);
}
.btn-stop {
background: #ed8936;
color: white;
}
.btn-stop:hover:not(:disabled) {
background: #dd6b20;
}
.btn-reset {
background: #f56565;
color: white;
}
.btn-reset:hover:not(:disabled) {
background: #e53e3e;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.explanation-box {
background: var(--vp-c-bg-soft);
border-left: 4px solid var(--vp-c-brand-1);
border-radius: 8px;
padding: 16px;
}
.explanation-box p {
margin: 0 0 12px 0;
font-size: 14px;
line-height: 1.6;
color: var(--vp-c-text-1);
}
.explanation-box p:last-child {
margin-bottom: 0;
}
.explanation-box strong {
color: var(--vp-c-brand-1);
}
.explanation-box ul {
margin: 12px 0;
padding-left: 20px;
}
.explanation-box li {
margin-bottom: 8px;
font-size: 14px;
color: var(--vp-c-text-2);
}
.explanation-box .highlight {
padding: 12px;
background: rgba(62, 175, 124, 0.1);
border-radius: 6px;
font-weight: 500;
color: var(--vp-c-brand-1);
}
</style>
@@ -0,0 +1,752 @@
<script setup>
import { ref } from 'vue'
const phase = ref('mark')
const isAnimating = ref(false)
const currentStep = ref(0)
const objects = ref([
{ id: 1, name: 'obj1', color: '#68d391', marked: false, collected: false },
{ id: 2, name: 'obj2', color: '#4299e1', marked: false, collected: false },
{ id: 3, name: 'obj3', color: '#ed8936', marked: false, collected: false },
{ id: 4, name: 'obj4', color: '#f687b3', marked: false, collected: false },
{ id: 5, name: 'obj5', color: '#a3bffa', marked: false, collected: false },
{ id: 6, name: 'obj6', color: '#fc8181', marked: false, collected: false }
])
const references = ref([
{ from: 'root', to: 1, active: false },
{ from: 1, to: 2, active: false },
{ from: 1, to: 3, active: false },
{ from: 3, to: 4, active: false }
])
const phases = [
{ name: 'mark', label: '标记阶段', description: '从根对象开始,标记所有可达对象' },
{ name: 'sweep', label: '清除阶段', description: '回收未标记的对象' }
]
const steps = [
{ phase: 'mark', action: 'mark-root', description: '从根对象开始标记' },
{ phase: 'mark', action: 'mark-1', description: '标记 obj1 (根对象引用)' },
{ phase: 'mark', action: 'mark-2', description: '标记 obj2 (obj1 引用)' },
{ phase: 'mark', action: 'mark-3', description: '标记 obj3 (obj1 引用)' },
{ phase: 'mark', action: 'mark-4', description: '标记 obj4 (obj3 引用)' },
{ phase: 'sweep', action: 'collect-5', description: '回收 obj5 (未标记)' },
{ phase: 'sweep', action: 'collect-6', description: '回收 obj6 (未标记)' },
{ phase: 'done', action: 'finish', description: '垃圾回收完成' }
]
const reset = () => {
currentStep.value = 0
phase.value = 'mark'
isAnimating.value = false
objects.value.forEach(obj => {
obj.marked = false
obj.collected = false
})
references.value.forEach(ref => {
ref.active = false
})
}
const nextStep = () => {
if (currentStep.value >= steps.length) return
const step = steps[currentStep.value]
switch (step.action) {
case 'mark-root':
references.value[0].active = true
break
case 'mark-1':
objects.value[0].marked = true
references.value[1].active = true
references.value[2].active = true
break
case 'mark-2':
objects.value[1].marked = true
break
case 'mark-3':
objects.value[2].marked = true
references.value[3].active = true
break
case 'mark-4':
objects.value[3].marked = true
phase.value = 'sweep'
break
case 'collect-5':
objects.value[4].collected = true
break
case 'collect-6':
objects.value[5].collected = true
phase.value = 'done'
break
case 'finish':
phase.value = 'done'
break
}
currentStep.value++
}
const play = async () => {
if (isAnimating.value) return
isAnimating.value = true
reset()
while (currentStep.value < steps.length && isAnimating.value) {
nextStep()
await new Promise(resolve => setTimeout(resolve, 1200))
}
isAnimating.value = false
}
const stop = () => {
isAnimating.value = false
}
</script>
<template>
<div class="garbage-collection-demo">
<h3>垃圾回收机制</h3>
<!-- 阶段指示器 -->
<div class="phase-indicator">
<div class="phase-tabs">
<div
v-for="p in phases"
:key="p.name"
:class="{ 'active': phase === p.name }"
class="phase-tab"
>
<span class="phase-label">{{ p.label }}</span>
<span class="phase-description">{{ p.description }}</span>
</div>
</div>
</div>
<!-- 对象关系图 -->
<div class="graph-container">
<div class="graph-header">
<h4>对象引用关系</h4>
<div class="legend">
<div class="legend-item">
<span class="legend-color unmarked"></span>
<span>未标记</span>
</div>
<div class="legend-item">
<span class="legend-color marked"></span>
<span>已标记(可达)</span>
</div>
<div class="legend-item">
<span class="legend-color collected"></span>
<span>已回收</span>
</div>
</div>
</div>
<div class="object-graph">
<!-- 根对象 -->
<div class="root-object">
<div class="object-box root">
<div class="object-icon">🌳</div>
<div class="object-name">Root</div>
</div>
</div>
<!-- 对象节点 -->
<div class="objects-grid">
<div
v-for="obj in objects"
:key="obj.id"
class="object-node"
:class="{
'marked': obj.marked,
'collected': obj.collected
}"
>
<div class="object-box" :style="{ borderColor: obj.color }">
<div class="object-icon" :style="{ background: obj.color }">
{{ obj.collected ? '💀' : '📦' }}
</div>
<div class="object-name">{{ obj.name }}</div>
<div v-if="obj.marked" class="object-status"> 可达</div>
<div v-if="obj.collected" class="object-status collected"> 回收</div>
</div>
</div>
</div>
<!-- 引用连线 (用SVG绘制) -->
<svg class="connections" viewBox="0 0 600 400">
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#a0aec0" />
</marker>
</defs>
<!-- Root -> obj1 -->
<line
x1="80"
y1="200"
x2="180"
y2="100"
:class="{ 'active': references[0].active }"
marker-end="url(#arrowhead)"
/>
<!-- obj1 -> obj2 -->
<line
x1="220"
y1="120"
x2="220"
y2="180"
:class="{ 'active': references[1].active }"
marker-end="url(#arrowhead)"
/>
<!-- obj1 -> obj3 -->
<line
x1="260"
y1="120"
x2="380"
y2="120"
:class="{ 'active': references[2].active }"
marker-end="url(#arrowhead)"
/>
<!-- obj3 -> obj4 -->
<line
x1="400"
y1="140"
x2="400"
y2="200"
:class="{ 'active': references[3].active }"
marker-end="url(#arrowhead)"
/>
</svg>
</div>
</div>
<!-- 当前步骤说明 -->
<div class="step-description">
<div class="step-content">
<strong>当前操作:</strong>
<span v-if="currentStep < steps.length">
{{ steps[currentStep].description }}
</span>
<span v-else>
垃圾回收完成
</span>
</div>
</div>
<!-- 控制按钮 -->
<div class="controls">
<button @click="play" :disabled="isAnimating" class="btn-play">
{{ isAnimating ? '执行中...' : '▶ 自动演示' }}
</button>
<button @click="nextStep" :disabled="isAnimating || currentStep >= steps.length" class="btn-step">
单步执行
</button>
<button @click="stop" :disabled="!isAnimating" class="btn-stop">
停止
</button>
<button @click="reset" :disabled="isAnimating" class="btn-reset">
🔄 重置
</button>
</div>
<!-- 算法说明 -->
<div class="algorithm-box">
<h4>标记-清除算法 (Mark-and-Sweep)</h4>
<div class="algorithm-steps">
<div class="algorithm-step">
<span class="step-number">1</span>
<div class="step-content">
<strong>标记阶段</strong>
<p>从根对象(Root)开始,遍历所有可达对象,标记为"活动对象"</p>
</div>
</div>
<div class="algorithm-step">
<span class="step-number">2</span>
<div class="step-content">
<strong>清除阶段</strong>
<p>遍历整个堆内存,回收所有未被标记的对象</p>
</div>
</div>
<div class="algorithm-step">
<span class="step-number">3</span>
<div class="step-content">
<strong>重置标记</strong>
<p>清除所有标记位,为下一次垃圾回收做准备</p>
</div>
</div>
</div>
<div class="key-points">
<h5>核心要点</h5>
<ul>
<li><strong>根对象(Root):</strong> 全局变量栈上的变量等,总是被认为是可达的</li>
<li><strong>可达对象:</strong> 从根对象出发,通过引用链能访问到的对象</li>
<li><strong>垃圾对象:</strong> 无法从根对象访问到的对象,会被回收</li>
<li><strong>循环引用:</strong> 如果两个对象互相引用但都不可达,仍会被回收</li>
</ul>
</div>
</div>
<!-- 实际应用 -->
<div class="practical-tips">
<h4>实际应用技巧</h4>
<div class="tips-grid">
<div class="tip-card">
<div class="tip-icon">💡</div>
<div class="tip-content">
<strong>及时解除引用</strong>
<p>对象不再使用时,将其设为 null</p>
</div>
</div>
<div class="tip-card">
<div class="tip-icon">🔒</div>
<div class="tip-content">
<strong>避免意外的全局变量</strong>
<p>使用 const/let 代替 var</p>
</div>
</div>
<div class="tip-card">
<div class="tip-icon">🧹</div>
<div class="tip-content">
<strong>清理事件监听</strong>
<p>组件销毁时移除所有监听器</p>
</div>
</div>
<div class="tip-card">
<div class="tip-icon">📊</div>
<div class="tip-content">
<strong>定期检查内存</strong>
<p> DevTools Memory 面板监控</p>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.garbage-collection-demo {
border: 1px solid var(--vp-c-border);
border-radius: 12px;
padding: 24px;
margin: 24px 0;
background: var(--vp-c-bg);
}
h3 {
margin: 0 0 20px 0;
font-size: 18px;
font-weight: 600;
color: var(--vp-c-text-1);
}
h4 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-1);
}
h5 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
}
.phase-indicator {
margin-bottom: 20px;
}
.phase-tabs {
display: flex;
gap: 12px;
}
.phase-tab {
flex: 1;
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border: 2px solid transparent;
transition: all 0.3s ease;
}
.phase-tab.active {
border-color: var(--vp-c-brand-1);
background: rgba(62, 175, 124, 0.1);
}
.phase-label {
display: block;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.phase-description {
display: block;
font-size: 13px;
color: var(--vp-c-text-2);
}
.graph-container {
margin-bottom: 20px;
}
.graph-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.legend {
display: flex;
gap: 16px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--vp-c-text-2);
}
.legend-color {
width: 20px;
height: 20px;
border-radius: 4px;
border: 2px solid;
}
.legend-color.unmarked {
background: var(--vp-c-bg);
border-color: var(--vp-c-border);
}
.legend-color.marked {
background: rgba(104, 217, 145, 0.2);
border-color: #68d391;
}
.legend-color.collected {
background: rgba(245, 101, 101, 0.2);
border-color: #f56565;
}
.object-graph {
position: relative;
height: 400px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
}
.root-object {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
}
.objects-grid {
position: absolute;
left: 150px;
top: 20px;
right: 20px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.object-node {
transition: all 0.3s ease;
}
.object-box {
padding: 16px;
background: var(--vp-c-bg);
border: 3px solid var(--vp-c-border);
border-radius: 8px;
text-align: center;
transition: all 0.3s ease;
}
.object-box.root {
border-color: var(--vp-c-brand-1);
background: rgba(62, 175, 124, 0.1);
}
.object-node.marked .object-box {
border-color: #68d391;
background: rgba(104, 217, 145, 0.1);
}
.object-node.collected .object-box {
border-color: #f56565;
background: rgba(245, 101, 101, 0.1);
opacity: 0.5;
}
.object-icon {
width: 48px;
height: 48px;
margin: 0 auto 12px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
background: var(--vp-c-bg-soft);
}
.object-name {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 8px;
}
.object-status {
font-size: 12px;
font-weight: 600;
}
.object-status:not(.collected) {
color: #68d391;
}
.object-status.collected {
color: #f56565;
}
.connections {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.connections line {
stroke: #a0aec0;
stroke-width: 2;
transition: all 0.3s ease;
}
.connections line.active {
stroke: var(--vp-c-brand-1);
stroke-width: 3;
animation: pulse 1s ease infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.step-description {
padding: 16px;
background: var(--vp-c-bg-soft);
border-left: 4px solid var(--vp-c-brand-1);
border-radius: 8px;
margin-bottom: 20px;
}
.step-content {
font-size: 14px;
color: var(--vp-c-text-1);
}
.step-content strong {
color: var(--vp-c-brand-1);
}
.controls {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
button:active {
transform: scale(0.95);
}
.btn-play {
background: var(--vp-c-brand-1);
color: white;
}
.btn-play:hover:not(:disabled) {
background: var(--vp-c-brand-2);
}
.btn-step {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.btn-step:hover:not(:disabled) {
background: var(--vp-c-bg-soft-hover);
}
.btn-stop {
background: #ed8936;
color: white;
}
.btn-stop:hover:not(:disabled) {
background: #dd6b20;
}
.btn-reset {
background: #f56565;
color: white;
}
.btn-reset:hover:not(:disabled) {
background: #e53e3e;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.algorithm-box {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.algorithm-steps {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 20px;
}
.algorithm-step {
display: flex;
gap: 16px;
}
.step-number {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--vp-c-brand-1);
color: white;
font-size: 16px;
font-weight: 700;
flex-shrink: 0;
}
.algorithm-step .step-content {
flex: 1;
}
.algorithm-step strong {
display: block;
margin-bottom: 4px;
font-size: 14px;
color: var(--vp-c-text-1);
}
.algorithm-step p {
margin: 0;
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.key-points {
padding: 16px;
background: var(--vp-c-bg);
border-radius: 6px;
}
.key-points ul {
margin: 0;
padding-left: 20px;
}
.key-points li {
margin-bottom: 8px;
font-size: 14px;
line-height: 1.6;
color: var(--vp-c-text-2);
}
.key-points strong {
color: var(--vp-c-text-1);
}
.practical-tips {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
}
.tips-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.tip-card {
display: flex;
gap: 12px;
padding: 16px;
background: var(--vp-c-bg);
border-radius: 6px;
}
.tip-icon {
font-size: 24px;
flex-shrink: 0;
}
.tip-content {
flex: 1;
}
.tip-content strong {
display: block;
margin-bottom: 4px;
font-size: 14px;
color: var(--vp-c-text-1);
}
.tip-content p {
margin: 0;
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.4;
}
</style>
@@ -0,0 +1,682 @@
<script setup>
import { ref } from 'vue'
const activeScenario = ref('global-vars')
const scenarios = [
{ value: 'global-vars', label: '全局变量', icon: '🌍' },
{ value: 'event-listeners', label: '事件监听', icon: '🎯' },
{ value: 'closures', label: '闭包引用', icon: '🔒' }
]
// 全局变量场景
const globalMemory = ref([])
// 事件监听场景
const eventListeners = ref([])
const eventCount = ref(0)
// 闭包场景
const closureItems = ref([])
const memoryUsage = ref(0)
const maxMemory = ref(100)
const addGlobalVariable = () => {
const largeData = new Array(10000).fill(`数据 ${globalMemory.value.length}`)
globalMemory.value.push({
id: Date.now(),
data: largeData,
timestamp: new Date().toLocaleTimeString()
})
updateMemory()
}
const clearGlobalVariables = () => {
globalMemory.value = []
updateMemory()
}
// 事件监听场景
const addEventListener = () => {
const handler = () => console.log('事件监听器')
eventListeners.value.push({
id: Date.now(),
handler: handler,
active: true
})
eventCount.value++
updateMemory()
}
const removeAllListeners = () => {
eventListeners.value = []
eventCount.value = 0
updateMemory()
}
// 闭包场景
const createClosure = () => {
const largeData = new Array(10000).fill('闭包数据')
const closure = () => {
return largeData.length
}
closureItems.value.push({
id: Date.now(),
closure: closure,
data: largeData,
timestamp: new Date().toLocaleTimeString()
})
updateMemory()
}
const clearClosures = () => {
closureItems.value = []
updateMemory()
}
const updateMemory = () => {
const total = globalMemory.value.length + eventListeners.value.length + closureItems.value.length
memoryUsage.value = Math.min(total, maxMemory.value)
}
const resetAll = () => {
globalMemory.value = []
eventListeners.value = []
eventCount.value = 0
closureItems.value = []
memoryUsage.value = 0
}
</script>
<template>
<div class="memory-leak-demo">
<h3>内存泄漏演示</h3>
<!-- 场景选择 -->
<div class="scenario-tabs">
<button
v-for="scenario in scenarios"
:key="scenario.value"
@click="activeScenario = scenario.value"
:class="{ 'active': activeScenario === scenario.value }"
class="scenario-tab"
>
<span class="tab-icon">{{ scenario.icon }}</span>
<span class="tab-label">{{ scenario.label }}</span>
</button>
</div>
<!-- 内存使用情况 -->
<div class="memory-monitor">
<div class="monitor-header">
<span class="monitor-title">内存使用情况</span>
<span class="monitor-value">{{ memoryUsage }}%</span>
</div>
<div class="memory-bar">
<div
class="memory-fill"
:class="{ 'warning': memoryUsage > 70, 'danger': memoryUsage > 90 }"
:style="{ width: `${memoryUsage}%` }"
>
<span v-if="memoryUsage > 10" class="memory-text">{{ memoryUsage }}%</span>
</div>
</div>
<div v-if="memoryUsage > 90" class="memory-alert">
内存占用过高!可能导致页面卡顿或崩溃
</div>
</div>
<!-- 场景内容 -->
<div class="scenario-content">
<!-- 全局变量场景 -->
<div v-if="activeScenario === 'global-vars'" class="scenario-panel">
<h4>全局变量泄漏</h4>
<div class="scenario-description">
<p><strong>问题:</strong>全局变量不会被垃圾回收,会一直占用内存</p>
<p><strong>示例:</strong>不断往全局数组添加数据,从不清理</p>
</div>
<div class="action-buttons">
<button @click="addGlobalVariable" class="btn-add">
添加全局变量
</button>
<button @click="clearGlobalVariables" class="btn-clear">
🗑 清空全局变量
</button>
</div>
<div class="data-preview">
<div class="preview-header">
<span>全局变量 ({{ globalMemory.length }} )</span>
</div>
<div class="preview-list">
<div
v-for="item in globalMemory.slice(-5)"
:key="item.id"
class="preview-item"
>
<span class="item-id">ID: {{ item.id }}</span>
<span class="item-time">{{ item.timestamp }}</span>
<span class="item-size">{{ item.data.length }} 项数据</span>
</div>
<div v-if="globalMemory.length === 0" class="empty-state">
暂无全局变量
</div>
<div v-if="globalMemory.length > 5" class="more-items">
... 还有 {{ globalMemory.length - 5 }}
</div>
</div>
</div>
<div class="code-example">
<h5> 错误做法</h5>
<pre><code>// 全局变量不会被回收
globalCache = []
function addItem() {
globalCache.push(largeData)
}</code></pre>
</div>
</div>
<!-- 事件监听场景 -->
<div v-if="activeScenario === 'event-listeners'" class="scenario-panel">
<h4>事件监听器泄漏</h4>
<div class="scenario-description">
<p><strong>问题:</strong>事件监听器没有被移除,持续占用内存</p>
<p><strong>示例:</strong>动态创建元素并添加监听,但从不移除</p>
</div>
<div class="action-buttons">
<button @click="addEventListener" class="btn-add">
添加事件监听
</button>
<button @click="removeAllListeners" class="btn-clear">
🗑 移除所有监听
</button>
</div>
<div class="data-preview">
<div class="preview-header">
<span>活跃监听器: {{ eventCount }} </span>
</div>
<div class="listener-list">
<div
v-for="listener in eventListeners.slice(-5)"
:key="listener.id"
class="listener-item"
>
<div class="listener-icon">🎯</div>
<div class="listener-info">
<span class="listener-id">监听器 #{{ listener.id }}</span>
<span class="listener-status">活跃中</span>
</div>
</div>
<div v-if="eventListeners.length === 0" class="empty-state">
暂无事件监听器
</div>
<div v-if="eventListeners.length > 5" class="more-items">
... 还有 {{ eventListeners.length - 5 }} 个监听器
</div>
</div>
</div>
<div class="code-example">
<h5> 错误做法</h5>
<pre><code>// 监听器没有被移除
button.addEventListener('click', handler)
// 元素删除时监听器还在!</code></pre>
<h5> 正确做法</h5>
<pre><code>// 保存监听器引用
const handler = () => { ... }
button.addEventListener('click', handler)
// 不需要时移除
button.removeEventListener('click', handler)</code></pre>
</div>
</div>
<!-- 闭包场景 -->
<div v-if="activeScenario === 'closures'" class="scenario-panel">
<h4>闭包引用泄漏</h4>
<div class="scenario-description">
<p><strong>问题:</strong>闭包持有大对象引用,导致对象无法被回收</p>
<p><strong>示例:</strong>闭包函数一直引用大数组</p>
</div>
<div class="action-buttons">
<button @click="createClosure" class="btn-add">
创建闭包
</button>
<button @click="clearClosures" class="btn-clear">
🗑 清空闭包
</button>
</div>
<div class="data-preview">
<div class="preview-header">
<span>活跃闭包: {{ closureItems.length }} </span>
</div>
<div class="closure-list">
<div
v-for="item in closureItems.slice(-5)"
:key="item.id"
class="closure-item"
>
<div class="closure-icon">🔒</div>
<div class="closure-info">
<span class="closure-id">闭包 #{{ item.id }}</span>
<span class="closure-time">{{ item.timestamp }}</span>
<span class="closure-size">持有 {{ item.data.length }} 项数据</span>
</div>
</div>
<div v-if="closureItems.length === 0" class="empty-state">
暂无闭包
</div>
<div v-if="closureItems.length > 5" class="more-items">
... 还有 {{ closureItems.length - 5 }} 个闭包
</div>
</div>
</div>
<div class="code-example">
<h5> 错误做法</h5>
<pre><code>// 闭包持有大对象引用
function createHandler() {
const largeData = new Array(1000000)
return function() {
// largeData 一直被引用,不会被回收
console.log('处理中')
}
}
const handler = createHandler()</code></pre>
<h5> 正确做法</h5>
<pre><code>// 使用后释放引用
let handler = createHandler()
handler() // 使用
handler = null // 释放引用</code></pre>
</div>
</div>
</div>
<!-- 重置按钮 -->
<div class="global-actions">
<button @click="resetAll" class="btn-reset">
🔄 重置所有场景
</button>
</div>
<!-- 总结 -->
<div class="summary-box">
<h4>如何避免内存泄漏</h4>
<ul>
<li><strong>避免全局变量:</strong> 使用 const/let 代替 var,尽量使用局部变量</li>
<li><strong>及时清理监听器:</strong> 组件销毁时移除所有事件监听</li>
<li><strong>释放闭包引用:</strong> 不需要时将闭包变量设为 null</li>
<li><strong>使用 WeakMap/WeakSet:</strong> 自动清理不再被引用的对象</li>
<li><strong>定期检查:</strong> DevTools Memory 面板检查内存泄漏</li>
</ul>
</div>
</div>
</template>
<style scoped>
.memory-leak-demo {
border: 1px solid var(--vp-c-border);
border-radius: 12px;
padding: 24px;
margin: 24px 0;
background: var(--vp-c-bg);
}
h3 {
margin: 0 0 20px 0;
font-size: 18px;
font-weight: 600;
color: var(--vp-c-text-1);
}
h4 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-1);
}
h5 {
margin: 12px 0 8px 0;
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-2);
}
.scenario-tabs {
display: flex;
gap: 12px;
margin-bottom: 20px;
border-bottom: 2px solid var(--vp-c-border);
}
.scenario-tab {
padding: 12px 24px;
border: none;
background: transparent;
color: var(--vp-c-text-2);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
}
.scenario-tab:hover {
color: var(--vp-c-brand-1);
}
.scenario-tab.active {
color: var(--vp-c-brand-1);
border-bottom-color: var(--vp-c-brand-1);
}
.tab-icon {
font-size: 18px;
margin-right: 8px;
}
.tab-label {
font-size: 14px;
}
.memory-monitor {
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
margin-bottom: 20px;
}
.monitor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.monitor-title {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.monitor-value {
font-size: 18px;
font-weight: 700;
color: var(--vp-c-brand-1);
}
.memory-bar {
height: 32px;
background: var(--vp-c-bg);
border-radius: 6px;
overflow: hidden;
position: relative;
}
.memory-fill {
height: 100%;
background: var(--vp-c-brand-1);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.memory-fill.warning {
background: #ed8936;
}
.memory-fill.danger {
background: #f56565;
}
.memory-text {
color: white;
font-size: 12px;
font-weight: 600;
}
.memory-alert {
margin-top: 12px;
padding: 12px;
background: rgba(245, 101, 101, 0.1);
border-left: 4px solid #f56565;
border-radius: 6px;
font-size: 13px;
color: #f56565;
font-weight: 500;
}
.scenario-content {
margin-bottom: 20px;
}
.scenario-panel {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.scenario-description {
margin-bottom: 16px;
padding: 12px;
background: var(--vp-c-bg);
border-radius: 6px;
}
.scenario-description p {
margin: 0 0 8px 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.scenario-description p:last-child {
margin-bottom: 0;
}
.scenario-description strong {
color: var(--vp-c-text-1);
}
.action-buttons {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
button {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
button:active {
transform: scale(0.95);
}
.btn-add {
background: var(--vp-c-brand-1);
color: white;
}
.btn-add:hover {
background: var(--vp-c-brand-2);
}
.btn-clear {
background: #ed8936;
color: white;
}
.btn-clear:hover {
background: #dd6b20;
}
.btn-reset {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
border: 2px solid var(--vp-c-border);
}
.btn-reset:hover {
background: var(--vp-c-bg-soft-hover);
border-color: var(--vp-c-brand-1);
}
.data-preview {
margin-bottom: 20px;
}
.preview-header {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.preview-list,
.listener-list,
.closure-list {
background: var(--vp-c-bg);
border-radius: 6px;
padding: 12px;
min-height: 150px;
}
.preview-item,
.listener-item,
.closure-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px;
margin-bottom: 8px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 13px;
}
.preview-item {
justify-content: space-between;
}
.listener-icon,
.closure-icon {
font-size: 20px;
}
.listener-info,
.closure-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.listener-id,
.closure-id {
font-weight: 600;
color: var(--vp-c-text-1);
}
.listener-status {
font-size: 12px;
color: #68d391;
}
.item-id,
.item-time,
.item-size,
.closure-time,
.closure-size {
font-size: 12px;
color: var(--vp-c-text-2);
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--vp-c-text-3);
font-size: 14px;
}
.more-items {
text-align: center;
padding: 8px;
color: var(--vp-c-text-3);
font-size: 12px;
font-style: italic;
}
.code-example {
background: #1e1e1e;
border-radius: 6px;
padding: 16px;
}
.code-example pre {
margin: 0;
}
.code-example code {
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #d4d4d4;
}
.global-actions {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.summary-box {
background: var(--vp-c-bg-soft);
border-left: 4px solid var(--vp-c-brand-1);
border-radius: 8px;
padding: 16px;
}
.summary-box h4 {
margin: 0 0 12px 0;
font-size: 14px;
color: var(--vp-c-brand-1);
}
.summary-box ul {
margin: 0;
padding-left: 20px;
}
.summary-box li {
margin-bottom: 8px;
font-size: 14px;
line-height: 1.6;
color: var(--vp-c-text-2);
}
.summary-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -0,0 +1,487 @@
<script setup>
import { ref } from 'vue'
const activeTab = ref('browser')
const tabs = [
{ value: 'browser', label: '浏览器环境', icon: '🌐' },
{ value: 'nodejs', label: 'Node.js 环境', icon: '🟢' }
]
const browserApis = [
{ name: 'window', description: '浏览器全局对象', example: 'window.location.href' },
{ name: 'document', description: 'DOM 操作', example: 'document.querySelector("h1")' },
{ name: 'localStorage', description: '本地存储', example: 'localStorage.setItem("key", "value")' },
{ name: 'fetch', description: '网络请求', example: 'fetch("/api/data")' },
{ name: 'setTimeout', description: '定时器', example: 'setTimeout(() => {}, 1000)' }
]
const nodeApis = [
{ name: 'global', description: 'Node.js 全局对象', example: 'global.process' },
{ name: 'process', description: '进程信息', example: 'process.env.NODE_ENV' },
{ name: 'fs', description: '文件系统', example: 'fs.readFile("./data.txt")' },
{ name: 'http', description: 'HTTP 服务器', example: 'http.createServer((req, res) => {})' },
{ name: 'path', description: '路径处理', example: 'path.join("/a", "b")' }
]
const tryCode = ref('console.log(typeof window)')
const browserResult = ref('')
const nodeResult = ref('')
const runInBrowser = () => {
try {
browserResult.value = eval(tryCode.value)
} catch (e) {
browserResult.value = e.message
}
nodeResult.value = '在 Node.js 中运行...'
}
const runInNode = () => {
nodeResult.value = '在浏览器中无法直接运行 Node.js 代码'
try {
browserResult.value = eval(tryCode.value)
} catch (e) {
browserResult.value = e.message
}
}
const reset = () => {
browserResult.value = ''
nodeResult.value = ''
tryCode.value = 'console.log(typeof window)'
}
</script>
<template>
<div class="runtime-environment-demo">
<h3>运行时环境对比</h3>
<div class="tab-container">
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.value"
@click="activeTab = tab.value"
:class="{ 'active': activeTab === tab.value }"
class="tab-btn"
>
<span class="tab-icon">{{ tab.icon }}</span>
<span class="tab-label">{{ tab.label }}</span>
</button>
</div>
<div class="tab-content">
<!-- 浏览器环境 -->
<div v-if="activeTab === 'browser'" class="environment-content">
<h4>浏览器环境</h4>
<div class="api-grid">
<div
v-for="api in browserApis"
:key="api.name"
class="api-card"
>
<div class="api-name">{{ api.name }}</div>
<div class="api-description">{{ api.description }}</div>
<div class="api-example">{{ api.example }}</div>
</div>
</div>
<div class="environment-note">
<strong>特点:</strong>
<ul>
<li> DOM BOM API,可以操作网页</li>
<li> Web Storage (localStorage, sessionStorage)</li>
<li> fetch XMLHttpRequest 进行网络请求</li>
<li> 没有文件系统访问权限</li>
<li> 不能直接创建 HTTP 服务器</li>
</ul>
</div>
</div>
<!-- Node.js 环境 -->
<div v-if="activeTab === 'nodejs'" class="environment-content">
<h4>Node.js 环境</h4>
<div class="api-grid">
<div
v-for="api in nodeApis"
:key="api.name"
class="api-card"
>
<div class="api-name">{{ api.name }}</div>
<div class="api-description">{{ api.description }}</div>
<div class="api-example">{{ api.example }}</div>
</div>
</div>
<div class="environment-note">
<strong>特点:</strong>
<ul>
<li> 有文件系统访问权限</li>
<li> 可以创建 HTTP 服务器</li>
<li> 可以操作进程和系统资源</li>
<li> 没有 DOM BOM</li>
<li> 不能直接操作网页元素</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 代码对比演示 -->
<div class="code-comparison-section">
<h4>代码演示:不同环境的差异</h4>
<div class="code-input">
<label>试试运行这段代码:</label>
<input
v-model="tryCode"
type="text"
placeholder="输入 JavaScript 代码"
class="code-input-field"
>
</div>
<div class="result-grid">
<div class="result-card">
<div class="result-header">
<span class="result-icon">🌐</span>
<span class="result-title">浏览器结果</span>
</div>
<div class="result-content">
{{ browserResult || '点击"在浏览器运行"查看结果' }}
</div>
<button @click="runInBrowser" class="run-btn">
在浏览器运行
</button>
</div>
<div class="result-card">
<div class="result-header">
<span class="result-icon">🟢</span>
<span class="result-title">Node.js 结果</span>
</div>
<div class="result-content">
{{ nodeResult || '需要在 Node.js 环境中运行' }}
</div>
<button @click="runInNode" class="run-btn" disabled>
需要终端运行
</button>
</div>
</div>
<button @click="reset" class="reset-btn">
重置
</button>
</div>
<!-- 总结 -->
<div class="summary-box">
<p><strong>核心区别:</strong></p>
<p>浏览器运行时专注于用户界面和网页交互,提供 DOMBOMfetch 等前端专用 API</p>
<p>Node.js 运行时专注于服务器端开发,提供文件系统HTTP 服务器进程管理等后端专用 API</p>
<p class="highlight">同样的 JavaScript 语法,但能用的 API 完全不同这就是"环境判断"的重要性</p>
</div>
</div>
</template>
<style scoped>
.runtime-environment-demo {
border: 1px solid var(--vp-c-border);
border-radius: 12px;
padding: 24px;
margin: 24px 0;
background: var(--vp-c-bg);
}
h3 {
margin: 0 0 20px 0;
font-size: 18px;
font-weight: 600;
color: var(--vp-c-text-1);
}
h4 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.tab-container {
margin-bottom: 24px;
}
.tabs {
display: flex;
gap: 12px;
margin-bottom: 20px;
border-bottom: 2px solid var(--vp-c-border);
}
.tab-btn {
padding: 12px 24px;
border: none;
background: transparent;
color: var(--vp-c-text-2);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
}
.tab-btn:hover {
color: var(--vp-c-brand-1);
}
.tab-btn.active {
color: var(--vp-c-brand-1);
border-bottom-color: var(--vp-c-brand-1);
}
.tab-icon {
font-size: 18px;
margin-right: 8px;
}
.tab-label {
font-size: 14px;
}
.tab-content {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.api-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.api-card {
padding: 16px;
border: 2px solid var(--vp-c-border);
border-radius: 8px;
background: var(--vp-c-bg);
transition: all 0.2s ease;
}
.api-card:hover {
border-color: var(--vp-c-brand-1);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.api-name {
font-size: 16px;
font-weight: 600;
color: var(--vp-c-brand-1);
margin-bottom: 8px;
font-family: 'Courier New', monospace;
}
.api-description {
font-size: 13px;
color: var(--vp-c-text-2);
margin-bottom: 8px;
}
.api-example {
font-size: 12px;
color: var(--vp-c-text-3);
font-family: 'Courier New', monospace;
padding: 8px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
}
.environment-note {
padding: 16px;
background: rgba(62, 175, 124, 0.1);
border-left: 4px solid var(--vp-c-brand-1);
border-radius: 8px;
}
.environment-note strong {
display: block;
margin-bottom: 8px;
color: var(--vp-c-text-1);
}
.environment-note ul {
list-style: none;
padding: 0;
margin: 0;
}
.environment-note li {
padding: 4px 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.code-comparison-section {
border-top: 2px solid var(--vp-c-border);
padding-top: 24px;
}
.code-input {
margin-bottom: 20px;
}
.code-input label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.code-input-field {
width: 100%;
padding: 12px;
border: 2px solid var(--vp-c-border);
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 14px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
transition: border-color 0.2s ease;
}
.code-input-field:focus {
outline: none;
border-color: var(--vp-c-brand-1);
}
.result-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.result-grid {
grid-template-columns: 1fr;
}
}
.result-card {
border: 2px solid var(--vp-c-border);
border-radius: 8px;
padding: 16px;
background: var(--vp-c-bg);
}
.result-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--vp-c-border);
}
.result-icon {
font-size: 20px;
}
.result-title {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.result-content {
min-height: 60px;
padding: 12px;
margin-bottom: 12px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 13px;
color: var(--vp-c-text-1);
word-break: break-all;
}
.run-btn {
width: 100%;
padding: 10px;
border: none;
border-radius: 6px;
background: var(--vp-c-brand-1);
color: white;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.run-btn:hover:not(:disabled) {
background: var(--vp-c-brand-2);
transform: translateY(-1px);
}
.run-btn:disabled {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-3);
cursor: not-allowed;
}
.reset-btn {
padding: 10px 24px;
border: 2px solid var(--vp-c-border);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.reset-btn:hover {
background: var(--vp-c-bg-soft);
border-color: var(--vp-c-brand-1);
}
.summary-box {
background: var(--vp-c-bg-soft);
border-left: 4px solid var(--vp-c-brand-1);
border-radius: 8px;
padding: 16px;
margin-top: 24px;
}
.summary-box p {
margin: 0 0 12px 0;
font-size: 14px;
line-height: 1.6;
color: var(--vp-c-text-1);
}
.summary-box p:last-child {
margin-bottom: 0;
}
.summary-box strong {
color: var(--vp-c-brand-1);
}
.summary-box .highlight {
padding: 12px;
background: rgba(62, 175, 124, 0.1);
border-radius: 6px;
font-weight: 500;
color: var(--vp-c-brand-1);
}
</style>
@@ -0,0 +1,652 @@
<script setup>
import { ref } from 'vue'
const isAnimating = ref(false)
const currentStep = ref(0)
const syncCode = ref([
{ id: 1, code: 'console.log("1")', type: 'sync', output: '1' },
{ id: 2, code: 'setTimeout(() => console.log("2"), 0)', type: 'macro', output: '2' },
{ id: 3, code: 'Promise.resolve().then(() => console.log("3"))', type: 'micro', output: '3' },
{ id: 4, code: 'console.log("4")', type: 'sync', output: '4' },
{ id: 5, code: 'setTimeout(() => console.log("5"), 0)', type: 'macro', output: '5' }
])
const microTaskQueue = ref([])
const macroTaskQueue = ref([])
const outputLog = ref([])
const executionSteps = [
{ description: '执行 console.log("1")', action: 'execute', output: '1', source: '同步' },
{ description: '遇到 setTimeout,将回调加入宏任务队列', action: 'add-macro', task: 'console.log("2")' },
{ description: '遇到 Promise.then,将回调加入微任务队列', action: 'add-micro', task: 'console.log("3")' },
{ description: '执行 console.log("4")', action: 'execute', output: '4', source: '同步' },
{ description: '遇到 setTimeout,将回调加入宏任务队列', action: 'add-macro', task: 'console.log("5")' },
{ description: '同步代码执行完毕,检查微任务队列', action: 'check-micro' },
{ description: '执行微任务: console.log("3")', action: 'execute-micro', output: '3', source: '微任务' },
{ description: '微任务队列为空,检查宏任务队列', action: 'check-macro' },
{ description: '执行宏任务: console.log("2")', action: 'execute-macro', output: '2', source: '宏任务' },
{ description: '检查微任务队列(空)', action: 'check-micro' },
{ description: '执行宏任务: console.log("5")', action: 'execute-macro', output: '5', source: '宏任务' },
{ description: '所有任务执行完毕', action: 'done' }
]
const reset = () => {
currentStep.value = 0
microTaskQueue.value = []
macroTaskQueue.value = []
outputLog.value = []
isAnimating.value = false
}
const nextStep = () => {
if (currentStep.value >= executionSteps.length) return
const step = executionSteps[currentStep.value]
switch (step.action) {
case 'execute':
outputLog.value.push({ output: step.output, source: step.source })
break
case 'add-macro':
macroTaskQueue.value.push({ code: step.task, status: 'pending' })
break
case 'add-micro':
microTaskQueue.value.push({ code: step.task, status: 'pending' })
break
case 'check-micro':
if (microTaskQueue.value.length > 0) {
microTaskQueue.value[0].status = 'ready'
}
break
case 'execute-micro':
if (microTaskQueue.value.length > 0) {
outputLog.value.push({ output: step.output, source: step.source })
microTaskQueue.value.shift()
}
break
case 'check-macro':
if (macroTaskQueue.value.length > 0) {
macroTaskQueue.value[0].status = 'ready'
}
break
case 'execute-macro':
if (macroTaskQueue.value.length > 0) {
outputLog.value.push({ output: step.output, source: step.source })
macroTaskQueue.value.shift()
}
break
}
currentStep.value++
}
const play = async () => {
if (isAnimating.value) return
isAnimating.value = true
reset()
while (currentStep.value < executionSteps.length && isAnimating.value) {
nextStep()
await new Promise(resolve => setTimeout(resolve, 1000))
}
isAnimating.value = false
}
const stop = () => {
isAnimating.value = false
}
</script>
<template>
<div class="task-queue-demo">
<h3>任务队列:宏任务 vs 微任务</h3>
<!-- 代码展示 -->
<div class="code-section">
<h4>代码示例</h4>
<div class="code-display">
<div
v-for="(item, index) in syncCode"
:key="item.id"
class="code-item"
:class="{
'current': currentStep === index,
'executed': currentStep > index && index < 4
}"
>
<span class="item-number">{{ item.id }}</span>
<span class="item-code" :class="`type-${item.type}`">{{ item.code }}</span>
<span v-if="item.type === 'sync'" class="item-tag">同步</span>
<span v-else-if="item.type === 'micro'" class="item-tag micro">微任务</span>
<span v-else-if="item.type === 'macro'" class="item-tag macro">宏任务</span>
</div>
</div>
</div>
<!-- 执行过程可视化 -->
<div class="visualization">
<!-- 调用栈 -->
<div class="stack-panel">
<h4>调用栈 (正在执行)</h4>
<div class="stack-content">
<div v-if="currentStep < executionSteps.length" class="current-action">
{{ executionSteps[currentStep]?.description }}
</div>
<div v-else class="current-action done">
执行完成
</div>
</div>
</div>
<!-- 微任务队列 -->
<div class="queue-panel micro">
<h4>
微任务队列
<span class="badge">Microtask</span>
</h4>
<div class="queue-content">
<transition-group name="task-item">
<div
v-for="(task, index) in microTaskQueue"
:key="`micro-${index}`"
class="task-item micro"
:class="{ 'ready': task.status === 'ready' }"
>
<div class="task-code">{{ task.code }}</div>
<div v-if="task.status === 'ready'" class="task-status"> 就绪</div>
<div v-else class="task-status"> 等待</div>
</div>
</transition-group>
<div v-if="microTaskQueue.length === 0" class="empty-queue">
队列为空
</div>
</div>
</div>
<!-- 宏任务队列 -->
<div class="queue-panel macro">
<h4>
宏任务队列
<span class="badge">Macrotask</span>
</h4>
<div class="queue-content">
<transition-group name="task-item">
<div
v-for="(task, index) in macroTaskQueue"
:key="`macro-${index}`"
class="task-item macro"
:class="{ 'ready': task.status === 'ready' }"
>
<div class="task-code">{{ task.code }}</div>
<div v-if="task.status === 'ready'" class="task-status"> 就绪</div>
<div v-else class="task-status"> 等待</div>
</div>
</transition-group>
<div v-if="macroTaskQueue.length === 0" class="empty-queue">
队列为空
</div>
</div>
</div>
</div>
<!-- 输出日志 -->
<div class="output-section">
<h4>输出日志 (执行顺序)</h4>
<div class="output-log">
<div v-if="outputLog.length === 0" class="empty-log">
等待输出...
</div>
<transition-group name="output">
<div
v-for="(log, index) in outputLog"
:key="`log-${index}`"
class="log-entry"
>
<span class="log-output">{{ log.output }}</span>
<span class="log-source">({{ log.source }})</span>
</div>
</transition-group>
</div>
</div>
<!-- 控制按钮 -->
<div class="controls">
<button @click="play" :disabled="isAnimating" class="btn-play">
{{ isAnimating ? '执行中...' : '▶ 自动演示' }}
</button>
<button @click="nextStep" :disabled="isAnimating || currentStep >= executionSteps.length" class="btn-step">
单步执行
</button>
<button @click="stop" :disabled="!isAnimating" class="btn-stop">
停止
</button>
<button @click="reset" :disabled="isAnimating" class="btn-reset">
🔄 重置
</button>
</div>
<!-- 执行规则 -->
<div class="rules-box">
<h4>执行顺序规则</h4>
<div class="rule-list">
<div class="rule-item">
<span class="rule-number">1</span>
<span class="rule-text">执行所有同步代码</span>
</div>
<div class="rule-item">
<span class="rule-number">2</span>
<span class="rule-text">执行微任务队列中的所有任务</span>
</div>
<div class="rule-item">
<span class="rule-number">3</span>
<span class="rule-text">执行一个宏任务</span>
</div>
<div class="rule-item">
<span class="rule-number">4</span>
<span class="rule-text">重复步骤 2-3</span>
</div>
</div>
<p class="highlight">
<strong>核心要点:</strong> 微任务优先级高于宏任务每次执行完一个宏任务后,都会检查并执行所有微任务,然后再执行下一个宏任务
</p>
</div>
</div>
</template>
<style scoped>
.task-queue-demo {
border: 1px solid var(--vp-c-border);
border-radius: 12px;
padding: 24px;
margin: 24px 0;
background: var(--vp-c-bg);
}
h3 {
margin: 0 0 20px 0;
font-size: 18px;
font-weight: 600;
color: var(--vp-c-text-1);
}
h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
}
.code-section {
margin-bottom: 20px;
}
.code-display {
background: #1e1e1e;
border-radius: 8px;
padding: 16px;
font-family: 'Courier New', monospace;
}
.code-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px;
border-radius: 4px;
transition: all 0.3s ease;
}
.code-item.current {
background: rgba(62, 175, 124, 0.2);
border-left: 3px solid var(--vp-c-brand-1);
}
.code-item.executed {
opacity: 0.5;
}
.item-number {
color: #858585;
font-size: 12px;
min-width: 20px;
}
.item-code {
flex: 1;
color: #d4d4d4;
font-size: 13px;
}
.item-code.type-micro {
color: #68d391;
}
.item-code.type-macro {
color: #f687b3;
}
.item-tag {
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
}
.item-tag.micro {
background: rgba(104, 217, 145, 0.2);
color: #68d391;
}
.item-tag.macro {
background: rgba(246, 135, 179, 0.2);
color: #f687b3;
}
.visualization {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
margin-bottom: 20px;
}
@media (max-width: 1024px) {
.visualization {
grid-template-columns: 1fr;
}
}
.stack-panel,
.queue-panel {
border: 2px solid var(--vp-c-border);
border-radius: 8px;
padding: 16px;
background: var(--vp-c-bg-soft);
min-height: 250px;
}
.queue-panel.micro {
border-color: #68d391;
}
.queue-panel.macro {
border-color: #f687b3;
}
.badge {
margin-left: 8px;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
}
.stack-content,
.queue-content {
min-height: 200px;
}
.current-action {
padding: 16px;
background: var(--vp-c-bg);
border-radius: 8px;
border-left: 4px solid var(--vp-c-brand-1);
font-size: 14px;
color: var(--vp-c-text-1);
line-height: 1.6;
}
.current-action.done {
border-color: #48bb78;
text-align: center;
font-weight: 600;
}
.task-item {
padding: 12px;
margin-bottom: 8px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-border);
border-radius: 6px;
transition: all 0.3s ease;
}
.task-item.micro {
border-color: #68d391;
}
.task-item.macro {
border-color: #f687b3;
}
.task-item.ready {
animation: pulse 1s ease infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(104, 217, 145, 0.4); }
50% { box-shadow: 0 0 0 6px rgba(104, 217, 145, 0); }
}
.task-code {
font-family: 'Courier New', monospace;
font-size: 12px;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.task-status {
font-size: 11px;
font-weight: 600;
color: var(--vp-c-text-2);
}
.task-item-enter-active,
.task-item-leave-active {
transition: all 0.3s ease;
}
.task-item-enter-from {
opacity: 0;
transform: translateX(-20px);
}
.task-item-leave-to {
opacity: 0;
transform: translateX(20px);
}
.empty-queue {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--vp-c-text-3);
font-size: 13px;
}
.output-section {
margin-bottom: 20px;
}
.output-log {
min-height: 60px;
padding: 12px;
border: 2px solid var(--vp-c-border);
border-radius: 8px;
background: var(--vp-c-bg);
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.empty-log {
color: var(--vp-c-text-3);
font-size: 14px;
}
.log-entry {
padding: 8px 12px;
background: var(--vp-c-brand-1);
color: white;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.log-output {
font-family: 'Courier New', monospace;
}
.log-source {
margin-left: 8px;
font-size: 12px;
opacity: 0.8;
}
.output-enter-active,
.output-leave-active {
transition: all 0.3s ease;
}
.output-enter-from {
opacity: 0;
transform: translateY(-10px);
}
.output-leave-to {
opacity: 0;
transform: translateY(10px);
}
.controls {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
button:active {
transform: scale(0.95);
}
.btn-play {
background: var(--vp-c-brand-1);
color: white;
}
.btn-play:hover:not(:disabled) {
background: var(--vp-c-brand-2);
}
.btn-step {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.btn-step:hover:not(:disabled) {
background: var(--vp-c-bg-soft-hover);
}
.btn-stop {
background: #ed8936;
color: white;
}
.btn-stop:hover:not(:disabled) {
background: #dd6b20;
}
.btn-reset {
background: #f56565;
color: white;
}
.btn-reset:hover:not(:disabled) {
background: #e53e3e;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.rules-box {
background: var(--vp-c-bg-soft);
border-left: 4px solid var(--vp-c-brand-1);
border-radius: 8px;
padding: 16px;
}
.rule-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
}
.rule-item {
display: flex;
align-items: center;
gap: 12px;
}
.rule-number {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--vp-c-brand-1);
color: white;
font-size: 14px;
font-weight: 600;
flex-shrink: 0;
}
.rule-text {
font-size: 14px;
color: var(--vp-c-text-1);
}
.highlight {
padding: 12px;
background: rgba(62, 175, 124, 0.1);
border-radius: 6px;
font-size: 14px;
line-height: 1.6;
color: var(--vp-c-text-1);
margin: 0;
}
.highlight strong {
color: var(--vp-c-brand-1);
}
</style>