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:
@@ -76,15 +76,19 @@
|
||||
<div class="code-line">obj2.x = 20</div>
|
||||
<div class="code-line result">// obj1.x = 20 (变了!)</div>
|
||||
</div>
|
||||
<div class="visual-box">
|
||||
<div class="ref-box">
|
||||
<div>obj1 →</div>
|
||||
<div class="memory-box">{x: 20}</div>
|
||||
</div>
|
||||
<div class="arrow">指向同一位置</div>
|
||||
<div class="ref-box">
|
||||
<div>obj2 →</div>
|
||||
<div class="visual-box ref-visual">
|
||||
<div class="ref-boxes">
|
||||
<div class="ref-var-box">
|
||||
<div class="ref-var-name">obj1</div>
|
||||
<div class="ref-var-arrow">→</div>
|
||||
</div>
|
||||
<div class="ref-var-box">
|
||||
<div class="ref-var-name">obj2</div>
|
||||
<div class="ref-var-arrow">→</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arrow down-arrow">指向同一位置</div>
|
||||
<div class="memory-box">{x: 20}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -469,6 +473,40 @@ const convertType = () => {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 修复引用类型可视化 */
|
||||
.ref-visual {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.ref-boxes {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ref-var-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.ref-var-name {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.ref-var-arrow {
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.down-arrow {
|
||||
color: var(--vp-c-brand);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.ref-types-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,140 +1,106 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
// 基本类型
|
||||
const basicStep = ref(0)
|
||||
const basicA = ref(10)
|
||||
const basicB = ref(null)
|
||||
const basicStep = ref(0)
|
||||
const basicMessage = ref('')
|
||||
|
||||
// 引用类型
|
||||
const obj1Data = ref({ name: '张三', age: 25 })
|
||||
const obj2Exists = ref(false)
|
||||
const obj2Age = ref('')
|
||||
const refMessage = ref('')
|
||||
const refStep = ref(0)
|
||||
const objData = ref({ age: 25 })
|
||||
|
||||
const basicCopy = () => {
|
||||
basicB.value = basicA.value
|
||||
basicStep.value = 1
|
||||
basicMessage.value = '✅ 基本类型复制的是值本身'
|
||||
}
|
||||
const basicCopy = () => { basicB.value = basicA.value; basicStep.value = 1 }
|
||||
const basicModify = () => { basicB.value = 20; basicStep.value = 2 }
|
||||
const basicReset = () => { basicStep.value = 0; basicB.value = null }
|
||||
|
||||
const basicModify = () => {
|
||||
if (basicB.value === null) {
|
||||
basicMessage.value = '⚠️ 请先复制'
|
||||
return
|
||||
}
|
||||
basicB.value = 20
|
||||
basicMessage.value = '✅ 修改 b 不影响 a'
|
||||
}
|
||||
|
||||
const refCopy = () => {
|
||||
obj2Exists.value = true
|
||||
obj2Age.value = obj1Data.value.age
|
||||
refMessage.value = '⚠️ 两个变量指向同一份数据'
|
||||
}
|
||||
|
||||
const refModify = () => {
|
||||
if (!obj2Exists.value) {
|
||||
refMessage.value = '⚠️ 请先复制'
|
||||
return
|
||||
}
|
||||
obj1Data.value.age = 30
|
||||
obj2Age.value = 30
|
||||
refMessage.value = '❌ 两个变量指向同一份数据,改了一个另一个也变了!'
|
||||
}
|
||||
|
||||
const refSpreadCopy = () => {
|
||||
obj2Exists.value = true
|
||||
obj2Age.value = 25
|
||||
refMessage.value = '✅ 用展开运算符创建真正的副本,现在互不影响'
|
||||
}
|
||||
const refCopy = () => { refStep.value = 1 }
|
||||
const refModify = () => { objData.value.age = 30; refStep.value = 2 }
|
||||
const refSpread = () => { refStep.value = 3 }
|
||||
const refReset = () => { refStep.value = 0; objData.value.age = 25 }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="reference-demo">
|
||||
<h3>值 vs 引用</h3>
|
||||
|
||||
<div class="demo-container">
|
||||
<div class="demo-title">🔄 值 vs 引用</div>
|
||||
|
||||
<div class="compare-grid">
|
||||
<!-- 左侧:基本类型 -->
|
||||
<div class="demo-section basic-section">
|
||||
<h4>基本类型(复制值)</h4>
|
||||
|
||||
<div class="visualization">
|
||||
<div class="box" :class="{ 'active': basicA !== null }">
|
||||
<div class="box-label">a</div>
|
||||
<div class="box-value">{{ basicA }}</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow" v-if="basicStep >= 1">
|
||||
<div class="arrow-line"></div>
|
||||
<div class="arrow-head">→</div>
|
||||
</div>
|
||||
|
||||
<div class="box" :class="{ 'active': basicB !== null }" v-if="basicStep >= 1">
|
||||
<div class="box-label">b</div>
|
||||
<div class="box-value">{{ basicB }}</div>
|
||||
<div class="compare-box">
|
||||
<div class="box-header blue">基本类型(复制值)</div>
|
||||
|
||||
<div class="memory-area">
|
||||
<div class="vars-row">
|
||||
<div class="var-item" :class="{ active: basicStep >= 0 }">
|
||||
<span class="var-label">a</span>
|
||||
<span class="var-val">{{ basicA }}</span>
|
||||
</div>
|
||||
<div class="var-item" :class="{ active: basicStep >= 1, changed: basicStep >= 2 }">
|
||||
<span class="var-label">b</span>
|
||||
<span class="var-val">{{ basicB ?? '?' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy-arrow" v-if="basicStep >= 1">↓ 复制值</div>
|
||||
</div>
|
||||
|
||||
<div class="message" v-if="basicMessage">{{ basicMessage }}</div>
|
||||
|
||||
<div class="controls">
|
||||
<button @click="basicCopy" class="btn-primary">let b = a(复制)</button>
|
||||
<button @click="basicModify" class="btn-secondary">b = 20</button>
|
||||
|
||||
<div class="result-text" :class="basicStep === 2 ? 'success' : 'info'">
|
||||
{{ basicStep === 0 ? '点击复制' : basicStep === 1 ? 'b 得到 10' : '✅ 修改 b 不影响 a' }}
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button @click="basicCopy" :disabled="basicStep >= 1">复制</button>
|
||||
<button @click="basicModify" :disabled="basicStep !== 1">改 b</button>
|
||||
<button @click="basicReset" class="reset">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 右侧:引用类型 -->
|
||||
<div class="demo-section reference-section">
|
||||
<h4>引用类型(复制地址)</h4>
|
||||
|
||||
<div class="visualization">
|
||||
<!-- 数据区 -->
|
||||
<div class="data-area">
|
||||
<div class="data-label">数据区</div>
|
||||
<div class="data-content">
|
||||
<div>{{ `{ name: "${obj1Data.name}", age: ${obj1Data.age} }` }}</div>
|
||||
<div class="compare-box">
|
||||
<div class="box-header orange">引用类型(复制地址)</div>
|
||||
|
||||
<div class="memory-area">
|
||||
<div class="vars-row">
|
||||
<div class="var-item" :class="{ active: refStep >= 0 }">
|
||||
<span class="var-label">obj1</span>
|
||||
<span class="var-addr">0x001</span>
|
||||
</div>
|
||||
<div class="var-item" :class="{ active: refStep >= 1 }">
|
||||
<span class="var-label">obj2</span>
|
||||
<span class="var-addr">{{ refStep >= 1 ? '0x001' : '?' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pointers">
|
||||
<div class="pointer">
|
||||
<div class="pointer-label">obj1</div>
|
||||
<div class="arrow-line-down"></div>
|
||||
</div>
|
||||
|
||||
<div class="pointer" v-if="obj2Exists">
|
||||
<div class="pointer-label">obj2</div>
|
||||
<div class="arrow-line-down"></div>
|
||||
</div>
|
||||
<div class="data-box" :class="{ changed: refStep === 2, copied: refStep === 3 }">
|
||||
<div class="data-addr">0x001</div>
|
||||
<div class="data-content">{ age: {{ objData.age }} }</div>
|
||||
</div>
|
||||
<div class="copy-arrow" v-if="refStep >= 1">指向同一地址</div>
|
||||
</div>
|
||||
|
||||
<div class="message" v-if="refMessage">{{ refMessage }}</div>
|
||||
|
||||
<div class="controls">
|
||||
<button @click="refCopy" class="btn-primary">let obj2 = obj1(复制)</button>
|
||||
<button @click="refModify" class="btn-danger">obj2.age = 30</button>
|
||||
<button @click="refSpreadCopy" class="btn-success">用展开运算符创建副本</button>
|
||||
|
||||
<div class="result-text" :class="refStep === 2 ? 'warning' : refStep === 3 ? 'success' : 'info'">
|
||||
{{ refStep === 0 ? '点击复制' : refStep === 1 ? '共享地址' : refStep === 2 ? '⚠️ 一改全变' : '✅ 已分离' }}
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button @click="refCopy" :disabled="refStep >= 1">复制</button>
|
||||
<button @click="refModify" :disabled="refStep !== 1">修改</button>
|
||||
<button @click="refSpread" :disabled="refStep !== 2">展开</button>
|
||||
<button @click="refReset" class="reset">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="code-display">
|
||||
<h4>代码</h4>
|
||||
<pre><code>// 基本类型
|
||||
let a = {{ basicA }}
|
||||
{{ basicB !== null ? `let b = ${basicB}` : '' }}
|
||||
{{ basicB !== null ? `b = ${basicB}` : '' }}
|
||||
console.log(a) // {{ basicA }}
|
||||
|
||||
// 引用类型
|
||||
let obj1 = { name: "{{ obj1Data.name }}", age: {{ obj1Data.age }} }
|
||||
{{ obj2Exists ? 'let obj2 = obj1 // 指向同一份数据' : '' }}
|
||||
{{ obj2Exists ? `obj2.age = ${obj1Data.age}` : '' }}
|
||||
console.log(obj1.age) // {{ obj1Data.age }}
|
||||
</code></pre>
|
||||
|
||||
<div class="code-compare">
|
||||
<div class="code-col">
|
||||
<div class="code-title">基本类型</div>
|
||||
<pre><code>let a = 10
|
||||
let b = a // b=10
|
||||
b = 20 // a还是10</code></pre>
|
||||
</div>
|
||||
<div class="code-col">
|
||||
<div class="code-title">引用类型</div>
|
||||
<pre><code>let obj1 = {age:25}
|
||||
let obj2 = obj1
|
||||
obj2.age=30 // obj1也变了!
|
||||
// 用 {...obj1} 复制</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -143,235 +109,221 @@ console.log(obj1.age) // {{ obj1Data.age }}
|
||||
.reference-demo {
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin: 24px 0;
|
||||
padding: 20px;
|
||||
margin: 16px 0;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 18px;
|
||||
.demo-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
.compare-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-container {
|
||||
.compare-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
border: 1px dashed var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
.compare-box {
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.visualization {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 16px;
|
||||
min-height: 120px;
|
||||
.box-header {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.box-header.blue {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.box-header.orange {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.memory-area {
|
||||
background: var(--vp-c-bg);
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.box.active {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
box-shadow: 0 0 0 3px rgba(62, 175, 124, 0.1);
|
||||
}
|
||||
|
||||
.box-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.box-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
.vars-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.arrow-line {
|
||||
width: 40px;
|
||||
height: 2px;
|
||||
background: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.arrow-head {
|
||||
font-size: 24px;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.data-area {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.data-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.data-content {
|
||||
border: 2px solid var(--vp-c-brand-1);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.pointers {
|
||||
.var-item {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-top: 12px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 6px;
|
||||
opacity: 0.4;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
text-align: center;
|
||||
.var-item.active {
|
||||
opacity: 1;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.pointer-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
.var-item.changed {
|
||||
border-color: #10b981;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
|
||||
.var-label {
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.arrow-line-down {
|
||||
width: 2px;
|
||||
height: 30px;
|
||||
background: var(--vp-c-brand-1);
|
||||
margin: 0 auto;
|
||||
.var-val, .var-addr {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
font-family: monospace;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.message {
|
||||
.var-addr {
|
||||
color: #8b5cf6;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.copy-arrow {
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-2);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.data-box {
|
||||
border: 2px solid #8b5cf6;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
background: #f3e8ff;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.data-box.changed {
|
||||
border-color: #ef4444;
|
||||
background: #fee2e2;
|
||||
}
|
||||
|
||||
.data-box.copied {
|
||||
border-color: #10b981;
|
||||
background: #d1fae5;
|
||||
}
|
||||
|
||||
.data-addr {
|
||||
font-size: 10px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.data-content {
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.result-text {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.controls {
|
||||
.result-text.info {
|
||||
background: #f3f4f6;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.result-text.success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.result-text.warning {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button {
|
||||
.btn-group button {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--vp-c-brand-1);
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--vp-c-brand-2);
|
||||
.btn-group button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--vp-c-bg-soft);
|
||||
.btn-group button.reset {
|
||||
background: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-1);
|
||||
border: 1px solid var(--vp-c-border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--vp-c-bg-soft-hover);
|
||||
.code-compare {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #f56565;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #e53e3e;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #38a169;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #2f855a;
|
||||
}
|
||||
|
||||
.code-display {
|
||||
.code-col {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.code-display h4 {
|
||||
color: #d4d4d4;
|
||||
margin-bottom: 12px;
|
||||
.code-title {
|
||||
color: #9ca3af;
|
||||
font-size: 11px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.code-display pre {
|
||||
.code-col pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-display code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
.code-col code {
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const activeScope = ref('block') // 'global', 'function', 'block'
|
||||
const activeScope = ref('global')
|
||||
const explanation = ref('')
|
||||
|
||||
const scopes = [
|
||||
@@ -9,31 +9,36 @@ const scopes = [
|
||||
id: 'global',
|
||||
name: '全局作用域',
|
||||
color: '#a0aec0',
|
||||
variables: ['appName = "Todo"'],
|
||||
canSee: ['appName']
|
||||
vars: [{ name: 'appName', value: '"Todo"', own: true }]
|
||||
},
|
||||
{
|
||||
id: 'function',
|
||||
name: '函数 greet() 作用域',
|
||||
color: '#4299e1',
|
||||
variables: ['message = "你好"'],
|
||||
canSee: ['appName', 'message']
|
||||
vars: [
|
||||
{ name: 'appName', value: '"Todo"', own: false, from: '全局' },
|
||||
{ name: 'message', value: '"你好"', own: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'block',
|
||||
name: 'if 块作用域',
|
||||
color: '#38a169',
|
||||
variables: ['greeting = message + appName'],
|
||||
canSee: ['appName', 'message', 'greeting']
|
||||
vars: [
|
||||
{ name: 'appName', value: '"Todo"', own: false, from: '全局' },
|
||||
{ name: 'message', value: '"你好"', own: false, from: '函数' },
|
||||
{ name: 'greeting', value: 'message+appName', own: true }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const updateExplanation = () => {
|
||||
const scope = scopes.find(s => s.id === activeScope.value)
|
||||
if (scope) {
|
||||
const visible = scope.canSee.map(v => `✅ ${v}`).join('、')
|
||||
explanation.value = `在这个位置,你能使用这些变量:${visible}`
|
||||
const texts = {
|
||||
global: '在全局作用域,只能使用全局变量 appName',
|
||||
function: '在函数作用域,可以使用自己的 message 和全局的 appName(作用域链查找)',
|
||||
block: '在块级作用域,可以使用自己的 greeting,以及外层的 message 和 appName'
|
||||
}
|
||||
explanation.value = texts[activeScope.value]
|
||||
}
|
||||
|
||||
updateExplanation()
|
||||
@@ -41,67 +46,54 @@ updateExplanation()
|
||||
|
||||
<template>
|
||||
<div class="scope-demo">
|
||||
<h3>作用域:变量的"可见范围"</h3>
|
||||
<h3>🔍 作用域:变量的"可见范围"</h3>
|
||||
|
||||
<div class="scopes-container">
|
||||
<!-- 全局作用域 -->
|
||||
<div
|
||||
class="scope global-scope"
|
||||
:class="{ 'active': activeScope === 'global' }"
|
||||
@click="activeScope = 'global'; updateExplanation()"
|
||||
<div class="scope-selector">
|
||||
<button
|
||||
v-for="scope in scopes"
|
||||
:key="scope.id"
|
||||
@click="activeScope = scope.id; updateExplanation()"
|
||||
class="scope-btn"
|
||||
:class="{ active: activeScope === scope.id }"
|
||||
:style="{ borderColor: scope.color }"
|
||||
>
|
||||
<div class="scope-header">全局作用域</div>
|
||||
<div class="scope-content">
|
||||
<div class="variable" :class="{ 'visible': activeScope === 'global', 'dimmed': activeScope !== 'global' }">
|
||||
appName = "Todo"
|
||||
{{ scope.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="scope-visual">
|
||||
<!-- 作用域层级图 -->
|
||||
<div class="scope-levels">
|
||||
<div
|
||||
v-for="(scope, index) in scopes"
|
||||
:key="scope.id"
|
||||
class="level"
|
||||
:class="{ active: activeScope === scope.id, dimmed: activeScope !== scope.id }"
|
||||
:style="{ borderLeftColor: scope.color }"
|
||||
>
|
||||
<div class="level-header" :style="{ color: scope.color }">
|
||||
{{ scope.name }}
|
||||
</div>
|
||||
|
||||
<!-- 函数作用域 -->
|
||||
<div class="nested-scope">
|
||||
<div class="level-vars">
|
||||
<div
|
||||
class="scope function-scope"
|
||||
:class="{ 'active': activeScope === 'function' }"
|
||||
@click.stop="activeScope = 'function'; updateExplanation()"
|
||||
v-for="v in scope.vars"
|
||||
:key="v.name"
|
||||
class="var-tag"
|
||||
:class="{ own: v.own, inherited: !v.own }"
|
||||
>
|
||||
<div class="scope-header">函数 greet() 作用域</div>
|
||||
<div class="scope-content">
|
||||
<div class="variable" :class="{ 'visible': ['global', 'function'].includes(activeScope), 'dimmed': !['global', 'function'].includes(activeScope) }">
|
||||
appName = "Todo"
|
||||
</div>
|
||||
<div class="variable" :class="{ 'visible': activeScope === 'function', 'dimmed': activeScope !== 'function' }">
|
||||
message = "你好"
|
||||
</div>
|
||||
|
||||
<!-- 块级作用域 -->
|
||||
<div class="nested-scope">
|
||||
<div
|
||||
class="scope block-scope"
|
||||
:class="{ 'active': activeScope === 'block' }"
|
||||
@click.stop="activeScope = 'block'; updateExplanation()"
|
||||
>
|
||||
<div class="scope-header">if 块作用域</div>
|
||||
<div class="scope-content">
|
||||
<div class="variable" :class="{ 'visible': true, 'dimmed': false }">
|
||||
appName = "Todo"
|
||||
</div>
|
||||
<div class="variable" :class="{ 'visible': true, 'dimmed': false }">
|
||||
message = "你好"
|
||||
</div>
|
||||
<div class="variable" :class="{ 'visible': activeScope === 'block', 'dimmed': activeScope !== 'block' }">
|
||||
greeting = message + appName
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="var-name">{{ v.name }}</span>
|
||||
<span class="var-value">= {{ v.value }}</span>
|
||||
<span v-if="!v.own" class="var-from">← {{ v.from }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="explanation" v-if="explanation">
|
||||
{{ explanation }}
|
||||
<!-- 说明 -->
|
||||
<div class="explanation-box">
|
||||
<div class="explanation-title">💡 当前位置可见的变量</div>
|
||||
<div class="explanation-text">{{ explanation }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="code-display">
|
||||
@@ -112,7 +104,7 @@ function greet() {
|
||||
const message = "你好" // 函数作用域
|
||||
|
||||
if (true) {
|
||||
const greeting = message + appName // 块级作用域 ✅ 能看到外层的
|
||||
const greeting = message + appName // 块级作用域
|
||||
console.log(greeting)
|
||||
}
|
||||
|
||||
@@ -138,112 +130,133 @@ h3 {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.scopes-container {
|
||||
.scope-selector {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.scope {
|
||||
border: 3px solid var(--vp-c-border);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
.scope-btn {
|
||||
padding: 10px 16px;
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.scope-btn:hover {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.scope:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
transform: scale(1.02);
|
||||
.scope-btn.active {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.scope.active {
|
||||
border-width: 4px;
|
||||
box-shadow: 0 0 0 4px rgba(62, 175, 124, 0.1);
|
||||
.scope-visual {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.global-scope {
|
||||
border-color: #a0aec0;
|
||||
@media (max-width: 768px) {
|
||||
.scope-visual {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.global-scope.active {
|
||||
border-color: #a0aec0;
|
||||
box-shadow: 0 0 0 4px rgba(160, 174, 192, 0.2);
|
||||
}
|
||||
|
||||
.function-scope {
|
||||
border-color: #4299e1;
|
||||
}
|
||||
|
||||
.function-scope.active {
|
||||
border-color: #4299e1;
|
||||
box-shadow: 0 0 0 4px rgba(66, 153, 225, 0.2);
|
||||
}
|
||||
|
||||
.block-scope {
|
||||
border-color: #38a169;
|
||||
}
|
||||
|
||||
.block-scope.active {
|
||||
border-color: #38a169;
|
||||
box-shadow: 0 0 0 4px rgba(56, 161, 105, 0.2);
|
||||
}
|
||||
|
||||
.scope-header {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.scope-content {
|
||||
.scope-levels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.level {
|
||||
border-left: 4px solid;
|
||||
padding: 12px 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 0 8px 8px 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.level.active {
|
||||
background: var(--vp-c-bg);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.level.dimmed {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.level-header {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.level-vars {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.nested-scope {
|
||||
.var-tag {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.variable {
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: var(--vp-c-bg);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.variable.visible {
|
||||
color: var(--vp-c-text-1);
|
||||
.var-tag.own {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border: 1px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.var-tag.inherited {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px dashed var(--vp-c-border);
|
||||
}
|
||||
|
||||
.var-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.variable.dimmed {
|
||||
color: var(--vp-c-text-3);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.explanation {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
.var-value {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.var-from {
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-3);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.explanation-box {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.explanation-title {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand);
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.explanation-text {
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.code-display {
|
||||
@@ -270,15 +283,4 @@ h3 {
|
||||
line-height: 1.6;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.scopes-container {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.scope {
|
||||
min-width: 280px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,85 +5,80 @@ const name = ref('张三')
|
||||
const age = ref(25)
|
||||
const isStudent = ref(true)
|
||||
const showMessage = ref('')
|
||||
const showSuccess = ref(false)
|
||||
const messageType = ref('')
|
||||
let messageTimer = null
|
||||
|
||||
const clearMessage = () => {
|
||||
showMessage.value = ''
|
||||
messageType.value = ''
|
||||
}
|
||||
|
||||
const setMessage = (msg, type) => {
|
||||
if (messageTimer) clearTimeout(messageTimer)
|
||||
showMessage.value = msg
|
||||
messageType.value = type
|
||||
messageTimer = setTimeout(() => clearMessage(), 2000)
|
||||
}
|
||||
|
||||
const modifyAge = () => {
|
||||
age.value = 26
|
||||
showMessage.value = '✅ let 变量可以修改值'
|
||||
showSuccess.value = true
|
||||
setTimeout(() => {
|
||||
showMessage.value = ''
|
||||
showSuccess.value = false
|
||||
}, 2000)
|
||||
setMessage('✅ let 可以修改', 'success')
|
||||
}
|
||||
|
||||
const modifyName = () => {
|
||||
showMessage.value = '❌ const 不能重新赋值'
|
||||
showSuccess.value = false
|
||||
setTimeout(() => {
|
||||
showMessage.value = ''
|
||||
}, 2000)
|
||||
setMessage('❌ const 不能改', 'error')
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
name.value = '张三'
|
||||
age.value = 25
|
||||
isStudent.value = true
|
||||
showMessage.value = ''
|
||||
clearMessage()
|
||||
}
|
||||
|
||||
const codeLines = ref([
|
||||
`const name = "张三"`,
|
||||
`let age = 25`,
|
||||
`const isStudent = true`
|
||||
])
|
||||
|
||||
const executeCode = ref([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="variable-box-demo">
|
||||
<h3>变量就像带名字的盒子</h3>
|
||||
<div class="demo-header">
|
||||
<span class="title">📦 变量就像带名字的盒子</span>
|
||||
</div>
|
||||
|
||||
<div class="boxes-container">
|
||||
<!-- 盒子 1: const name -->
|
||||
<div class="variable-box const-box">
|
||||
<div class="box-label">const name</div>
|
||||
<div class="boxes-row">
|
||||
<div class="var-box" :class="{ error: messageType === 'error' }">
|
||||
<div class="box-tag const">const</div>
|
||||
<div class="box-name">name</div>
|
||||
<div class="box-value">{{ name }}</div>
|
||||
<div class="box-icon">🔒</div>
|
||||
<div class="box-lock">🔒</div>
|
||||
</div>
|
||||
|
||||
<!-- 盒子 2: let age -->
|
||||
<div class="variable-box let-box" :class="{ 'success': showSuccess && age === 26 }">
|
||||
<div class="box-label">let age</div>
|
||||
<div class="var-box" :class="{ success: messageType === 'success' }">
|
||||
<div class="box-tag let">let</div>
|
||||
<div class="box-name">age</div>
|
||||
<div class="box-value">{{ age }}</div>
|
||||
<div class="box-icon">🔓</div>
|
||||
<div class="box-lock">🔓</div>
|
||||
</div>
|
||||
|
||||
<!-- 盒子 3: const isStudent -->
|
||||
<div class="variable-box const-box">
|
||||
<div class="box-label">const isStudent</div>
|
||||
<div class="var-box">
|
||||
<div class="box-tag const">const</div>
|
||||
<div class="box-name">isStudent</div>
|
||||
<div class="box-value">{{ isStudent }}</div>
|
||||
<div class="box-icon">🔒</div>
|
||||
<div class="box-lock">🔒</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-bubble" :class="{ 'error': !showSuccess, 'success': showSuccess }" v-if="showMessage">
|
||||
<div class="message" v-if="showMessage" :class="messageType">
|
||||
{{ showMessage }}
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button @click="modifyAge" class="btn-primary">修改 age 为 26</button>
|
||||
<button @click="modifyName" class="btn-danger">修改 name 为李四</button>
|
||||
<button @click="reset" class="btn-secondary">重置</button>
|
||||
<button @click="modifyAge" class="btn btn-primary">修改 age</button>
|
||||
<button @click="modifyName" class="btn btn-danger">修改 name</button>
|
||||
<button @click="reset" class="btn btn-secondary">重置</button>
|
||||
</div>
|
||||
|
||||
<div class="code-display">
|
||||
<pre><code>const name = "张三"
|
||||
let age = 25
|
||||
const isStudent = true
|
||||
|
||||
{{ executeCode.join('\n') }}</code></pre>
|
||||
<div class="code-snippet">
|
||||
<code>const name = "{{ name }}"</code>
|
||||
<code>let age = {{ age }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -92,42 +87,59 @@ const isStudent = true
|
||||
.variable-box-demo {
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin: 24px 0;
|
||||
padding: 20px;
|
||||
margin: 16px 0;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 18px;
|
||||
.demo-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.boxes-container {
|
||||
.boxes-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.variable-box {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
.var-box {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 12px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background: var(--vp-c-bg);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.variable-box.success {
|
||||
border-color: #3eaf7c;
|
||||
animation: pulse 0.5s ease;
|
||||
.var-box.error {
|
||||
border-color: #ef4444;
|
||||
background: #fef2f2;
|
||||
animation: shake 0.4s ease;
|
||||
}
|
||||
|
||||
.var-box.success {
|
||||
border-color: #10b981;
|
||||
background: #ecfdf5;
|
||||
animation: pulse 0.4s ease;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-4px); }
|
||||
75% { transform: translateX(4px); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
@@ -135,144 +147,107 @@ h3 {
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.box-label {
|
||||
.box-tag {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--vp-c-brand-1);
|
||||
top: -10px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.let-box .box-label {
|
||||
background: #42b983;
|
||||
.box-tag.const {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.box-tag.let {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.box-name {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.box-value {
|
||||
font-size: 24px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-family: monospace;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.box-icon {
|
||||
font-size: 16px;
|
||||
.box-lock {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
.message {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
.message.error {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.message-bubble.error {
|
||||
background: #fee;
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
.message-bubble.success {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
.message.success {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
.btn {
|
||||
padding: 8px 14px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--vp-c-brand-1);
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #f56565;
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #e53e3e;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
border: 1px solid var(--vp-c-border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--vp-c-bg-soft-hover);
|
||||
}
|
||||
|
||||
.code-display {
|
||||
.code-snippet {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
border-radius: 6px;
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.code-display pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-display code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
.code-snippet code {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.boxes-container {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.variable-box {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,556 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
// 泛型函数演示
|
||||
const inputValue = ref('')
|
||||
const selectedType = ref('number')
|
||||
const result = ref(null)
|
||||
const showResult = ref(false)
|
||||
|
||||
// 泛型数组反转(不使用 TypeScript 泛型语法)
|
||||
function reverseArray(arr) {
|
||||
return [...arr].reverse()
|
||||
}
|
||||
|
||||
// 执行反转操作
|
||||
const executeReverse = () => {
|
||||
if (!inputValue.value) {
|
||||
result.value = '请输入内容'
|
||||
showResult.value = true
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
switch (selectedType.value) {
|
||||
case 'number':
|
||||
const numArray = inputValue.value.split(',').map(n => parseFloat(n.trim())).filter(n => !isNaN(n))
|
||||
result.value = {
|
||||
input: numArray,
|
||||
output: reverseArray(numArray),
|
||||
type: 'number[]'
|
||||
}
|
||||
break
|
||||
case 'string':
|
||||
const strArray = inputValue.value.split(',').map(s => s.trim())
|
||||
result.value = {
|
||||
input: strArray,
|
||||
output: reverseArray(strArray),
|
||||
type: 'string[]'
|
||||
}
|
||||
break
|
||||
default:
|
||||
result.value = { error: '未知类型' }
|
||||
}
|
||||
showResult.value = true
|
||||
} catch (error) {
|
||||
result.value = { error: '输入格式错误' }
|
||||
showResult.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const reset = () => {
|
||||
inputValue.value = ''
|
||||
result.value = null
|
||||
showResult.value = false
|
||||
}
|
||||
|
||||
// 示例数据
|
||||
const loadExample = (type) => {
|
||||
selectedType.value = type
|
||||
if (type === 'number') {
|
||||
inputValue.value = '1, 2, 3, 4, 5'
|
||||
} else {
|
||||
inputValue.value = '苹果, 香蕉, 橙子, 葡萄'
|
||||
}
|
||||
result.value = null
|
||||
showResult.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="generic-demo">
|
||||
<h3>🔄 泛型 (Generics) 演示</h3>
|
||||
|
||||
<div class="demo-container">
|
||||
<!-- 泛型概念说明 -->
|
||||
<div class="concept-box">
|
||||
<div class="concept-icon">💡</div>
|
||||
<div class="concept-text">
|
||||
<strong>泛型就像"通用模板"</strong> - 可以处理不同类型的数据,同时保持类型安全
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 泛型函数定义 -->
|
||||
<div class="function-definition">
|
||||
<div class="code-header">
|
||||
<span class="typescript-logo">TS</span>
|
||||
<span>泛型函数定义</span>
|
||||
</div>
|
||||
<pre><code class="typescript">// T 是类型变量,使用时才会确定具体类型
|
||||
function identity<T>(arg: T): T {
|
||||
return arg
|
||||
}
|
||||
|
||||
// 泛型数组反转
|
||||
function reverseArray<T>(arr: T[]): T[] {
|
||||
return [...arr].reverse()
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- 交互演示 -->
|
||||
<div class="interactive-demo">
|
||||
<div class="demo-controls">
|
||||
<div class="input-group">
|
||||
<label>选择数据类型:</label>
|
||||
<div class="type-selector">
|
||||
<button
|
||||
:class="['type-btn', { active: selectedType === 'number' }]"
|
||||
@click="selectedType = 'number'"
|
||||
>
|
||||
数字数组
|
||||
</button>
|
||||
<button
|
||||
:class="['type-btn', { active: selectedType === 'string' }]"
|
||||
@click="selectedType = 'string'"
|
||||
>
|
||||
字符串数组
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>输入数组(逗号分隔):</label>
|
||||
<input
|
||||
v-model="inputValue"
|
||||
type="text"
|
||||
:placeholder="selectedType === 'number' ? '1, 2, 3, 4, 5' : '苹果, 香蕉, 橙子'"
|
||||
class="text-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="example-buttons">
|
||||
<button @click="loadExample('number')" class="btn-example">
|
||||
加载数字示例
|
||||
</button>
|
||||
<button @click="loadExample('string')" class="btn-example">
|
||||
加载字符串示例
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button @click="executeReverse" class="btn-primary">
|
||||
执行反转
|
||||
</button>
|
||||
<button @click="reset" class="btn-secondary">
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 结果展示 -->
|
||||
<div v-if="showResult" class="result-display">
|
||||
<div class="result-header">
|
||||
<span class="result-icon">📊</span>
|
||||
<span>执行结果</span>
|
||||
</div>
|
||||
|
||||
<div v-if="result && !result.error" class="result-content">
|
||||
<div class="result-item">
|
||||
<div class="result-label">输入类型:</div>
|
||||
<div class="result-value type-badge">{{ result.type }}</div>
|
||||
</div>
|
||||
|
||||
<div class="result-item">
|
||||
<div class="result-label">输入数组:</div>
|
||||
<div class="result-value array-display">
|
||||
[{{ result.input.join(', ') }}]
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-item">
|
||||
<div class="result-label">输出数组:</div>
|
||||
<div class="result-value array-display output">
|
||||
[{{ result.output.join(', ') }}]
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="type-info">
|
||||
<div class="info-icon">✅</div>
|
||||
<div>类型安全:输入 {{ result.type }},输出 {{ result.type }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="error-display">
|
||||
{{ result?.error || result }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用示例 -->
|
||||
<div class="usage-examples">
|
||||
<h4>📝 泛型使用示例</h4>
|
||||
<div class="example-grid">
|
||||
<div class="example-card">
|
||||
<div class="example-title">数字数组</div>
|
||||
<pre><code class="typescript">const nums = [1, 2, 3, 4, 5]
|
||||
const reversed = reverseArray<number>(nums)
|
||||
// 结果: [5, 4, 3, 2, 1]
|
||||
// 类型: number[]</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="example-card">
|
||||
<div class="example-title">字符串数组</div>
|
||||
<pre><code class="typescript">const strs = ["a", "b", "c"]
|
||||
const reversed = reverseArray<string>(strs)
|
||||
// 结果: ["c", "b", "a"]
|
||||
// 类型: string[]</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.generic-demo {
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 16px;
|
||||
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);
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.concept-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.concept-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.concept-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.function-definition {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid #3178c6;
|
||||
}
|
||||
|
||||
.code-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.typescript-logo {
|
||||
background: #3178c6;
|
||||
color: white;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.function-definition pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.function-definition code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.interactive-demo {
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.type-selector {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.type-btn {
|
||||
flex: 1;
|
||||
padding: 10px 16px;
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.type-btn:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.type-btn.active {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
background: var(--vp-c-brand-1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.text-input {
|
||||
padding: 12px 16px;
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-family: 'Courier New', monospace;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.text-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.example-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 18px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: white;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
border: 1px solid var(--vp-c-border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--vp-c-bg-soft-hover);
|
||||
}
|
||||
|
||||
.btn-example {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-example:hover {
|
||||
background: #bfdbfe;
|
||||
}
|
||||
|
||||
.result-display {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
border: 2px solid var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.result-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.result-label {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
flex: 1;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
padding: 4px 10px;
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.array-display {
|
||||
padding: 8px 12px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.array-display.output {
|
||||
background: #d1fae5;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.type-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: #f0fdf4;
|
||||
border-radius: 6px;
|
||||
color: #166534;
|
||||
font-size: 13px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.error-display {
|
||||
padding: 12px;
|
||||
background: #fef2f2;
|
||||
color: #991b1b;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.usage-examples {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.usage-examples h4 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.example-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.example-card {
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.example-title {
|
||||
padding: 12px 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.example-card pre {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
background: #1e1e1e;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.example-card code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,445 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
// 用户数据
|
||||
const user = ref({
|
||||
id: 1,
|
||||
name: '张三',
|
||||
email: 'zhangsan@example.com',
|
||||
age: 25
|
||||
})
|
||||
|
||||
// 显示错误信息
|
||||
const showError = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
const setMessage = (msg, isError = false) => {
|
||||
errorMessage.value = msg
|
||||
showError.value = isError
|
||||
setTimeout(() => {
|
||||
errorMessage.value = ''
|
||||
showError.value = false
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// 尝试添加错误类型的属性
|
||||
const addErrorProperty = () => {
|
||||
showError.value = true
|
||||
errorMessage.value = '❌ TypeScript 错误:类型 "string" 不可分配给类型 "number"'
|
||||
setTimeout(() => {
|
||||
showError.value = false
|
||||
errorMessage.value = ''
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// 添加新用户
|
||||
const addNewUser = () => {
|
||||
user.value = {
|
||||
id: 2,
|
||||
name: '李四',
|
||||
email: 'lisi@example.com',
|
||||
age: 30
|
||||
}
|
||||
setMessage('✅ 创建新用户成功!类型检查通过', false)
|
||||
}
|
||||
|
||||
// 修改用户年龄
|
||||
const modifyAge = () => {
|
||||
user.value.age = user.value.age + 1
|
||||
setMessage(`✅ 年龄更新为 ${user.value.age}`, false)
|
||||
}
|
||||
|
||||
// 重置
|
||||
const reset = () => {
|
||||
user.value = {
|
||||
id: 1,
|
||||
name: '张三',
|
||||
email: 'zhangsan@example.com',
|
||||
age: 25
|
||||
}
|
||||
errorMessage.value = ''
|
||||
showError.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="interface-demo">
|
||||
<h3>🎯 Interface 接口演示</h3>
|
||||
|
||||
<div class="demo-container">
|
||||
<!-- 接口定义 -->
|
||||
<div class="interface-definition">
|
||||
<div class="code-header">
|
||||
<span class="typescript-logo">TS</span>
|
||||
<span>User Interface 定义</span>
|
||||
</div>
|
||||
<pre><code class="typescript">interface User {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
age: number
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- 用户对象展示 -->
|
||||
<div class="user-display">
|
||||
<div class="user-card">
|
||||
<div class="card-header">
|
||||
<div class="avatar">👤</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name">{{ user.name }}</div>
|
||||
<div class="user-email">{{ user.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<div class="detail-item">
|
||||
<span class="label">ID:</span>
|
||||
<span class="value">{{ user.id }}</span>
|
||||
<span class="type-badge">number</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">年龄:</span>
|
||||
<span class="value">{{ user.age }}</span>
|
||||
<span class="type-badge">number</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误消息显示 -->
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
:class="['message-box', showError ? 'error' : 'success']"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="controls">
|
||||
<button @click="modifyAge" class="btn-primary">
|
||||
增加年龄
|
||||
</button>
|
||||
<button @click="addErrorProperty" class="btn-danger">
|
||||
尝试赋值错误类型
|
||||
</button>
|
||||
<button @click="addNewUser" class="btn-secondary">
|
||||
创建新用户
|
||||
</button>
|
||||
<button @click="reset" class="btn-ghost">重置</button>
|
||||
</div>
|
||||
|
||||
<!-- 代码示例 -->
|
||||
<div class="code-examples">
|
||||
<div class="example-item">
|
||||
<div class="example-header">✅ 正确使用</div>
|
||||
<pre><code class="typescript">const user: User = {
|
||||
id: 1,
|
||||
name: "张三",
|
||||
email: "zhangsan@example.com",
|
||||
age: 25
|
||||
} // ✅ 类型完全匹配</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="example-item error">
|
||||
<div class="example-header">❌ 错误使用</div>
|
||||
<pre><code class="typescript">const user: User = {
|
||||
id: 1,
|
||||
name: "张三",
|
||||
email: "zhangsan@example.com",
|
||||
age: "25" // ❌ 错误:age 应该是 number,不是 string
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.interface-demo {
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 16px;
|
||||
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);
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.interface-definition {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid #3178c6;
|
||||
}
|
||||
|
||||
.code-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.typescript-logo {
|
||||
background: #3178c6;
|
||||
color: white;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.interface-definition pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.interface-definition code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.user-display {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--vp-c-border);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.user-email {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.user-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-family: 'Courier New', monospace;
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
margin-left: auto;
|
||||
padding: 3px 8px;
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.message-box {
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.message-box.error {
|
||||
background: #fef2f2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
.message-box.success {
|
||||
background: #f0fdf4;
|
||||
color: #166534;
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 18px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6b7280;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--vp-c-text-2);
|
||||
border: 1px solid var(--vp-c-border);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: var(--vp-c-bg-soft-hover);
|
||||
}
|
||||
|
||||
.code-examples {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.code-examples {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.example-item {
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.example-item.error {
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.example-header {
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.example-item.error .example-header {
|
||||
background: #fef2f2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.example-item pre {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
background: #1e1e1e;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.example-item code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,402 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const name = ref('张三')
|
||||
const age = ref(25)
|
||||
const isActive = ref(true)
|
||||
const showError = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
const setMessage = (msg, isError = false) => {
|
||||
errorMessage.value = msg
|
||||
showError.value = isError
|
||||
setTimeout(() => {
|
||||
errorMessage.value = ''
|
||||
showError.value = false
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
const modifyName = () => {
|
||||
// TypeScript 会在编译时检查类型错误
|
||||
// name.value = 123 // 这行会在 TypeScript 中报错
|
||||
name.value = '李四'
|
||||
setMessage('✅ 修改成功!类型检查通过', false)
|
||||
}
|
||||
|
||||
const modifyAgeError = () => {
|
||||
// 演示类型错误
|
||||
showError.value = true
|
||||
errorMessage.value = '❌ TypeScript 错误:不能将类型 "string" 分配给类型 "number"'
|
||||
setTimeout(() => {
|
||||
showError.value = false
|
||||
errorMessage.value = ''
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
const toggleActive = () => {
|
||||
isActive.value = !isActive.value
|
||||
setMessage(`✅ 状态切换为 ${isActive.value}`, false)
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
name.value = '张三'
|
||||
age.value = 25
|
||||
isActive.value = true
|
||||
errorMessage.value = ''
|
||||
showError.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="type-annotation-demo">
|
||||
<h3>📝 TypeScript 类型注解演示</h3>
|
||||
|
||||
<div class="demo-container">
|
||||
<div class="variables-grid">
|
||||
<!-- String 类型 -->
|
||||
<div class="variable-card string-card">
|
||||
<div class="card-header">
|
||||
<span class="type-badge string">string</span>
|
||||
<span class="var-name">name</span>
|
||||
</div>
|
||||
<div class="card-value">{{ name }}</div>
|
||||
<div class="card-code">
|
||||
<code>const name: string = "{{ name }}"</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Number 类型 -->
|
||||
<div class="variable-card number-card">
|
||||
<div class="card-header">
|
||||
<span class="type-badge number">number</span>
|
||||
<span class="var-name">age</span>
|
||||
</div>
|
||||
<div class="card-value">{{ age }}</div>
|
||||
<div class="card-code">
|
||||
<code>const age: number = {{ age }}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Boolean 类型 -->
|
||||
<div class="variable-card boolean-card">
|
||||
<div class="card-header">
|
||||
<span class="type-badge boolean">boolean</span>
|
||||
<span class="var-name">isActive</span>
|
||||
</div>
|
||||
<div class="card-value">
|
||||
<span :class="['status-dot', isActive ? 'active' : 'inactive']"></span>
|
||||
{{ isActive ? 'true' : 'false' }}
|
||||
</div>
|
||||
<div class="card-code">
|
||||
<code>const isActive: boolean = {{ isActive }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误消息显示 -->
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
:class="['message-box', showError ? 'error' : 'success']"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="controls">
|
||||
<button @click="modifyName" class="btn-primary">
|
||||
修改 name (正确)
|
||||
</button>
|
||||
<button @click="modifyAgeError" class="btn-danger">
|
||||
赋值错误类型
|
||||
</button>
|
||||
<button @click="toggleActive" class="btn-secondary">
|
||||
切换 isActive
|
||||
</button>
|
||||
<button @click="reset" class="btn-ghost">重置</button>
|
||||
</div>
|
||||
|
||||
<!-- 代码对比 -->
|
||||
<div class="code-comparison">
|
||||
<div class="code-panel javascript">
|
||||
<div class="panel-header">JavaScript (无类型检查)</div>
|
||||
<pre><code>let name = "张三"
|
||||
name = 123 // ✅ 运行时才会报错(可能很晚才发现)</code></pre>
|
||||
</div>
|
||||
<div class="code-panel typescript">
|
||||
<div class="panel-header">TypeScript (编译时检查)</div>
|
||||
<pre><code>let name: string = "张三"
|
||||
name = 123 // ❌ 编译时立即报错(写代码时就发现)</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.type-annotation-demo {
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 16px;
|
||||
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);
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.variables-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.variable-card {
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.variable-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-family: 'Courier New', monospace;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.type-badge.string {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.type-badge.number {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.type-badge.boolean {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.var-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-dot.active {
|
||||
background: #10b981;
|
||||
box-shadow: 0 0 8px rgba(16, 185, 129, 0.5);
|
||||
}
|
||||
|
||||
.status-dot.inactive {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.card-code {
|
||||
background: #1e1e1e;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.card-code code {
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #d4d4d4;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.message-box {
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.message-box.error {
|
||||
background: #fef2f2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
.message-box.success {
|
||||
background: #f0fdf4;
|
||||
color: #166534;
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 18px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6b7280;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--vp-c-text-2);
|
||||
border: 1px solid var(--vp-c-border);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: var(--vp-c-bg-soft-hover);
|
||||
}
|
||||
|
||||
.code-comparison {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.code-comparison {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.code-panel {
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.code-panel.javascript {
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
.code-panel.typescript {
|
||||
border-color: #3178c6;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.code-panel.javascript .panel-header {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.code-panel.typescript .panel-header {
|
||||
background: #3178c6;
|
||||
}
|
||||
|
||||
.code-panel pre {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
background: #1e1e1e;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-panel code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,618 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// 类型推断演示
|
||||
const codeExamples = ref([
|
||||
{
|
||||
id: 1,
|
||||
code: 'let name = "张三"',
|
||||
inferredType: 'string',
|
||||
explanation: 'TypeScript 根据赋值的字符串推断出 name 的类型是 string'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
code: 'let age = 25',
|
||||
inferredType: 'number',
|
||||
explanation: 'TypeScript 根据数字字面量推断出 age 的类型是 number'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
code: 'let isActive = true',
|
||||
inferredType: 'boolean',
|
||||
explanation: 'TypeScript 根据布尔值推断出 isActive 的类型是 boolean'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
code: 'let numbers = [1, 2, 3]',
|
||||
inferredType: 'number[]',
|
||||
explanation: 'TypeScript 推断出这是一个数字数组'
|
||||
}
|
||||
])
|
||||
|
||||
const currentExample = ref(codeExamples.value[0])
|
||||
|
||||
// 显示类型错误
|
||||
const showError = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
const setMessage = (msg, isError = false) => {
|
||||
errorMessage.value = msg
|
||||
showError.value = isError
|
||||
setTimeout(() => {
|
||||
errorMessage.value = ''
|
||||
showError.value = false
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// 切换示例
|
||||
const selectExample = (example) => {
|
||||
currentExample.value = example
|
||||
errorMessage.value = ''
|
||||
showError.value = false
|
||||
}
|
||||
|
||||
// 尝试类型错误
|
||||
const tryTypeError = () => {
|
||||
showError.value = true
|
||||
errorMessage.value = `❌ TypeScript 错误:不能将类型 "number" 分配给类型 "${currentExample.value.inferredType}"`
|
||||
setTimeout(() => {
|
||||
showError.value = false
|
||||
errorMessage.value = ''
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// 最佳实践示例
|
||||
const bestPractices = ref([
|
||||
{
|
||||
title: '何时使用类型推断',
|
||||
items: [
|
||||
'变量初始化时有明确的值',
|
||||
'函数返回值可以明显推断',
|
||||
'简单的字面量赋值'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '何时需要显式注解',
|
||||
items: [
|
||||
'函数参数(必须)',
|
||||
'对象或数组的复杂结构',
|
||||
'无法从初始值推断类型',
|
||||
'需要明确的类型约束'
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
// 代码对比
|
||||
const codeComparisons = ref([
|
||||
{
|
||||
scenario: '函数返回值',
|
||||
withInference: 'function add(a: number, b: number) {\n return a + b // 推断为 number\n}',
|
||||
withAnnotation: 'function add(a: number, b: number): number {\n return a + b\n}',
|
||||
recommendation: '推荐使用推断'
|
||||
},
|
||||
{
|
||||
scenario: '复杂对象',
|
||||
withInference: 'const user = {\n name: "张三",\n age: 25,\n email: "test@example.com"\n} // 类型自动推断',
|
||||
withAnnotation: 'interface User {\n name: string\n age: number\n email: string\n}\n\nconst user: User = { ... }',
|
||||
recommendation: '复杂结构建议用接口'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="type-inference-demo">
|
||||
<h3>🔮 类型推断演示</h3>
|
||||
|
||||
<div class="demo-container">
|
||||
<!-- 概念说明 -->
|
||||
<div class="concept-section">
|
||||
<div class="concept-card">
|
||||
<div class="concept-icon">🧠</div>
|
||||
<div class="concept-content">
|
||||
<h4>什么是类型推断?</h4>
|
||||
<p>TypeScript 很聪明,它能根据你写的代码自动推断出变量的类型,不需要每次都手动标注。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 示例选择器 -->
|
||||
<div class="example-selector">
|
||||
<h4>选择一个示例看看类型推断是如何工作的:</h4>
|
||||
<div class="examples-grid">
|
||||
<div
|
||||
v-for="example in codeExamples"
|
||||
:key="example.id"
|
||||
:class="['example-card', { active: currentExample.id === example.id }]"
|
||||
@click="selectExample(example)"
|
||||
>
|
||||
<div class="example-code">{{ example.code }}</div>
|
||||
<div class="example-type">→ {{ example.inferredType }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前示例详情 -->
|
||||
<div class="current-example">
|
||||
<div class="example-display">
|
||||
<div class="code-panel">
|
||||
<div class="panel-header">
|
||||
<span class="code-icon">💻</span>
|
||||
<span>代码</span>
|
||||
</div>
|
||||
<pre><code class="typescript">{{ currentExample.code }}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="inference-arrow">→</div>
|
||||
|
||||
<div class="type-panel">
|
||||
<div class="panel-header">
|
||||
<span class="type-icon">🏷️</span>
|
||||
<span>推断的类型</span>
|
||||
</div>
|
||||
<div class="inferred-type">{{ currentExample.inferredType }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="explanation">
|
||||
<div class="explanation-icon">💡</div>
|
||||
<div class="explanation-text">{{ currentExample.explanation }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误消息 -->
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
:class="['message-box', showError ? 'error' : 'success']"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="controls">
|
||||
<button @click="tryTypeError" class="btn-danger">
|
||||
尝试类型错误
|
||||
</button>
|
||||
<button @click="showError = false; errorMessage = ''" class="btn-secondary">
|
||||
清除消息
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 最佳实践 -->
|
||||
<div class="best-practices">
|
||||
<h4>📚 最佳实践</h4>
|
||||
<div class="practices-grid">
|
||||
<div
|
||||
v-for="(practice, index) in bestPractices"
|
||||
:key="index"
|
||||
class="practice-card"
|
||||
>
|
||||
<div class="practice-header">{{ practice.title }}</div>
|
||||
<ul class="practice-list">
|
||||
<li v-for="(item, i) in practice.items" :key="i">
|
||||
{{ item }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 代码对比 -->
|
||||
<div class="comparisons">
|
||||
<h4>🔄 类型推断 vs 显式注解</h4>
|
||||
<div
|
||||
v-for="(comparison, index) in codeComparisons"
|
||||
:key="index"
|
||||
class="comparison-item"
|
||||
>
|
||||
<div class="comparison-scenario">{{ comparison.scenario }}</div>
|
||||
<div class="comparison-codes">
|
||||
<div class="comparison-code">
|
||||
<div class="code-label">使用推断</div>
|
||||
<pre><code class="typescript">{{ comparison.withInference }}</code></pre>
|
||||
</div>
|
||||
<div class="comparison-code">
|
||||
<div class="code-label">显式注解</div>
|
||||
<pre><code class="typescript">{{ comparison.withAnnotation }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comparison-recommendation">
|
||||
<span class="recommendation-icon">✅</span>
|
||||
{{ comparison.recommendation }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.type-inference-demo {
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin: 24px 0;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
h3, h4 {
|
||||
margin: 0 0 16px 0;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.concept-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.concept-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.concept-icon {
|
||||
font-size: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.concept-content h4 {
|
||||
color: white;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.concept-content p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.example-selector {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.examples-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.example-card {
|
||||
padding: 16px;
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.example-card:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.example-card.active {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
background: #dbeafe;
|
||||
}
|
||||
|
||||
.example-code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.example-type {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.current-example {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.example-display {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.example-display {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.inference-arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.code-panel, .type-panel {
|
||||
flex: 1;
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 12px 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.code-icon, .type-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.code-panel pre {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #1e1e1e;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-panel code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.inference-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
color: var(--vp-c-brand-1);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.type-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.inferred-type {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand-1);
|
||||
background: #dbeafe;
|
||||
}
|
||||
|
||||
.explanation {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.explanation-icon {
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.explanation-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.message-box {
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.message-box.error {
|
||||
background: #fef2f2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
.message-box.success {
|
||||
background: #f0fdf4;
|
||||
color: #166534;
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 18px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
border: 1px solid var(--vp-c-border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--vp-c-bg-soft-hover);
|
||||
}
|
||||
|
||||
.best-practices {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.practices-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.practice-card {
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.practice-header {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.practice-list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.practice-list li {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.comparisons {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.comparison-item {
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.comparison-scenario {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.comparison-codes {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.comparison-codes {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.comparison-code {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.code-label {
|
||||
padding: 10px 16px;
|
||||
background: #374151;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.comparison-code pre {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.comparison-code code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.comparison-recommendation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: #f0fdf4;
|
||||
border-radius: 6px;
|
||||
color: #166534;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.recommendation-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -494,6 +494,19 @@ import ThisContextDemo from './components/appendix/javascript-intro/ThisContextD
|
||||
import PrototypeDemo from './components/appendix/javascript-intro/PrototypeDemo.vue'
|
||||
import AsyncDemo from './components/appendix/javascript-intro/AsyncDemo.vue'
|
||||
|
||||
// JavaScript Runtime Components
|
||||
import RuntimeEnvironmentDemo from './components/appendix/js-runtime/RuntimeEnvironmentDemo.vue'
|
||||
import CallStackDemo from './components/appendix/js-runtime/CallStackDemo.vue'
|
||||
import TaskQueueDemo from './components/appendix/js-runtime/TaskQueueDemo.vue'
|
||||
import MemoryLeakDemo from './components/appendix/js-runtime/MemoryLeakDemo.vue'
|
||||
import GarbageCollectionDemo from './components/appendix/js-runtime/GarbageCollectionDemo.vue'
|
||||
|
||||
// TypeScript Intro Components
|
||||
import TypeAnnotationDemo from './components/appendix/typescript-intro/TypeAnnotationDemo.vue'
|
||||
import InterfaceDemo from './components/appendix/typescript-intro/InterfaceDemo.vue'
|
||||
import GenericDemo from './components/appendix/typescript-intro/GenericDemo.vue'
|
||||
import TypeInferenceDemo from './components/appendix/typescript-intro/TypeInferenceDemo.vue'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout,
|
||||
@@ -935,6 +948,7 @@ export default {
|
||||
app.component('MacroMicroTaskDemo', MacroMicroTaskDemo)
|
||||
app.component('RenderingPerformanceDemo', RenderingPerformanceDemo)
|
||||
app.component('RenderingPipelineDemo', RenderingPipelineDemo)
|
||||
app.component('EventLoopDemo', JSEventLoopDemo) // Alias for browser rendering context
|
||||
|
||||
// Cache Design Extra Components Registration
|
||||
app.component('CacheArchitectureOverview', CacheArchitectureOverview)
|
||||
@@ -986,6 +1000,19 @@ export default {
|
||||
app.component('DOMTreeDemo', DOMTreeDemo)
|
||||
app.component('AsyncRestaurantDemo', AsyncRestaurantDemo)
|
||||
app.component('JSEventLoopDemo', JSEventLoopDemo)
|
||||
|
||||
// JavaScript Runtime Components Registration
|
||||
app.component('RuntimeEnvironmentDemo', RuntimeEnvironmentDemo)
|
||||
app.component('CallStackDemo', CallStackDemo)
|
||||
app.component('TaskQueueDemo', TaskQueueDemo)
|
||||
app.component('MemoryLeakDemo', MemoryLeakDemo)
|
||||
app.component('GarbageCollectionDemo', GarbageCollectionDemo)
|
||||
|
||||
// TypeScript Intro Components Registration
|
||||
app.component('TypeAnnotationDemo', TypeAnnotationDemo)
|
||||
app.component('InterfaceDemo', InterfaceDemo)
|
||||
app.component('GenericDemo', GenericDemo)
|
||||
app.component('TypeInferenceDemo', TypeInferenceDemo)
|
||||
},
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
|
||||
Reference in New Issue
Block a user