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

refactor(demos): improve variable box, scope, and type annotation demos
style(demos): update visual styles and animations for better UX
docs(browser-as-os): restructure content with tables and practical examples
feat(demos): add new TypeScript and runtime environment demos
This commit is contained in:
sanbuphy
2026-02-17 01:39:59 +08:00
parent 9ee3312569
commit 47377646df
20 changed files with 8097 additions and 1310 deletions
@@ -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>浏览器运行时专注于用户界面和网页交互,提供 DOMBOMfetch 等前端专用 API</p>
<p>Node.js 运行时专注于服务器端开发,提供文件系统HTTP 服务器进程管理等后端专用 API</p>
<p class="highlight">同样的 JavaScript 语法,但能用的 API 完全不同这就是"环境判断"的重要性</p>
</div>
</div>
</template>
<style scoped>
.runtime-environment-demo {
border: 1px solid var(--vp-c-border);
border-radius: 12px;
padding: 24px;
margin: 24px 0;
background: var(--vp-c-bg);
}
h3 {
margin: 0 0 20px 0;
font-size: 18px;
font-weight: 600;
color: var(--vp-c-text-1);
}
h4 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.tab-container {
margin-bottom: 24px;
}
.tabs {
display: flex;
gap: 12px;
margin-bottom: 20px;
border-bottom: 2px solid var(--vp-c-border);
}
.tab-btn {
padding: 12px 24px;
border: none;
background: transparent;
color: var(--vp-c-text-2);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
}
.tab-btn:hover {
color: var(--vp-c-brand-1);
}
.tab-btn.active {
color: var(--vp-c-brand-1);
border-bottom-color: var(--vp-c-brand-1);
}
.tab-icon {
font-size: 18px;
margin-right: 8px;
}
.tab-label {
font-size: 14px;
}
.tab-content {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.api-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.api-card {
padding: 16px;
border: 2px solid var(--vp-c-border);
border-radius: 8px;
background: var(--vp-c-bg);
transition: all 0.2s ease;
}
.api-card:hover {
border-color: var(--vp-c-brand-1);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.api-name {
font-size: 16px;
font-weight: 600;
color: var(--vp-c-brand-1);
margin-bottom: 8px;
font-family: 'Courier New', monospace;
}
.api-description {
font-size: 13px;
color: var(--vp-c-text-2);
margin-bottom: 8px;
}
.api-example {
font-size: 12px;
color: var(--vp-c-text-3);
font-family: 'Courier New', monospace;
padding: 8px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
}
.environment-note {
padding: 16px;
background: rgba(62, 175, 124, 0.1);
border-left: 4px solid var(--vp-c-brand-1);
border-radius: 8px;
}
.environment-note strong {
display: block;
margin-bottom: 8px;
color: var(--vp-c-text-1);
}
.environment-note ul {
list-style: none;
padding: 0;
margin: 0;
}
.environment-note li {
padding: 4px 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.code-comparison-section {
border-top: 2px solid var(--vp-c-border);
padding-top: 24px;
}
.code-input {
margin-bottom: 20px;
}
.code-input label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.code-input-field {
width: 100%;
padding: 12px;
border: 2px solid var(--vp-c-border);
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 14px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
transition: border-color 0.2s ease;
}
.code-input-field:focus {
outline: none;
border-color: var(--vp-c-brand-1);
}
.result-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.result-grid {
grid-template-columns: 1fr;
}
}
.result-card {
border: 2px solid var(--vp-c-border);
border-radius: 8px;
padding: 16px;
background: var(--vp-c-bg);
}
.result-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--vp-c-border);
}
.result-icon {
font-size: 20px;
}
.result-title {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.result-content {
min-height: 60px;
padding: 12px;
margin-bottom: 12px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 13px;
color: var(--vp-c-text-1);
word-break: break-all;
}
.run-btn {
width: 100%;
padding: 10px;
border: none;
border-radius: 6px;
background: var(--vp-c-brand-1);
color: white;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.run-btn:hover:not(:disabled) {
background: var(--vp-c-brand-2);
transform: translateY(-1px);
}
.run-btn:disabled {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-3);
cursor: not-allowed;
}
.reset-btn {
padding: 10px 24px;
border: 2px solid var(--vp-c-border);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.reset-btn:hover {
background: var(--vp-c-bg-soft);
border-color: var(--vp-c-brand-1);
}
.summary-box {
background: var(--vp-c-bg-soft);
border-left: 4px solid var(--vp-c-brand-1);
border-radius: 8px;
padding: 16px;
margin-top: 24px;
}
.summary-box p {
margin: 0 0 12px 0;
font-size: 14px;
line-height: 1.6;
color: var(--vp-c-text-1);
}
.summary-box p:last-child {
margin-bottom: 0;
}
.summary-box strong {
color: var(--vp-c-brand-1);
}
.summary-box .highlight {
padding: 12px;
background: rgba(62, 175, 124, 0.1);
border-radius: 6px;
font-weight: 500;
color: var(--vp-c-brand-1);
}
</style>
@@ -0,0 +1,652 @@
<script setup>
import { ref } from 'vue'
const isAnimating = ref(false)
const currentStep = ref(0)
const syncCode = ref([
{ id: 1, code: 'console.log("1")', type: 'sync', output: '1' },
{ id: 2, code: 'setTimeout(() => console.log("2"), 0)', type: 'macro', output: '2' },
{ id: 3, code: 'Promise.resolve().then(() => console.log("3"))', type: 'micro', output: '3' },
{ id: 4, code: 'console.log("4")', type: 'sync', output: '4' },
{ id: 5, code: 'setTimeout(() => console.log("5"), 0)', type: 'macro', output: '5' }
])
const microTaskQueue = ref([])
const macroTaskQueue = ref([])
const outputLog = ref([])
const executionSteps = [
{ description: '执行 console.log("1")', action: 'execute', output: '1', source: '同步' },
{ description: '遇到 setTimeout,将回调加入宏任务队列', action: 'add-macro', task: 'console.log("2")' },
{ description: '遇到 Promise.then,将回调加入微任务队列', action: 'add-micro', task: 'console.log("3")' },
{ description: '执行 console.log("4")', action: 'execute', output: '4', source: '同步' },
{ description: '遇到 setTimeout,将回调加入宏任务队列', action: 'add-macro', task: 'console.log("5")' },
{ description: '同步代码执行完毕,检查微任务队列', action: 'check-micro' },
{ description: '执行微任务: console.log("3")', action: 'execute-micro', output: '3', source: '微任务' },
{ description: '微任务队列为空,检查宏任务队列', action: 'check-macro' },
{ description: '执行宏任务: console.log("2")', action: 'execute-macro', output: '2', source: '宏任务' },
{ description: '检查微任务队列(空)', action: 'check-micro' },
{ description: '执行宏任务: console.log("5")', action: 'execute-macro', output: '5', source: '宏任务' },
{ description: '所有任务执行完毕', action: 'done' }
]
const reset = () => {
currentStep.value = 0
microTaskQueue.value = []
macroTaskQueue.value = []
outputLog.value = []
isAnimating.value = false
}
const nextStep = () => {
if (currentStep.value >= executionSteps.length) return
const step = executionSteps[currentStep.value]
switch (step.action) {
case 'execute':
outputLog.value.push({ output: step.output, source: step.source })
break
case 'add-macro':
macroTaskQueue.value.push({ code: step.task, status: 'pending' })
break
case 'add-micro':
microTaskQueue.value.push({ code: step.task, status: 'pending' })
break
case 'check-micro':
if (microTaskQueue.value.length > 0) {
microTaskQueue.value[0].status = 'ready'
}
break
case 'execute-micro':
if (microTaskQueue.value.length > 0) {
outputLog.value.push({ output: step.output, source: step.source })
microTaskQueue.value.shift()
}
break
case 'check-macro':
if (macroTaskQueue.value.length > 0) {
macroTaskQueue.value[0].status = 'ready'
}
break
case 'execute-macro':
if (macroTaskQueue.value.length > 0) {
outputLog.value.push({ output: step.output, source: step.source })
macroTaskQueue.value.shift()
}
break
}
currentStep.value++
}
const play = async () => {
if (isAnimating.value) return
isAnimating.value = true
reset()
while (currentStep.value < executionSteps.length && isAnimating.value) {
nextStep()
await new Promise(resolve => setTimeout(resolve, 1000))
}
isAnimating.value = false
}
const stop = () => {
isAnimating.value = false
}
</script>
<template>
<div class="task-queue-demo">
<h3>任务队列:宏任务 vs 微任务</h3>
<!-- 代码展示 -->
<div class="code-section">
<h4>代码示例</h4>
<div class="code-display">
<div
v-for="(item, index) in syncCode"
:key="item.id"
class="code-item"
:class="{
'current': currentStep === index,
'executed': currentStep > index && index < 4
}"
>
<span class="item-number">{{ item.id }}</span>
<span class="item-code" :class="`type-${item.type}`">{{ item.code }}</span>
<span v-if="item.type === 'sync'" class="item-tag">同步</span>
<span v-else-if="item.type === 'micro'" class="item-tag micro">微任务</span>
<span v-else-if="item.type === 'macro'" class="item-tag macro">宏任务</span>
</div>
</div>
</div>
<!-- 执行过程可视化 -->
<div class="visualization">
<!-- 调用栈 -->
<div class="stack-panel">
<h4>调用栈 (正在执行)</h4>
<div class="stack-content">
<div v-if="currentStep < executionSteps.length" class="current-action">
{{ executionSteps[currentStep]?.description }}
</div>
<div v-else class="current-action done">
执行完成
</div>
</div>
</div>
<!-- 微任务队列 -->
<div class="queue-panel micro">
<h4>
微任务队列
<span class="badge">Microtask</span>
</h4>
<div class="queue-content">
<transition-group name="task-item">
<div
v-for="(task, index) in microTaskQueue"
:key="`micro-${index}`"
class="task-item micro"
:class="{ 'ready': task.status === 'ready' }"
>
<div class="task-code">{{ task.code }}</div>
<div v-if="task.status === 'ready'" class="task-status"> 就绪</div>
<div v-else class="task-status"> 等待</div>
</div>
</transition-group>
<div v-if="microTaskQueue.length === 0" class="empty-queue">
队列为空
</div>
</div>
</div>
<!-- 宏任务队列 -->
<div class="queue-panel macro">
<h4>
宏任务队列
<span class="badge">Macrotask</span>
</h4>
<div class="queue-content">
<transition-group name="task-item">
<div
v-for="(task, index) in macroTaskQueue"
:key="`macro-${index}`"
class="task-item macro"
:class="{ 'ready': task.status === 'ready' }"
>
<div class="task-code">{{ task.code }}</div>
<div v-if="task.status === 'ready'" class="task-status"> 就绪</div>
<div v-else class="task-status"> 等待</div>
</div>
</transition-group>
<div v-if="macroTaskQueue.length === 0" class="empty-queue">
队列为空
</div>
</div>
</div>
</div>
<!-- 输出日志 -->
<div class="output-section">
<h4>输出日志 (执行顺序)</h4>
<div class="output-log">
<div v-if="outputLog.length === 0" class="empty-log">
等待输出...
</div>
<transition-group name="output">
<div
v-for="(log, index) in outputLog"
:key="`log-${index}`"
class="log-entry"
>
<span class="log-output">{{ log.output }}</span>
<span class="log-source">({{ log.source }})</span>
</div>
</transition-group>
</div>
</div>
<!-- 控制按钮 -->
<div class="controls">
<button @click="play" :disabled="isAnimating" class="btn-play">
{{ isAnimating ? '执行中...' : '▶ 自动演示' }}
</button>
<button @click="nextStep" :disabled="isAnimating || currentStep >= executionSteps.length" class="btn-step">
单步执行
</button>
<button @click="stop" :disabled="!isAnimating" class="btn-stop">
停止
</button>
<button @click="reset" :disabled="isAnimating" class="btn-reset">
🔄 重置
</button>
</div>
<!-- 执行规则 -->
<div class="rules-box">
<h4>执行顺序规则</h4>
<div class="rule-list">
<div class="rule-item">
<span class="rule-number">1</span>
<span class="rule-text">执行所有同步代码</span>
</div>
<div class="rule-item">
<span class="rule-number">2</span>
<span class="rule-text">执行微任务队列中的所有任务</span>
</div>
<div class="rule-item">
<span class="rule-number">3</span>
<span class="rule-text">执行一个宏任务</span>
</div>
<div class="rule-item">
<span class="rule-number">4</span>
<span class="rule-text">重复步骤 2-3</span>
</div>
</div>
<p class="highlight">
<strong>核心要点:</strong> 微任务优先级高于宏任务每次执行完一个宏任务后,都会检查并执行所有微任务,然后再执行下一个宏任务
</p>
</div>
</div>
</template>
<style scoped>
.task-queue-demo {
border: 1px solid var(--vp-c-border);
border-radius: 12px;
padding: 24px;
margin: 24px 0;
background: var(--vp-c-bg);
}
h3 {
margin: 0 0 20px 0;
font-size: 18px;
font-weight: 600;
color: var(--vp-c-text-1);
}
h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
}
.code-section {
margin-bottom: 20px;
}
.code-display {
background: #1e1e1e;
border-radius: 8px;
padding: 16px;
font-family: 'Courier New', monospace;
}
.code-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px;
border-radius: 4px;
transition: all 0.3s ease;
}
.code-item.current {
background: rgba(62, 175, 124, 0.2);
border-left: 3px solid var(--vp-c-brand-1);
}
.code-item.executed {
opacity: 0.5;
}
.item-number {
color: #858585;
font-size: 12px;
min-width: 20px;
}
.item-code {
flex: 1;
color: #d4d4d4;
font-size: 13px;
}
.item-code.type-micro {
color: #68d391;
}
.item-code.type-macro {
color: #f687b3;
}
.item-tag {
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
}
.item-tag.micro {
background: rgba(104, 217, 145, 0.2);
color: #68d391;
}
.item-tag.macro {
background: rgba(246, 135, 179, 0.2);
color: #f687b3;
}
.visualization {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
margin-bottom: 20px;
}
@media (max-width: 1024px) {
.visualization {
grid-template-columns: 1fr;
}
}
.stack-panel,
.queue-panel {
border: 2px solid var(--vp-c-border);
border-radius: 8px;
padding: 16px;
background: var(--vp-c-bg-soft);
min-height: 250px;
}
.queue-panel.micro {
border-color: #68d391;
}
.queue-panel.macro {
border-color: #f687b3;
}
.badge {
margin-left: 8px;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
}
.stack-content,
.queue-content {
min-height: 200px;
}
.current-action {
padding: 16px;
background: var(--vp-c-bg);
border-radius: 8px;
border-left: 4px solid var(--vp-c-brand-1);
font-size: 14px;
color: var(--vp-c-text-1);
line-height: 1.6;
}
.current-action.done {
border-color: #48bb78;
text-align: center;
font-weight: 600;
}
.task-item {
padding: 12px;
margin-bottom: 8px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-border);
border-radius: 6px;
transition: all 0.3s ease;
}
.task-item.micro {
border-color: #68d391;
}
.task-item.macro {
border-color: #f687b3;
}
.task-item.ready {
animation: pulse 1s ease infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(104, 217, 145, 0.4); }
50% { box-shadow: 0 0 0 6px rgba(104, 217, 145, 0); }
}
.task-code {
font-family: 'Courier New', monospace;
font-size: 12px;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.task-status {
font-size: 11px;
font-weight: 600;
color: var(--vp-c-text-2);
}
.task-item-enter-active,
.task-item-leave-active {
transition: all 0.3s ease;
}
.task-item-enter-from {
opacity: 0;
transform: translateX(-20px);
}
.task-item-leave-to {
opacity: 0;
transform: translateX(20px);
}
.empty-queue {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--vp-c-text-3);
font-size: 13px;
}
.output-section {
margin-bottom: 20px;
}
.output-log {
min-height: 60px;
padding: 12px;
border: 2px solid var(--vp-c-border);
border-radius: 8px;
background: var(--vp-c-bg);
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.empty-log {
color: var(--vp-c-text-3);
font-size: 14px;
}
.log-entry {
padding: 8px 12px;
background: var(--vp-c-brand-1);
color: white;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.log-output {
font-family: 'Courier New', monospace;
}
.log-source {
margin-left: 8px;
font-size: 12px;
opacity: 0.8;
}
.output-enter-active,
.output-leave-active {
transition: all 0.3s ease;
}
.output-enter-from {
opacity: 0;
transform: translateY(-10px);
}
.output-leave-to {
opacity: 0;
transform: translateY(10px);
}
.controls {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
button:active {
transform: scale(0.95);
}
.btn-play {
background: var(--vp-c-brand-1);
color: white;
}
.btn-play:hover:not(:disabled) {
background: var(--vp-c-brand-2);
}
.btn-step {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.btn-step:hover:not(:disabled) {
background: var(--vp-c-bg-soft-hover);
}
.btn-stop {
background: #ed8936;
color: white;
}
.btn-stop:hover:not(:disabled) {
background: #dd6b20;
}
.btn-reset {
background: #f56565;
color: white;
}
.btn-reset:hover:not(:disabled) {
background: #e53e3e;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.rules-box {
background: var(--vp-c-bg-soft);
border-left: 4px solid var(--vp-c-brand-1);
border-radius: 8px;
padding: 16px;
}
.rule-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
}
.rule-item {
display: flex;
align-items: center;
gap: 12px;
}
.rule-number {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--vp-c-brand-1);
color: white;
font-size: 14px;
font-weight: 600;
flex-shrink: 0;
}
.rule-text {
font-size: 14px;
color: var(--vp-c-text-1);
}
.highlight {
padding: 12px;
background: rgba(62, 175, 124, 0.1);
border-radius: 6px;
font-size: 14px;
line-height: 1.6;
color: var(--vp-c-text-1);
margin: 0;
}
.highlight strong {
color: var(--vp-c-brand-1);
}
</style>
@@ -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&lt;T&gt;(arg: T): T {
return arg
}
// 泛型数组反转
function reverseArray&lt;T&gt;(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&lt;number&gt;(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&lt;string&gt;(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>