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:
@@ -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>浏览器运行时专注于用户界面和网页交互,提供 DOM、BOM、fetch 等前端专用 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>
|
||||
Reference in New Issue
Block a user