feat(docs): enhance JavaScript runtime and browser-as-os content
refactor(demos): improve variable box, scope, and type annotation demos style(demos): update visual styles and animations for better UX docs(browser-as-os): restructure content with tables and practical examples feat(demos): add new TypeScript and runtime environment demos
This commit is contained in:
@@ -76,15 +76,19 @@
|
|||||||
<div class="code-line">obj2.x = 20</div>
|
<div class="code-line">obj2.x = 20</div>
|
||||||
<div class="code-line result">// obj1.x = 20 (变了!)</div>
|
<div class="code-line result">// obj1.x = 20 (变了!)</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="visual-box">
|
<div class="visual-box ref-visual">
|
||||||
<div class="ref-box">
|
<div class="ref-boxes">
|
||||||
<div>obj1 →</div>
|
<div class="ref-var-box">
|
||||||
<div class="memory-box">{x: 20}</div>
|
<div class="ref-var-name">obj1</div>
|
||||||
</div>
|
<div class="ref-var-arrow">→</div>
|
||||||
<div class="arrow">指向同一位置</div>
|
</div>
|
||||||
<div class="ref-box">
|
<div class="ref-var-box">
|
||||||
<div>obj2 →</div>
|
<div class="ref-var-name">obj2</div>
|
||||||
|
<div class="ref-var-arrow">→</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="arrow down-arrow">指向同一位置</div>
|
||||||
|
<div class="memory-box">{x: 20}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -469,6 +473,40 @@ const convertType = () => {
|
|||||||
text-align: center;
|
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 {
|
.ref-types-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,140 +1,106 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
// 基本类型
|
const basicStep = ref(0)
|
||||||
const basicA = ref(10)
|
const basicA = ref(10)
|
||||||
const basicB = ref(null)
|
const basicB = ref(null)
|
||||||
const basicStep = ref(0)
|
|
||||||
const basicMessage = ref('')
|
|
||||||
|
|
||||||
// 引用类型
|
const refStep = ref(0)
|
||||||
const obj1Data = ref({ name: '张三', age: 25 })
|
const objData = ref({ age: 25 })
|
||||||
const obj2Exists = ref(false)
|
|
||||||
const obj2Age = ref('')
|
|
||||||
const refMessage = ref('')
|
|
||||||
|
|
||||||
const basicCopy = () => {
|
const basicCopy = () => { basicB.value = basicA.value; basicStep.value = 1 }
|
||||||
basicB.value = basicA.value
|
const basicModify = () => { basicB.value = 20; basicStep.value = 2 }
|
||||||
basicStep.value = 1
|
const basicReset = () => { basicStep.value = 0; basicB.value = null }
|
||||||
basicMessage.value = '✅ 基本类型复制的是值本身'
|
|
||||||
}
|
|
||||||
|
|
||||||
const basicModify = () => {
|
const refCopy = () => { refStep.value = 1 }
|
||||||
if (basicB.value === null) {
|
const refModify = () => { objData.value.age = 30; refStep.value = 2 }
|
||||||
basicMessage.value = '⚠️ 请先复制'
|
const refSpread = () => { refStep.value = 3 }
|
||||||
return
|
const refReset = () => { refStep.value = 0; objData.value.age = 25 }
|
||||||
}
|
|
||||||
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 = '✅ 用展开运算符创建真正的副本,现在互不影响'
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="reference-demo">
|
<div class="reference-demo">
|
||||||
<h3>值 vs 引用</h3>
|
<div class="demo-title">🔄 值 vs 引用</div>
|
||||||
|
|
||||||
<div class="demo-container">
|
<div class="compare-grid">
|
||||||
<!-- 左侧:基本类型 -->
|
<!-- 左侧:基本类型 -->
|
||||||
<div class="demo-section basic-section">
|
<div class="compare-box">
|
||||||
<h4>基本类型(复制值)</h4>
|
<div class="box-header blue">基本类型(复制值)</div>
|
||||||
|
|
||||||
<div class="visualization">
|
<div class="memory-area">
|
||||||
<div class="box" :class="{ 'active': basicA !== null }">
|
<div class="vars-row">
|
||||||
<div class="box-label">a</div>
|
<div class="var-item" :class="{ active: basicStep >= 0 }">
|
||||||
<div class="box-value">{{ basicA }}</div>
|
<span class="var-label">a</span>
|
||||||
</div>
|
<span class="var-val">{{ basicA }}</span>
|
||||||
|
</div>
|
||||||
<div class="arrow" v-if="basicStep >= 1">
|
<div class="var-item" :class="{ active: basicStep >= 1, changed: basicStep >= 2 }">
|
||||||
<div class="arrow-line"></div>
|
<span class="var-label">b</span>
|
||||||
<div class="arrow-head">→</div>
|
<span class="var-val">{{ basicB ?? '?' }}</span>
|
||||||
</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>
|
</div>
|
||||||
|
<div class="copy-arrow" v-if="basicStep >= 1">↓ 复制值</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message" v-if="basicMessage">{{ basicMessage }}</div>
|
<div class="result-text" :class="basicStep === 2 ? 'success' : 'info'">
|
||||||
|
{{ basicStep === 0 ? '点击复制' : basicStep === 1 ? 'b 得到 10' : '✅ 修改 b 不影响 a' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="btn-group">
|
||||||
<button @click="basicCopy" class="btn-primary">let b = a(复制)</button>
|
<button @click="basicCopy" :disabled="basicStep >= 1">复制</button>
|
||||||
<button @click="basicModify" class="btn-secondary">b = 20</button>
|
<button @click="basicModify" :disabled="basicStep !== 1">改 b</button>
|
||||||
|
<button @click="basicReset" class="reset">重置</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧:引用类型 -->
|
<!-- 右侧:引用类型 -->
|
||||||
<div class="demo-section reference-section">
|
<div class="compare-box">
|
||||||
<h4>引用类型(复制地址)</h4>
|
<div class="box-header orange">引用类型(复制地址)</div>
|
||||||
|
|
||||||
<div class="visualization">
|
<div class="memory-area">
|
||||||
<!-- 数据区 -->
|
<div class="vars-row">
|
||||||
<div class="data-area">
|
<div class="var-item" :class="{ active: refStep >= 0 }">
|
||||||
<div class="data-label">数据区</div>
|
<span class="var-label">obj1</span>
|
||||||
<div class="data-content">
|
<span class="var-addr">0x001</span>
|
||||||
<div>{{ `{ name: "${obj1Data.name}", age: ${obj1Data.age} }` }}</div>
|
</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>
|
</div>
|
||||||
|
<div class="data-box" :class="{ changed: refStep === 2, copied: refStep === 3 }">
|
||||||
<div class="pointers">
|
<div class="data-addr">0x001</div>
|
||||||
<div class="pointer">
|
<div class="data-content">{ age: {{ objData.age }} }</div>
|
||||||
<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>
|
</div>
|
||||||
|
<div class="copy-arrow" v-if="refStep >= 1">指向同一地址</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message" v-if="refMessage">{{ refMessage }}</div>
|
<div class="result-text" :class="refStep === 2 ? 'warning' : refStep === 3 ? 'success' : 'info'">
|
||||||
|
{{ refStep === 0 ? '点击复制' : refStep === 1 ? '共享地址' : refStep === 2 ? '⚠️ 一改全变' : '✅ 已分离' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="btn-group">
|
||||||
<button @click="refCopy" class="btn-primary">let obj2 = obj1(复制)</button>
|
<button @click="refCopy" :disabled="refStep >= 1">复制</button>
|
||||||
<button @click="refModify" class="btn-danger">obj2.age = 30</button>
|
<button @click="refModify" :disabled="refStep !== 1">修改</button>
|
||||||
<button @click="refSpreadCopy" class="btn-success">用展开运算符创建副本</button>
|
<button @click="refSpread" :disabled="refStep !== 2">展开</button>
|
||||||
|
<button @click="refReset" class="reset">重置</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="code-display">
|
<div class="code-compare">
|
||||||
<h4>代码</h4>
|
<div class="code-col">
|
||||||
<pre><code>// 基本类型
|
<div class="code-title">基本类型</div>
|
||||||
let a = {{ basicA }}
|
<pre><code>let a = 10
|
||||||
{{ basicB !== null ? `let b = ${basicB}` : '' }}
|
let b = a // b=10
|
||||||
{{ basicB !== null ? `b = ${basicB}` : '' }}
|
b = 20 // a还是10</code></pre>
|
||||||
console.log(a) // {{ basicA }}
|
</div>
|
||||||
|
<div class="code-col">
|
||||||
// 引用类型
|
<div class="code-title">引用类型</div>
|
||||||
let obj1 = { name: "{{ obj1Data.name }}", age: {{ obj1Data.age }} }
|
<pre><code>let obj1 = {age:25}
|
||||||
{{ obj2Exists ? 'let obj2 = obj1 // 指向同一份数据' : '' }}
|
let obj2 = obj1
|
||||||
{{ obj2Exists ? `obj2.age = ${obj1Data.age}` : '' }}
|
obj2.age=30 // obj1也变了!
|
||||||
console.log(obj1.age) // {{ obj1Data.age }}
|
// 用 {...obj1} 复制</code></pre>
|
||||||
</code></pre>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -143,235 +109,221 @@ console.log(obj1.age) // {{ obj1Data.age }}
|
|||||||
.reference-demo {
|
.reference-demo {
|
||||||
border: 1px solid var(--vp-c-border);
|
border: 1px solid var(--vp-c-border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 24px;
|
padding: 20px;
|
||||||
margin: 24px 0;
|
margin: 16px 0;
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
.demo-title {
|
||||||
margin: 0 0 20px 0;
|
font-size: 16px;
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
.compare-grid {
|
||||||
margin: 0 0 16px 0;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--vp-c-text-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-container {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 24px;
|
gap: 16px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.demo-container {
|
.compare-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-section {
|
.compare-box {
|
||||||
border: 1px dashed var(--vp-c-border);
|
border: 1px solid var(--vp-c-border);
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
padding: 20px;
|
padding: 16px;
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.visualization {
|
.box-header {
|
||||||
display: flex;
|
font-size: 13px;
|
||||||
align-items: center;
|
font-weight: 600;
|
||||||
justify-content: center;
|
padding: 6px 10px;
|
||||||
gap: 20px;
|
border-radius: 6px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 12px;
|
||||||
min-height: 120px;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
.box-header.blue {
|
||||||
width: 80px;
|
background: #3b82f6;
|
||||||
height: 80px;
|
}
|
||||||
border: 2px solid var(--vp-c-border);
|
|
||||||
border-radius: 8px;
|
.box-header.orange {
|
||||||
display: flex;
|
background: #f59e0b;
|
||||||
flex-direction: column;
|
}
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
.memory-area {
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
transition: all 0.3s ease;
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box.active {
|
.vars-row {
|
||||||
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 {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
gap: 12px;
|
||||||
gap: 4px;
|
justify-content: center;
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-content {
|
.var-item {
|
||||||
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 {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 24px;
|
flex-direction: column;
|
||||||
margin-top: 12px;
|
align-items: center;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
opacity: 0.4;
|
||||||
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pointer {
|
.var-item.active {
|
||||||
text-align: center;
|
opacity: 1;
|
||||||
|
border-color: #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pointer-label {
|
.var-item.changed {
|
||||||
font-size: 12px;
|
border-color: #10b981;
|
||||||
font-weight: 600;
|
background: #ecfdf5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.var-label {
|
||||||
|
font-size: 11px;
|
||||||
color: var(--vp-c-text-2);
|
color: var(--vp-c-text-2);
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow-line-down {
|
.var-val, .var-addr {
|
||||||
width: 2px;
|
font-size: 18px;
|
||||||
height: 30px;
|
font-weight: 600;
|
||||||
background: var(--vp-c-brand-1);
|
font-family: monospace;
|
||||||
margin: 0 auto;
|
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;
|
text-align: center;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
margin-bottom: 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;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
.btn-group button {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 4px;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
background: #3b82f6;
|
||||||
}
|
|
||||||
|
|
||||||
button:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: var(--vp-c-brand-1);
|
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-group button:disabled {
|
||||||
background: var(--vp-c-brand-2);
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-group button.reset {
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-alt);
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
|
border: 1px solid var(--vp-c-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.code-compare {
|
||||||
background: var(--vp-c-bg-soft-hover);
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.code-col {
|
||||||
background: #f56565;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger:hover {
|
|
||||||
background: #e53e3e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success {
|
|
||||||
background: #38a169;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success:hover {
|
|
||||||
background: #2f855a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-display {
|
|
||||||
background: #1e1e1e;
|
background: #1e1e1e;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 12px;
|
||||||
overflow-x: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display h4 {
|
.code-title {
|
||||||
color: #d4d4d4;
|
color: #9ca3af;
|
||||||
margin-bottom: 12px;
|
font-size: 11px;
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display pre {
|
.code-col pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display code {
|
.code-col code {
|
||||||
font-family: 'Courier New', monospace;
|
font-family: monospace;
|
||||||
font-size: 13px;
|
font-size: 11px;
|
||||||
line-height: 1.6;
|
line-height: 1.5;
|
||||||
color: #d4d4d4;
|
color: #d4d4d4;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const activeScope = ref('block') // 'global', 'function', 'block'
|
const activeScope = ref('global')
|
||||||
const explanation = ref('')
|
const explanation = ref('')
|
||||||
|
|
||||||
const scopes = [
|
const scopes = [
|
||||||
@@ -9,31 +9,36 @@ const scopes = [
|
|||||||
id: 'global',
|
id: 'global',
|
||||||
name: '全局作用域',
|
name: '全局作用域',
|
||||||
color: '#a0aec0',
|
color: '#a0aec0',
|
||||||
variables: ['appName = "Todo"'],
|
vars: [{ name: 'appName', value: '"Todo"', own: true }]
|
||||||
canSee: ['appName']
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'function',
|
id: 'function',
|
||||||
name: '函数 greet() 作用域',
|
name: '函数 greet() 作用域',
|
||||||
color: '#4299e1',
|
color: '#4299e1',
|
||||||
variables: ['message = "你好"'],
|
vars: [
|
||||||
canSee: ['appName', 'message']
|
{ name: 'appName', value: '"Todo"', own: false, from: '全局' },
|
||||||
|
{ name: 'message', value: '"你好"', own: true }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'block',
|
id: 'block',
|
||||||
name: 'if 块作用域',
|
name: 'if 块作用域',
|
||||||
color: '#38a169',
|
color: '#38a169',
|
||||||
variables: ['greeting = message + appName'],
|
vars: [
|
||||||
canSee: ['appName', 'message', 'greeting']
|
{ name: 'appName', value: '"Todo"', own: false, from: '全局' },
|
||||||
|
{ name: 'message', value: '"你好"', own: false, from: '函数' },
|
||||||
|
{ name: 'greeting', value: 'message+appName', own: true }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const updateExplanation = () => {
|
const updateExplanation = () => {
|
||||||
const scope = scopes.find(s => s.id === activeScope.value)
|
const texts = {
|
||||||
if (scope) {
|
global: '在全局作用域,只能使用全局变量 appName',
|
||||||
const visible = scope.canSee.map(v => `✅ ${v}`).join('、')
|
function: '在函数作用域,可以使用自己的 message 和全局的 appName(作用域链查找)',
|
||||||
explanation.value = `在这个位置,你能使用这些变量:${visible}`
|
block: '在块级作用域,可以使用自己的 greeting,以及外层的 message 和 appName'
|
||||||
}
|
}
|
||||||
|
explanation.value = texts[activeScope.value]
|
||||||
}
|
}
|
||||||
|
|
||||||
updateExplanation()
|
updateExplanation()
|
||||||
@@ -41,67 +46,54 @@ updateExplanation()
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="scope-demo">
|
<div class="scope-demo">
|
||||||
<h3>作用域:变量的"可见范围"</h3>
|
<h3>🔍 作用域:变量的"可见范围"</h3>
|
||||||
|
|
||||||
<div class="scopes-container">
|
<div class="scope-selector">
|
||||||
<!-- 全局作用域 -->
|
<button
|
||||||
<div
|
v-for="scope in scopes"
|
||||||
class="scope global-scope"
|
:key="scope.id"
|
||||||
:class="{ 'active': activeScope === 'global' }"
|
@click="activeScope = scope.id; updateExplanation()"
|
||||||
@click="activeScope = 'global'; updateExplanation()"
|
class="scope-btn"
|
||||||
|
:class="{ active: activeScope === scope.id }"
|
||||||
|
:style="{ borderColor: scope.color }"
|
||||||
>
|
>
|
||||||
<div class="scope-header">全局作用域</div>
|
{{ scope.name }}
|
||||||
<div class="scope-content">
|
</button>
|
||||||
<div class="variable" :class="{ 'visible': activeScope === 'global', 'dimmed': activeScope !== 'global' }">
|
</div>
|
||||||
appName = "Todo"
|
|
||||||
|
<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>
|
||||||
|
<div class="level-vars">
|
||||||
<!-- 函数作用域 -->
|
|
||||||
<div class="nested-scope">
|
|
||||||
<div
|
<div
|
||||||
class="scope function-scope"
|
v-for="v in scope.vars"
|
||||||
:class="{ 'active': activeScope === 'function' }"
|
:key="v.name"
|
||||||
@click.stop="activeScope = 'function'; updateExplanation()"
|
class="var-tag"
|
||||||
|
:class="{ own: v.own, inherited: !v.own }"
|
||||||
>
|
>
|
||||||
<div class="scope-header">函数 greet() 作用域</div>
|
<span class="var-name">{{ v.name }}</span>
|
||||||
<div class="scope-content">
|
<span class="var-value">= {{ v.value }}</span>
|
||||||
<div class="variable" :class="{ 'visible': ['global', 'function'].includes(activeScope), 'dimmed': !['global', 'function'].includes(activeScope) }">
|
<span v-if="!v.own" class="var-from">← {{ v.from }}</span>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div class="code-display">
|
<div class="code-display">
|
||||||
@@ -112,7 +104,7 @@ function greet() {
|
|||||||
const message = "你好" // 函数作用域
|
const message = "你好" // 函数作用域
|
||||||
|
|
||||||
if (true) {
|
if (true) {
|
||||||
const greeting = message + appName // 块级作用域 ✅ 能看到外层的
|
const greeting = message + appName // 块级作用域
|
||||||
console.log(greeting)
|
console.log(greeting)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,112 +130,133 @@ h3 {
|
|||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scopes-container {
|
.scope-selector {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
gap: 12px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scope {
|
.scope-btn {
|
||||||
border: 3px solid var(--vp-c-border);
|
padding: 10px 16px;
|
||||||
border-radius: 12px;
|
border: 2px solid var(--vp-c-border);
|
||||||
padding: 20px;
|
border-radius: 8px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope-btn:hover {
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scope:hover {
|
.scope-btn.active {
|
||||||
border-color: var(--vp-c-brand-1);
|
background: var(--vp-c-brand-soft);
|
||||||
transform: scale(1.02);
|
border-color: var(--vp-c-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scope.active {
|
.scope-visual {
|
||||||
border-width: 4px;
|
display: grid;
|
||||||
box-shadow: 0 0 0 4px rgba(62, 175, 124, 0.1);
|
grid-template-columns: 2fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-scope {
|
@media (max-width: 768px) {
|
||||||
border-color: #a0aec0;
|
.scope-visual {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-scope.active {
|
.scope-levels {
|
||||||
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 {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nested-scope {
|
.var-tag {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
margin-top: 12px;
|
gap: 4px;
|
||||||
}
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
.variable {
|
font-size: 13px;
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
background: var(--vp-c-bg);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.variable.visible {
|
.var-tag.own {
|
||||||
color: var(--vp-c-text-1);
|
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;
|
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);
|
color: var(--vp-c-text-1);
|
||||||
animation: fadeIn 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
.var-value {
|
||||||
from { opacity: 0; transform: translateY(-10px); }
|
color: var(--vp-c-text-2);
|
||||||
to { opacity: 1; transform: translateY(0); }
|
}
|
||||||
|
|
||||||
|
.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 {
|
.code-display {
|
||||||
@@ -270,15 +283,4 @@ h3 {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: #d4d4d4;
|
color: #d4d4d4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.scopes-container {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scope {
|
|
||||||
min-width: 280px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,85 +5,80 @@ const name = ref('张三')
|
|||||||
const age = ref(25)
|
const age = ref(25)
|
||||||
const isStudent = ref(true)
|
const isStudent = ref(true)
|
||||||
const showMessage = ref('')
|
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 = () => {
|
const modifyAge = () => {
|
||||||
age.value = 26
|
age.value = 26
|
||||||
showMessage.value = '✅ let 变量可以修改值'
|
setMessage('✅ let 可以修改', 'success')
|
||||||
showSuccess.value = true
|
|
||||||
setTimeout(() => {
|
|
||||||
showMessage.value = ''
|
|
||||||
showSuccess.value = false
|
|
||||||
}, 2000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const modifyName = () => {
|
const modifyName = () => {
|
||||||
showMessage.value = '❌ const 不能重新赋值'
|
setMessage('❌ const 不能改', 'error')
|
||||||
showSuccess.value = false
|
|
||||||
setTimeout(() => {
|
|
||||||
showMessage.value = ''
|
|
||||||
}, 2000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
name.value = '张三'
|
name.value = '张三'
|
||||||
age.value = 25
|
age.value = 25
|
||||||
isStudent.value = true
|
isStudent.value = true
|
||||||
showMessage.value = ''
|
clearMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
const codeLines = ref([
|
|
||||||
`const name = "张三"`,
|
|
||||||
`let age = 25`,
|
|
||||||
`const isStudent = true`
|
|
||||||
])
|
|
||||||
|
|
||||||
const executeCode = ref([])
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="variable-box-demo">
|
<div class="variable-box-demo">
|
||||||
<h3>变量就像带名字的盒子</h3>
|
<div class="demo-header">
|
||||||
|
<span class="title">📦 变量就像带名字的盒子</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="boxes-container">
|
<div class="boxes-row">
|
||||||
<!-- 盒子 1: const name -->
|
<div class="var-box" :class="{ error: messageType === 'error' }">
|
||||||
<div class="variable-box const-box">
|
<div class="box-tag const">const</div>
|
||||||
<div class="box-label">const name</div>
|
<div class="box-name">name</div>
|
||||||
<div class="box-value">{{ name }}</div>
|
<div class="box-value">{{ name }}</div>
|
||||||
<div class="box-icon">🔒</div>
|
<div class="box-lock">🔒</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 盒子 2: let age -->
|
<div class="var-box" :class="{ success: messageType === 'success' }">
|
||||||
<div class="variable-box let-box" :class="{ 'success': showSuccess && age === 26 }">
|
<div class="box-tag let">let</div>
|
||||||
<div class="box-label">let age</div>
|
<div class="box-name">age</div>
|
||||||
<div class="box-value">{{ age }}</div>
|
<div class="box-value">{{ age }}</div>
|
||||||
<div class="box-icon">🔓</div>
|
<div class="box-lock">🔓</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 盒子 3: const isStudent -->
|
<div class="var-box">
|
||||||
<div class="variable-box const-box">
|
<div class="box-tag const">const</div>
|
||||||
<div class="box-label">const isStudent</div>
|
<div class="box-name">isStudent</div>
|
||||||
<div class="box-value">{{ isStudent }}</div>
|
<div class="box-value">{{ isStudent }}</div>
|
||||||
<div class="box-icon">🔒</div>
|
<div class="box-lock">🔒</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message-bubble" :class="{ 'error': !showSuccess, 'success': showSuccess }" v-if="showMessage">
|
<div class="message" v-if="showMessage" :class="messageType">
|
||||||
{{ showMessage }}
|
{{ showMessage }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button @click="modifyAge" class="btn-primary">修改 age 为 26</button>
|
<button @click="modifyAge" class="btn btn-primary">修改 age</button>
|
||||||
<button @click="modifyName" class="btn-danger">修改 name 为李四</button>
|
<button @click="modifyName" class="btn btn-danger">修改 name</button>
|
||||||
<button @click="reset" class="btn-secondary">重置</button>
|
<button @click="reset" class="btn btn-secondary">重置</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="code-display">
|
<div class="code-snippet">
|
||||||
<pre><code>const name = "张三"
|
<code>const name = "{{ name }}"</code>
|
||||||
let age = 25
|
<code>let age = {{ age }}</code>
|
||||||
const isStudent = true
|
|
||||||
|
|
||||||
{{ executeCode.join('\n') }}</code></pre>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -92,42 +87,59 @@ const isStudent = true
|
|||||||
.variable-box-demo {
|
.variable-box-demo {
|
||||||
border: 1px solid var(--vp-c-border);
|
border: 1px solid var(--vp-c-border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 24px;
|
padding: 20px;
|
||||||
margin: 24px 0;
|
margin: 16px 0;
|
||||||
background: var(--vp-c-bg);
|
background: var(--vp-c-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
.demo-header {
|
||||||
margin: 0 0 16px 0;
|
margin-bottom: 16px;
|
||||||
font-size: 18px;
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.boxes-container {
|
.boxes-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 16px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.variable-box {
|
.var-box {
|
||||||
position: relative;
|
width: 100px;
|
||||||
width: 120px;
|
height: 100px;
|
||||||
height: 120px;
|
|
||||||
border: 2px solid var(--vp-c-border);
|
border: 2px solid var(--vp-c-border);
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.variable-box.success {
|
.var-box.error {
|
||||||
border-color: #3eaf7c;
|
border-color: #ef4444;
|
||||||
animation: pulse 0.5s ease;
|
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 {
|
@keyframes pulse {
|
||||||
@@ -135,144 +147,107 @@ h3 {
|
|||||||
50% { transform: scale(1.05); }
|
50% { transform: scale(1.05); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.box-label {
|
.box-tag {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -12px;
|
top: -10px;
|
||||||
left: 50%;
|
padding: 2px 8px;
|
||||||
transform: translateX(-50%);
|
border-radius: 10px;
|
||||||
background: var(--vp-c-brand-1);
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.let-box .box-label {
|
.box-tag.const {
|
||||||
background: #42b983;
|
background: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-tag.let {
|
||||||
|
background: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-name {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.box-value {
|
.box-value {
|
||||||
font-size: 24px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: monospace;
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.box-icon {
|
.box-lock {
|
||||||
font-size: 16px;
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble {
|
.message {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 12px;
|
padding: 10px;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 12px;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
animation: fadeIn 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
.message.error {
|
||||||
from { opacity: 0; transform: translateY(-10px); }
|
background: #fef2f2;
|
||||||
to { opacity: 1; transform: translateY(0); }
|
color: #dc2626;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble.error {
|
.message.success {
|
||||||
background: #fee;
|
background: #ecfdf5;
|
||||||
color: #c00;
|
color: #059669;
|
||||||
}
|
|
||||||
|
|
||||||
.message-bubble.success {
|
|
||||||
background: #e8f5e9;
|
|
||||||
color: #2e7d32;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
margin-bottom: 12px;
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
.btn {
|
||||||
padding: 8px 16px;
|
padding: 8px 14px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s;
|
||||||
}
|
|
||||||
|
|
||||||
button:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: var(--vp-c-brand-1);
|
background: #3b82f6;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: var(--vp-c-brand-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
background: #f56565;
|
background: #ef4444;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger:hover {
|
|
||||||
background: #e53e3e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: var(--vp-c-bg-soft);
|
background: var(--vp-c-bg-soft);
|
||||||
color: var(--vp-c-text-1);
|
color: var(--vp-c-text-1);
|
||||||
|
border: 1px solid var(--vp-c-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.code-snippet {
|
||||||
background: var(--vp-c-bg-soft-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-display {
|
|
||||||
background: #1e1e1e;
|
background: #1e1e1e;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
padding: 16px;
|
padding: 10px 14px;
|
||||||
overflow-x: auto;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-display pre {
|
.code-snippet code {
|
||||||
margin: 0;
|
font-family: monospace;
|
||||||
}
|
font-size: 12px;
|
||||||
|
|
||||||
.code-display code {
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #d4d4d4;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,542 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const isAnimating = ref(false)
|
||||||
|
const currentStep = ref(0)
|
||||||
|
const callStack = ref([])
|
||||||
|
const output = ref([])
|
||||||
|
|
||||||
|
const codeSteps = [
|
||||||
|
{ action: 'push', function: 'main', description: '调用 main()', code: 'main()' },
|
||||||
|
{ action: 'push', function: 'a', description: 'main() 调用 a()', code: 'function a() {' },
|
||||||
|
{ action: 'push', function: 'b', description: 'a() 调用 b()', code: 'function b() {' },
|
||||||
|
{ action: 'push', function: 'c', description: 'b() 调用 c()', code: 'function c() {' },
|
||||||
|
{ action: 'log', function: 'c', description: 'c() 执行 console.log', code: 'console.log("执行完毕")', output: '执行完毕' },
|
||||||
|
{ action: 'pop', function: 'c', description: 'c() 执行完成,从栈中弹出', code: '}' },
|
||||||
|
{ action: 'pop', function: 'b', description: 'b() 执行完成,从栈中弹出', code: '}' },
|
||||||
|
{ action: 'pop', function: 'a', description: 'a() 执行完成,从栈中弹出', code: '}' },
|
||||||
|
{ action: 'pop', function: 'main', description: 'main() 执行完成,从栈中弹出', code: '}' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
currentStep.value = 0
|
||||||
|
callStack.value = []
|
||||||
|
output.value = []
|
||||||
|
isAnimating.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStep = () => {
|
||||||
|
if (currentStep.value >= codeSteps.length) return
|
||||||
|
|
||||||
|
const step = codeSteps[currentStep.value]
|
||||||
|
|
||||||
|
if (step.action === 'push') {
|
||||||
|
callStack.value.push({
|
||||||
|
function: step.function,
|
||||||
|
code: step.code,
|
||||||
|
active: true
|
||||||
|
})
|
||||||
|
// 标记之前的为非活动
|
||||||
|
callStack.value.forEach((item, index) => {
|
||||||
|
if (index < callStack.value.length - 1) {
|
||||||
|
item.active = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (step.action === 'pop') {
|
||||||
|
callStack.value.pop()
|
||||||
|
// 标记新的顶部为活动
|
||||||
|
if (callStack.value.length > 0) {
|
||||||
|
callStack.value[callStack.value.length - 1].active = true
|
||||||
|
}
|
||||||
|
} else if (step.action === 'log') {
|
||||||
|
output.value.push(step.output)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStep.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
const play = async () => {
|
||||||
|
if (isAnimating.value) return
|
||||||
|
isAnimating.value = true
|
||||||
|
reset()
|
||||||
|
|
||||||
|
while (currentStep.value < codeSteps.length && isAnimating.value) {
|
||||||
|
nextStep()
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1200))
|
||||||
|
}
|
||||||
|
|
||||||
|
isAnimating.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
isAnimating.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="call-stack-demo">
|
||||||
|
<h3>调用栈:函数执行的足迹</h3>
|
||||||
|
|
||||||
|
<div class="demo-layout">
|
||||||
|
<!-- 代码显示 -->
|
||||||
|
<div class="code-section">
|
||||||
|
<h4>代码</h4>
|
||||||
|
<div class="code-display">
|
||||||
|
<div
|
||||||
|
v-for="(step, index) in codeSteps"
|
||||||
|
:key="index"
|
||||||
|
class="code-line"
|
||||||
|
:class="{
|
||||||
|
'current': currentStep === index,
|
||||||
|
'executed': currentStep > index
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="line-number">{{ index + 1 }}</span>
|
||||||
|
<span class="line-code">{{ step.code }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 调用栈可视化 -->
|
||||||
|
<div class="stack-section">
|
||||||
|
<h4>调用栈</h4>
|
||||||
|
<div class="stack-container">
|
||||||
|
<div class="stack-base">
|
||||||
|
<div class="stack-label">栈底</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stack-frames">
|
||||||
|
<transition-group name="stack-frame">
|
||||||
|
<div
|
||||||
|
v-for="(frame, index) in callStack"
|
||||||
|
:key="`${frame.function}-${index}`"
|
||||||
|
class="stack-frame"
|
||||||
|
:class="{ 'active': frame.active }"
|
||||||
|
:style="{ bottom: `${index * 60}px` }"
|
||||||
|
>
|
||||||
|
<div class="frame-function">{{ frame.function }}()</div>
|
||||||
|
<div class="frame-code">{{ frame.code }}</div>
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
|
||||||
|
<div v-if="callStack.length === 0" class="empty-stack">
|
||||||
|
栈为空
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stack-top">
|
||||||
|
<div class="stack-label">栈顶</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stack-explanation">
|
||||||
|
<p><strong>当前状态:</strong></p>
|
||||||
|
<p v-if="currentStep < codeSteps.length">
|
||||||
|
{{ codeSteps[currentStep]?.description }}
|
||||||
|
</p>
|
||||||
|
<p v-else>
|
||||||
|
执行完成
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 输出显示 -->
|
||||||
|
<div class="output-section">
|
||||||
|
<h4>输出</h4>
|
||||||
|
<div class="output-container">
|
||||||
|
<div v-if="output.length === 0" class="empty-output">
|
||||||
|
等待输出...
|
||||||
|
</div>
|
||||||
|
<transition-group name="output">
|
||||||
|
<div
|
||||||
|
v-for="(log, index) in output"
|
||||||
|
:key="`log-${index}`"
|
||||||
|
class="output-line"
|
||||||
|
>
|
||||||
|
{{ log }}
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 控制按钮 -->
|
||||||
|
<div class="controls">
|
||||||
|
<button @click="play" :disabled="isAnimating" class="btn-play">
|
||||||
|
{{ isAnimating ? '执行中...' : '▶ 自动演示' }}
|
||||||
|
</button>
|
||||||
|
<button @click="nextStep" :disabled="isAnimating || currentStep >= codeSteps.length" class="btn-step">
|
||||||
|
⏭ 单步执行
|
||||||
|
</button>
|
||||||
|
<button @click="stop" :disabled="!isAnimating" class="btn-stop">
|
||||||
|
⏸ 停止
|
||||||
|
</button>
|
||||||
|
<button @click="reset" :disabled="isAnimating" class="btn-reset">
|
||||||
|
🔄 重置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 说明 -->
|
||||||
|
<div class="explanation-box">
|
||||||
|
<p><strong>调用栈工作原理:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>每次调用函数,就会在栈上"压入"一个新的"栈帧"</li>
|
||||||
|
<li>栈帧记录了函数的执行状态、局部变量等信息</li>
|
||||||
|
<li>函数执行完毕,栈帧就会从栈上"弹出"</li>
|
||||||
|
<li>栈是"后进先出"(LIFO)的数据结构</li>
|
||||||
|
<li>如果递归太深,会导致"栈溢出"错误</li>
|
||||||
|
</ul>
|
||||||
|
<p class="highlight">
|
||||||
|
调用栈就像一摞盘子:最后放上去的盘子最先被取走。每个函数就是一个盘子,执行完就取走,然后继续执行下面的函数。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.call-stack-demo {
|
||||||
|
border: 1px solid var(--vp-c-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
margin: 24px 0;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.demo-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-section,
|
||||||
|
.stack-section {
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-display {
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-line {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-line.current {
|
||||||
|
background: rgba(62, 175, 124, 0.2);
|
||||||
|
border-left: 3px solid var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-line.executed {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-number {
|
||||||
|
color: #858585;
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: right;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-code {
|
||||||
|
color: #d4d4d4;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-container {
|
||||||
|
position: relative;
|
||||||
|
height: 350px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-base,
|
||||||
|
.stack-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-top {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-frames {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-frame {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
right: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-frame.active {
|
||||||
|
border-color: var(--vp-c-brand-1);
|
||||||
|
background: rgba(62, 175, 124, 0.1);
|
||||||
|
box-shadow: 0 0 0 3px rgba(62, 175, 124, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-frame-enter-active,
|
||||||
|
.stack-frame-leave-active {
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-frame-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-frame-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-function {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-code {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-stack {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-explanation {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(62, 175, 124, 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-explanation p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-explanation strong {
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-container {
|
||||||
|
min-height: 60px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-output {
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-line {
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: var(--vp-c-brand-1);
|
||||||
|
color: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-enter-active,
|
||||||
|
.output-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-play {
|
||||||
|
background: var(--vp-c-brand-1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-play:hover:not(:disabled) {
|
||||||
|
background: var(--vp-c-brand-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-step {
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-step:hover:not(:disabled) {
|
||||||
|
background: var(--vp-c-bg-soft-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-stop {
|
||||||
|
background: #ed8936;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-stop:hover:not(:disabled) {
|
||||||
|
background: #dd6b20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reset {
|
||||||
|
background: #f56565;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reset:hover:not(:disabled) {
|
||||||
|
background: #e53e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation-box {
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-left: 4px solid var(--vp-c-brand-1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation-box p {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation-box p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation-box strong {
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation-box ul {
|
||||||
|
margin: 12px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation-box li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation-box .highlight {
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(62, 175, 124, 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,752 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const phase = ref('mark')
|
||||||
|
const isAnimating = ref(false)
|
||||||
|
const currentStep = ref(0)
|
||||||
|
|
||||||
|
const objects = ref([
|
||||||
|
{ id: 1, name: 'obj1', color: '#68d391', marked: false, collected: false },
|
||||||
|
{ id: 2, name: 'obj2', color: '#4299e1', marked: false, collected: false },
|
||||||
|
{ id: 3, name: 'obj3', color: '#ed8936', marked: false, collected: false },
|
||||||
|
{ id: 4, name: 'obj4', color: '#f687b3', marked: false, collected: false },
|
||||||
|
{ id: 5, name: 'obj5', color: '#a3bffa', marked: false, collected: false },
|
||||||
|
{ id: 6, name: 'obj6', color: '#fc8181', marked: false, collected: false }
|
||||||
|
])
|
||||||
|
|
||||||
|
const references = ref([
|
||||||
|
{ from: 'root', to: 1, active: false },
|
||||||
|
{ from: 1, to: 2, active: false },
|
||||||
|
{ from: 1, to: 3, active: false },
|
||||||
|
{ from: 3, to: 4, active: false }
|
||||||
|
])
|
||||||
|
|
||||||
|
const phases = [
|
||||||
|
{ name: 'mark', label: '标记阶段', description: '从根对象开始,标记所有可达对象' },
|
||||||
|
{ name: 'sweep', label: '清除阶段', description: '回收未标记的对象' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{ phase: 'mark', action: 'mark-root', description: '从根对象开始标记' },
|
||||||
|
{ phase: 'mark', action: 'mark-1', description: '标记 obj1 (根对象引用)' },
|
||||||
|
{ phase: 'mark', action: 'mark-2', description: '标记 obj2 (obj1 引用)' },
|
||||||
|
{ phase: 'mark', action: 'mark-3', description: '标记 obj3 (obj1 引用)' },
|
||||||
|
{ phase: 'mark', action: 'mark-4', description: '标记 obj4 (obj3 引用)' },
|
||||||
|
{ phase: 'sweep', action: 'collect-5', description: '回收 obj5 (未标记)' },
|
||||||
|
{ phase: 'sweep', action: 'collect-6', description: '回收 obj6 (未标记)' },
|
||||||
|
{ phase: 'done', action: 'finish', description: '垃圾回收完成' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
currentStep.value = 0
|
||||||
|
phase.value = 'mark'
|
||||||
|
isAnimating.value = false
|
||||||
|
objects.value.forEach(obj => {
|
||||||
|
obj.marked = false
|
||||||
|
obj.collected = false
|
||||||
|
})
|
||||||
|
references.value.forEach(ref => {
|
||||||
|
ref.active = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStep = () => {
|
||||||
|
if (currentStep.value >= steps.length) return
|
||||||
|
|
||||||
|
const step = steps[currentStep.value]
|
||||||
|
|
||||||
|
switch (step.action) {
|
||||||
|
case 'mark-root':
|
||||||
|
references.value[0].active = true
|
||||||
|
break
|
||||||
|
case 'mark-1':
|
||||||
|
objects.value[0].marked = true
|
||||||
|
references.value[1].active = true
|
||||||
|
references.value[2].active = true
|
||||||
|
break
|
||||||
|
case 'mark-2':
|
||||||
|
objects.value[1].marked = true
|
||||||
|
break
|
||||||
|
case 'mark-3':
|
||||||
|
objects.value[2].marked = true
|
||||||
|
references.value[3].active = true
|
||||||
|
break
|
||||||
|
case 'mark-4':
|
||||||
|
objects.value[3].marked = true
|
||||||
|
phase.value = 'sweep'
|
||||||
|
break
|
||||||
|
case 'collect-5':
|
||||||
|
objects.value[4].collected = true
|
||||||
|
break
|
||||||
|
case 'collect-6':
|
||||||
|
objects.value[5].collected = true
|
||||||
|
phase.value = 'done'
|
||||||
|
break
|
||||||
|
case 'finish':
|
||||||
|
phase.value = 'done'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStep.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
const play = async () => {
|
||||||
|
if (isAnimating.value) return
|
||||||
|
isAnimating.value = true
|
||||||
|
reset()
|
||||||
|
|
||||||
|
while (currentStep.value < steps.length && isAnimating.value) {
|
||||||
|
nextStep()
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1200))
|
||||||
|
}
|
||||||
|
|
||||||
|
isAnimating.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
isAnimating.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="garbage-collection-demo">
|
||||||
|
<h3>垃圾回收机制</h3>
|
||||||
|
|
||||||
|
<!-- 阶段指示器 -->
|
||||||
|
<div class="phase-indicator">
|
||||||
|
<div class="phase-tabs">
|
||||||
|
<div
|
||||||
|
v-for="p in phases"
|
||||||
|
:key="p.name"
|
||||||
|
:class="{ 'active': phase === p.name }"
|
||||||
|
class="phase-tab"
|
||||||
|
>
|
||||||
|
<span class="phase-label">{{ p.label }}</span>
|
||||||
|
<span class="phase-description">{{ p.description }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 对象关系图 -->
|
||||||
|
<div class="graph-container">
|
||||||
|
<div class="graph-header">
|
||||||
|
<h4>对象引用关系</h4>
|
||||||
|
<div class="legend">
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-color unmarked"></span>
|
||||||
|
<span>未标记</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-color marked"></span>
|
||||||
|
<span>已标记(可达)</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend-item">
|
||||||
|
<span class="legend-color collected"></span>
|
||||||
|
<span>已回收</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="object-graph">
|
||||||
|
<!-- 根对象 -->
|
||||||
|
<div class="root-object">
|
||||||
|
<div class="object-box root">
|
||||||
|
<div class="object-icon">🌳</div>
|
||||||
|
<div class="object-name">Root</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 对象节点 -->
|
||||||
|
<div class="objects-grid">
|
||||||
|
<div
|
||||||
|
v-for="obj in objects"
|
||||||
|
:key="obj.id"
|
||||||
|
class="object-node"
|
||||||
|
:class="{
|
||||||
|
'marked': obj.marked,
|
||||||
|
'collected': obj.collected
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="object-box" :style="{ borderColor: obj.color }">
|
||||||
|
<div class="object-icon" :style="{ background: obj.color }">
|
||||||
|
{{ obj.collected ? '💀' : '📦' }}
|
||||||
|
</div>
|
||||||
|
<div class="object-name">{{ obj.name }}</div>
|
||||||
|
<div v-if="obj.marked" class="object-status">✓ 可达</div>
|
||||||
|
<div v-if="obj.collected" class="object-status collected">✗ 回收</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 引用连线 (用SVG绘制) -->
|
||||||
|
<svg class="connections" viewBox="0 0 600 400">
|
||||||
|
<defs>
|
||||||
|
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||||
|
<polygon points="0 0, 10 3.5, 0 7" fill="#a0aec0" />
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
<!-- Root -> obj1 -->
|
||||||
|
<line
|
||||||
|
x1="80"
|
||||||
|
y1="200"
|
||||||
|
x2="180"
|
||||||
|
y2="100"
|
||||||
|
:class="{ 'active': references[0].active }"
|
||||||
|
marker-end="url(#arrowhead)"
|
||||||
|
/>
|
||||||
|
<!-- obj1 -> obj2 -->
|
||||||
|
<line
|
||||||
|
x1="220"
|
||||||
|
y1="120"
|
||||||
|
x2="220"
|
||||||
|
y2="180"
|
||||||
|
:class="{ 'active': references[1].active }"
|
||||||
|
marker-end="url(#arrowhead)"
|
||||||
|
/>
|
||||||
|
<!-- obj1 -> obj3 -->
|
||||||
|
<line
|
||||||
|
x1="260"
|
||||||
|
y1="120"
|
||||||
|
x2="380"
|
||||||
|
y2="120"
|
||||||
|
:class="{ 'active': references[2].active }"
|
||||||
|
marker-end="url(#arrowhead)"
|
||||||
|
/>
|
||||||
|
<!-- obj3 -> obj4 -->
|
||||||
|
<line
|
||||||
|
x1="400"
|
||||||
|
y1="140"
|
||||||
|
x2="400"
|
||||||
|
y2="200"
|
||||||
|
:class="{ 'active': references[3].active }"
|
||||||
|
marker-end="url(#arrowhead)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 当前步骤说明 -->
|
||||||
|
<div class="step-description">
|
||||||
|
<div class="step-content">
|
||||||
|
<strong>当前操作:</strong>
|
||||||
|
<span v-if="currentStep < steps.length">
|
||||||
|
{{ steps[currentStep].description }}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
垃圾回收完成
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 控制按钮 -->
|
||||||
|
<div class="controls">
|
||||||
|
<button @click="play" :disabled="isAnimating" class="btn-play">
|
||||||
|
{{ isAnimating ? '执行中...' : '▶ 自动演示' }}
|
||||||
|
</button>
|
||||||
|
<button @click="nextStep" :disabled="isAnimating || currentStep >= steps.length" class="btn-step">
|
||||||
|
⏭ 单步执行
|
||||||
|
</button>
|
||||||
|
<button @click="stop" :disabled="!isAnimating" class="btn-stop">
|
||||||
|
⏸ 停止
|
||||||
|
</button>
|
||||||
|
<button @click="reset" :disabled="isAnimating" class="btn-reset">
|
||||||
|
🔄 重置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 算法说明 -->
|
||||||
|
<div class="algorithm-box">
|
||||||
|
<h4>标记-清除算法 (Mark-and-Sweep)</h4>
|
||||||
|
<div class="algorithm-steps">
|
||||||
|
<div class="algorithm-step">
|
||||||
|
<span class="step-number">1</span>
|
||||||
|
<div class="step-content">
|
||||||
|
<strong>标记阶段</strong>
|
||||||
|
<p>从根对象(Root)开始,遍历所有可达对象,标记为"活动对象"</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="algorithm-step">
|
||||||
|
<span class="step-number">2</span>
|
||||||
|
<div class="step-content">
|
||||||
|
<strong>清除阶段</strong>
|
||||||
|
<p>遍历整个堆内存,回收所有未被标记的对象</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="algorithm-step">
|
||||||
|
<span class="step-number">3</span>
|
||||||
|
<div class="step-content">
|
||||||
|
<strong>重置标记</strong>
|
||||||
|
<p>清除所有标记位,为下一次垃圾回收做准备</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="key-points">
|
||||||
|
<h5>核心要点</h5>
|
||||||
|
<ul>
|
||||||
|
<li><strong>根对象(Root):</strong> 全局变量、栈上的变量等,总是被认为是可达的</li>
|
||||||
|
<li><strong>可达对象:</strong> 从根对象出发,通过引用链能访问到的对象</li>
|
||||||
|
<li><strong>垃圾对象:</strong> 无法从根对象访问到的对象,会被回收</li>
|
||||||
|
<li><strong>循环引用:</strong> 如果两个对象互相引用但都不可达,仍会被回收</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 实际应用 -->
|
||||||
|
<div class="practical-tips">
|
||||||
|
<h4>实际应用技巧</h4>
|
||||||
|
<div class="tips-grid">
|
||||||
|
<div class="tip-card">
|
||||||
|
<div class="tip-icon">💡</div>
|
||||||
|
<div class="tip-content">
|
||||||
|
<strong>及时解除引用</strong>
|
||||||
|
<p>对象不再使用时,将其设为 null</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tip-card">
|
||||||
|
<div class="tip-icon">🔒</div>
|
||||||
|
<div class="tip-content">
|
||||||
|
<strong>避免意外的全局变量</strong>
|
||||||
|
<p>使用 const/let 代替 var</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tip-card">
|
||||||
|
<div class="tip-icon">🧹</div>
|
||||||
|
<div class="tip-content">
|
||||||
|
<strong>清理事件监听</strong>
|
||||||
|
<p>组件销毁时移除所有监听器</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tip-card">
|
||||||
|
<div class="tip-icon">📊</div>
|
||||||
|
<div class="tip-content">
|
||||||
|
<strong>定期检查内存</strong>
|
||||||
|
<p>用 DevTools Memory 面板监控</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.garbage-collection-demo {
|
||||||
|
border: 1px solid var(--vp-c-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
margin: 24px 0;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phase-indicator {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phase-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phase-tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phase-tab.active {
|
||||||
|
border-color: var(--vp-c-brand-1);
|
||||||
|
background: rgba(62, 175, 124, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phase-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phase-description {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-color {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-color.unmarked {
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-color: var(--vp-c-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-color.marked {
|
||||||
|
background: rgba(104, 217, 145, 0.2);
|
||||||
|
border-color: #68d391;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-color.collected {
|
||||||
|
background: rgba(245, 101, 101, 0.2);
|
||||||
|
border-color: #f56565;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-graph {
|
||||||
|
position: relative;
|
||||||
|
height: 400px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root-object {
|
||||||
|
position: absolute;
|
||||||
|
left: 20px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.objects-grid {
|
||||||
|
position: absolute;
|
||||||
|
left: 150px;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-node {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-box {
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border: 3px solid var(--vp-c-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-box.root {
|
||||||
|
border-color: var(--vp-c-brand-1);
|
||||||
|
background: rgba(62, 175, 124, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-node.marked .object-box {
|
||||||
|
border-color: #68d391;
|
||||||
|
background: rgba(104, 217, 145, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-node.collected .object-box {
|
||||||
|
border-color: #f56565;
|
||||||
|
background: rgba(245, 101, 101, 0.1);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
margin: 0 auto 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-status {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-status:not(.collected) {
|
||||||
|
color: #68d391;
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-status.collected {
|
||||||
|
color: #f56565;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connections {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connections line {
|
||||||
|
stroke: #a0aec0;
|
||||||
|
stroke-width: 2;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connections line.active {
|
||||||
|
stroke: var(--vp-c-brand-1);
|
||||||
|
stroke-width: 3;
|
||||||
|
animation: pulse 1s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-description {
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-left: 4px solid var(--vp-c-brand-1);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-content {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-content strong {
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-play {
|
||||||
|
background: var(--vp-c-brand-1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-play:hover:not(:disabled) {
|
||||||
|
background: var(--vp-c-brand-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-step {
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-step:hover:not(:disabled) {
|
||||||
|
background: var(--vp-c-bg-soft-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-stop {
|
||||||
|
background: #ed8936;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-stop:hover:not(:disabled) {
|
||||||
|
background: #dd6b20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reset {
|
||||||
|
background: #f56565;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reset:hover:not(:disabled) {
|
||||||
|
background: #e53e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.algorithm-box {
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.algorithm-steps {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.algorithm-step {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--vp-c-brand-1);
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.algorithm-step .step-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.algorithm-step strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.algorithm-step p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-points {
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-points ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-points li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-points strong {
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.practical-tips {
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-content strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-content p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,682 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const activeScenario = ref('global-vars')
|
||||||
|
|
||||||
|
const scenarios = [
|
||||||
|
{ value: 'global-vars', label: '全局变量', icon: '🌍' },
|
||||||
|
{ value: 'event-listeners', label: '事件监听', icon: '🎯' },
|
||||||
|
{ value: 'closures', label: '闭包引用', icon: '🔒' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 全局变量场景
|
||||||
|
const globalMemory = ref([])
|
||||||
|
|
||||||
|
// 事件监听场景
|
||||||
|
const eventListeners = ref([])
|
||||||
|
const eventCount = ref(0)
|
||||||
|
|
||||||
|
// 闭包场景
|
||||||
|
const closureItems = ref([])
|
||||||
|
|
||||||
|
const memoryUsage = ref(0)
|
||||||
|
const maxMemory = ref(100)
|
||||||
|
|
||||||
|
const addGlobalVariable = () => {
|
||||||
|
const largeData = new Array(10000).fill(`数据 ${globalMemory.value.length}`)
|
||||||
|
globalMemory.value.push({
|
||||||
|
id: Date.now(),
|
||||||
|
data: largeData,
|
||||||
|
timestamp: new Date().toLocaleTimeString()
|
||||||
|
})
|
||||||
|
updateMemory()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearGlobalVariables = () => {
|
||||||
|
globalMemory.value = []
|
||||||
|
updateMemory()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 事件监听场景
|
||||||
|
const addEventListener = () => {
|
||||||
|
const handler = () => console.log('事件监听器')
|
||||||
|
eventListeners.value.push({
|
||||||
|
id: Date.now(),
|
||||||
|
handler: handler,
|
||||||
|
active: true
|
||||||
|
})
|
||||||
|
eventCount.value++
|
||||||
|
updateMemory()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeAllListeners = () => {
|
||||||
|
eventListeners.value = []
|
||||||
|
eventCount.value = 0
|
||||||
|
updateMemory()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 闭包场景
|
||||||
|
const createClosure = () => {
|
||||||
|
const largeData = new Array(10000).fill('闭包数据')
|
||||||
|
const closure = () => {
|
||||||
|
return largeData.length
|
||||||
|
}
|
||||||
|
closureItems.value.push({
|
||||||
|
id: Date.now(),
|
||||||
|
closure: closure,
|
||||||
|
data: largeData,
|
||||||
|
timestamp: new Date().toLocaleTimeString()
|
||||||
|
})
|
||||||
|
updateMemory()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearClosures = () => {
|
||||||
|
closureItems.value = []
|
||||||
|
updateMemory()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateMemory = () => {
|
||||||
|
const total = globalMemory.value.length + eventListeners.value.length + closureItems.value.length
|
||||||
|
memoryUsage.value = Math.min(total, maxMemory.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetAll = () => {
|
||||||
|
globalMemory.value = []
|
||||||
|
eventListeners.value = []
|
||||||
|
eventCount.value = 0
|
||||||
|
closureItems.value = []
|
||||||
|
memoryUsage.value = 0
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="memory-leak-demo">
|
||||||
|
<h3>内存泄漏演示</h3>
|
||||||
|
|
||||||
|
<!-- 场景选择 -->
|
||||||
|
<div class="scenario-tabs">
|
||||||
|
<button
|
||||||
|
v-for="scenario in scenarios"
|
||||||
|
:key="scenario.value"
|
||||||
|
@click="activeScenario = scenario.value"
|
||||||
|
:class="{ 'active': activeScenario === scenario.value }"
|
||||||
|
class="scenario-tab"
|
||||||
|
>
|
||||||
|
<span class="tab-icon">{{ scenario.icon }}</span>
|
||||||
|
<span class="tab-label">{{ scenario.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内存使用情况 -->
|
||||||
|
<div class="memory-monitor">
|
||||||
|
<div class="monitor-header">
|
||||||
|
<span class="monitor-title">内存使用情况</span>
|
||||||
|
<span class="monitor-value">{{ memoryUsage }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="memory-bar">
|
||||||
|
<div
|
||||||
|
class="memory-fill"
|
||||||
|
:class="{ 'warning': memoryUsage > 70, 'danger': memoryUsage > 90 }"
|
||||||
|
:style="{ width: `${memoryUsage}%` }"
|
||||||
|
>
|
||||||
|
<span v-if="memoryUsage > 10" class="memory-text">{{ memoryUsage }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="memoryUsage > 90" class="memory-alert">
|
||||||
|
⚠️ 内存占用过高!可能导致页面卡顿或崩溃
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 场景内容 -->
|
||||||
|
<div class="scenario-content">
|
||||||
|
<!-- 全局变量场景 -->
|
||||||
|
<div v-if="activeScenario === 'global-vars'" class="scenario-panel">
|
||||||
|
<h4>全局变量泄漏</h4>
|
||||||
|
|
||||||
|
<div class="scenario-description">
|
||||||
|
<p><strong>问题:</strong>全局变量不会被垃圾回收,会一直占用内存</p>
|
||||||
|
<p><strong>示例:</strong>不断往全局数组添加数据,从不清理</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button @click="addGlobalVariable" class="btn-add">
|
||||||
|
➕ 添加全局变量
|
||||||
|
</button>
|
||||||
|
<button @click="clearGlobalVariables" class="btn-clear">
|
||||||
|
🗑️ 清空全局变量
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="data-preview">
|
||||||
|
<div class="preview-header">
|
||||||
|
<span>全局变量 ({{ globalMemory.length }} 项)</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-list">
|
||||||
|
<div
|
||||||
|
v-for="item in globalMemory.slice(-5)"
|
||||||
|
:key="item.id"
|
||||||
|
class="preview-item"
|
||||||
|
>
|
||||||
|
<span class="item-id">ID: {{ item.id }}</span>
|
||||||
|
<span class="item-time">{{ item.timestamp }}</span>
|
||||||
|
<span class="item-size">{{ item.data.length }} 项数据</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="globalMemory.length === 0" class="empty-state">
|
||||||
|
暂无全局变量
|
||||||
|
</div>
|
||||||
|
<div v-if="globalMemory.length > 5" class="more-items">
|
||||||
|
... 还有 {{ globalMemory.length - 5 }} 项
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="code-example">
|
||||||
|
<h5>❌ 错误做法</h5>
|
||||||
|
<pre><code>// 全局变量不会被回收
|
||||||
|
globalCache = []
|
||||||
|
function addItem() {
|
||||||
|
globalCache.push(largeData)
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 事件监听场景 -->
|
||||||
|
<div v-if="activeScenario === 'event-listeners'" class="scenario-panel">
|
||||||
|
<h4>事件监听器泄漏</h4>
|
||||||
|
|
||||||
|
<div class="scenario-description">
|
||||||
|
<p><strong>问题:</strong>事件监听器没有被移除,持续占用内存</p>
|
||||||
|
<p><strong>示例:</strong>动态创建元素并添加监听,但从不移除</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button @click="addEventListener" class="btn-add">
|
||||||
|
➕ 添加事件监听
|
||||||
|
</button>
|
||||||
|
<button @click="removeAllListeners" class="btn-clear">
|
||||||
|
🗑️ 移除所有监听
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="data-preview">
|
||||||
|
<div class="preview-header">
|
||||||
|
<span>活跃监听器: {{ eventCount }} 个</span>
|
||||||
|
</div>
|
||||||
|
<div class="listener-list">
|
||||||
|
<div
|
||||||
|
v-for="listener in eventListeners.slice(-5)"
|
||||||
|
:key="listener.id"
|
||||||
|
class="listener-item"
|
||||||
|
>
|
||||||
|
<div class="listener-icon">🎯</div>
|
||||||
|
<div class="listener-info">
|
||||||
|
<span class="listener-id">监听器 #{{ listener.id }}</span>
|
||||||
|
<span class="listener-status">活跃中</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="eventListeners.length === 0" class="empty-state">
|
||||||
|
暂无事件监听器
|
||||||
|
</div>
|
||||||
|
<div v-if="eventListeners.length > 5" class="more-items">
|
||||||
|
... 还有 {{ eventListeners.length - 5 }} 个监听器
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="code-example">
|
||||||
|
<h5>❌ 错误做法</h5>
|
||||||
|
<pre><code>// 监听器没有被移除
|
||||||
|
button.addEventListener('click', handler)
|
||||||
|
// 元素删除时监听器还在!</code></pre>
|
||||||
|
|
||||||
|
<h5>✅ 正确做法</h5>
|
||||||
|
<pre><code>// 保存监听器引用
|
||||||
|
const handler = () => { ... }
|
||||||
|
button.addEventListener('click', handler)
|
||||||
|
|
||||||
|
// 不需要时移除
|
||||||
|
button.removeEventListener('click', handler)</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 闭包场景 -->
|
||||||
|
<div v-if="activeScenario === 'closures'" class="scenario-panel">
|
||||||
|
<h4>闭包引用泄漏</h4>
|
||||||
|
|
||||||
|
<div class="scenario-description">
|
||||||
|
<p><strong>问题:</strong>闭包持有大对象引用,导致对象无法被回收</p>
|
||||||
|
<p><strong>示例:</strong>闭包函数一直引用大数组</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button @click="createClosure" class="btn-add">
|
||||||
|
➕ 创建闭包
|
||||||
|
</button>
|
||||||
|
<button @click="clearClosures" class="btn-clear">
|
||||||
|
🗑️ 清空闭包
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="data-preview">
|
||||||
|
<div class="preview-header">
|
||||||
|
<span>活跃闭包: {{ closureItems.length }} 个</span>
|
||||||
|
</div>
|
||||||
|
<div class="closure-list">
|
||||||
|
<div
|
||||||
|
v-for="item in closureItems.slice(-5)"
|
||||||
|
:key="item.id"
|
||||||
|
class="closure-item"
|
||||||
|
>
|
||||||
|
<div class="closure-icon">🔒</div>
|
||||||
|
<div class="closure-info">
|
||||||
|
<span class="closure-id">闭包 #{{ item.id }}</span>
|
||||||
|
<span class="closure-time">{{ item.timestamp }}</span>
|
||||||
|
<span class="closure-size">持有 {{ item.data.length }} 项数据</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="closureItems.length === 0" class="empty-state">
|
||||||
|
暂无闭包
|
||||||
|
</div>
|
||||||
|
<div v-if="closureItems.length > 5" class="more-items">
|
||||||
|
... 还有 {{ closureItems.length - 5 }} 个闭包
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="code-example">
|
||||||
|
<h5>❌ 错误做法</h5>
|
||||||
|
<pre><code>// 闭包持有大对象引用
|
||||||
|
function createHandler() {
|
||||||
|
const largeData = new Array(1000000)
|
||||||
|
return function() {
|
||||||
|
// largeData 一直被引用,不会被回收
|
||||||
|
console.log('处理中')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handler = createHandler()</code></pre>
|
||||||
|
|
||||||
|
<h5>✅ 正确做法</h5>
|
||||||
|
<pre><code>// 使用后释放引用
|
||||||
|
let handler = createHandler()
|
||||||
|
handler() // 使用
|
||||||
|
handler = null // 释放引用</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 重置按钮 -->
|
||||||
|
<div class="global-actions">
|
||||||
|
<button @click="resetAll" class="btn-reset">
|
||||||
|
🔄 重置所有场景
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 总结 -->
|
||||||
|
<div class="summary-box">
|
||||||
|
<h4>如何避免内存泄漏</h4>
|
||||||
|
<ul>
|
||||||
|
<li><strong>避免全局变量:</strong> 使用 const/let 代替 var,尽量使用局部变量</li>
|
||||||
|
<li><strong>及时清理监听器:</strong> 组件销毁时移除所有事件监听</li>
|
||||||
|
<li><strong>释放闭包引用:</strong> 不需要时将闭包变量设为 null</li>
|
||||||
|
<li><strong>使用 WeakMap/WeakSet:</strong> 自动清理不再被引用的对象</li>
|
||||||
|
<li><strong>定期检查:</strong> 用 DevTools Memory 面板检查内存泄漏</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.memory-leak-demo {
|
||||||
|
border: 1px solid var(--vp-c-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
margin: 24px 0;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin: 12px 0 8px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 2px solid var(--vp-c-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-tab {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-tab:hover {
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-tab.active {
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
border-bottom-color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-label {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-monitor {
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-bar {
|
||||||
|
height: 32px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--vp-c-brand-1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-fill.warning {
|
||||||
|
background: #ed8936;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-fill.danger {
|
||||||
|
background: #f56565;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-text {
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-alert {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(245, 101, 101, 0.1);
|
||||||
|
border-left: 4px solid #f56565;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #f56565;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-content {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-panel {
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-description {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-description p {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-description p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-description strong {
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add {
|
||||||
|
background: var(--vp-c-brand-1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add:hover {
|
||||||
|
background: var(--vp-c-brand-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear {
|
||||||
|
background: #ed8936;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear:hover {
|
||||||
|
background: #dd6b20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reset {
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reset:hover {
|
||||||
|
background: var(--vp-c-bg-soft-hover);
|
||||||
|
border-color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-preview {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-list,
|
||||||
|
.listener-list,
|
||||||
|
.closure-list {
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item,
|
||||||
|
.listener-item,
|
||||||
|
.closure-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listener-icon,
|
||||||
|
.closure-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listener-info,
|
||||||
|
.closure-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listener-id,
|
||||||
|
.closure-id {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listener-status {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #68d391;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-id,
|
||||||
|
.item-time,
|
||||||
|
.item-size,
|
||||||
|
.closure-time,
|
||||||
|
.closure-size {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-items {
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-example {
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-example pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-example code {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-box {
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-left: 4px solid var(--vp-c-brand-1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-box h4 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-box ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-box li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-box strong {
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,487 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const activeTab = ref('browser')
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ value: 'browser', label: '浏览器环境', icon: '🌐' },
|
||||||
|
{ value: 'nodejs', label: 'Node.js 环境', icon: '🟢' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const browserApis = [
|
||||||
|
{ name: 'window', description: '浏览器全局对象', example: 'window.location.href' },
|
||||||
|
{ name: 'document', description: 'DOM 操作', example: 'document.querySelector("h1")' },
|
||||||
|
{ name: 'localStorage', description: '本地存储', example: 'localStorage.setItem("key", "value")' },
|
||||||
|
{ name: 'fetch', description: '网络请求', example: 'fetch("/api/data")' },
|
||||||
|
{ name: 'setTimeout', description: '定时器', example: 'setTimeout(() => {}, 1000)' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const nodeApis = [
|
||||||
|
{ name: 'global', description: 'Node.js 全局对象', example: 'global.process' },
|
||||||
|
{ name: 'process', description: '进程信息', example: 'process.env.NODE_ENV' },
|
||||||
|
{ name: 'fs', description: '文件系统', example: 'fs.readFile("./data.txt")' },
|
||||||
|
{ name: 'http', description: 'HTTP 服务器', example: 'http.createServer((req, res) => {})' },
|
||||||
|
{ name: 'path', description: '路径处理', example: 'path.join("/a", "b")' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const tryCode = ref('console.log(typeof window)')
|
||||||
|
|
||||||
|
const browserResult = ref('')
|
||||||
|
const nodeResult = ref('')
|
||||||
|
|
||||||
|
const runInBrowser = () => {
|
||||||
|
try {
|
||||||
|
browserResult.value = eval(tryCode.value)
|
||||||
|
} catch (e) {
|
||||||
|
browserResult.value = e.message
|
||||||
|
}
|
||||||
|
nodeResult.value = '在 Node.js 中运行...'
|
||||||
|
}
|
||||||
|
|
||||||
|
const runInNode = () => {
|
||||||
|
nodeResult.value = '在浏览器中无法直接运行 Node.js 代码'
|
||||||
|
try {
|
||||||
|
browserResult.value = eval(tryCode.value)
|
||||||
|
} catch (e) {
|
||||||
|
browserResult.value = e.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
browserResult.value = ''
|
||||||
|
nodeResult.value = ''
|
||||||
|
tryCode.value = 'console.log(typeof window)'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="runtime-environment-demo">
|
||||||
|
<h3>运行时环境对比</h3>
|
||||||
|
|
||||||
|
<div class="tab-container">
|
||||||
|
<div class="tabs">
|
||||||
|
<button
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.value"
|
||||||
|
@click="activeTab = tab.value"
|
||||||
|
:class="{ 'active': activeTab === tab.value }"
|
||||||
|
class="tab-btn"
|
||||||
|
>
|
||||||
|
<span class="tab-icon">{{ tab.icon }}</span>
|
||||||
|
<span class="tab-label">{{ tab.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content">
|
||||||
|
<!-- 浏览器环境 -->
|
||||||
|
<div v-if="activeTab === 'browser'" class="environment-content">
|
||||||
|
<h4>浏览器环境</h4>
|
||||||
|
|
||||||
|
<div class="api-grid">
|
||||||
|
<div
|
||||||
|
v-for="api in browserApis"
|
||||||
|
:key="api.name"
|
||||||
|
class="api-card"
|
||||||
|
>
|
||||||
|
<div class="api-name">{{ api.name }}</div>
|
||||||
|
<div class="api-description">{{ api.description }}</div>
|
||||||
|
<div class="api-example">{{ api.example }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="environment-note">
|
||||||
|
<strong>特点:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>✅ 有 DOM 和 BOM API,可以操作网页</li>
|
||||||
|
<li>✅ 有 Web Storage (localStorage, sessionStorage)</li>
|
||||||
|
<li>✅ 有 fetch 和 XMLHttpRequest 进行网络请求</li>
|
||||||
|
<li>❌ 没有文件系统访问权限</li>
|
||||||
|
<li>❌ 不能直接创建 HTTP 服务器</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Node.js 环境 -->
|
||||||
|
<div v-if="activeTab === 'nodejs'" class="environment-content">
|
||||||
|
<h4>Node.js 环境</h4>
|
||||||
|
|
||||||
|
<div class="api-grid">
|
||||||
|
<div
|
||||||
|
v-for="api in nodeApis"
|
||||||
|
:key="api.name"
|
||||||
|
class="api-card"
|
||||||
|
>
|
||||||
|
<div class="api-name">{{ api.name }}</div>
|
||||||
|
<div class="api-description">{{ api.description }}</div>
|
||||||
|
<div class="api-example">{{ api.example }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="environment-note">
|
||||||
|
<strong>特点:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>✅ 有文件系统访问权限</li>
|
||||||
|
<li>✅ 可以创建 HTTP 服务器</li>
|
||||||
|
<li>✅ 可以操作进程和系统资源</li>
|
||||||
|
<li>❌ 没有 DOM 和 BOM</li>
|
||||||
|
<li>❌ 不能直接操作网页元素</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 代码对比演示 -->
|
||||||
|
<div class="code-comparison-section">
|
||||||
|
<h4>代码演示:不同环境的差异</h4>
|
||||||
|
|
||||||
|
<div class="code-input">
|
||||||
|
<label>试试运行这段代码:</label>
|
||||||
|
<input
|
||||||
|
v-model="tryCode"
|
||||||
|
type="text"
|
||||||
|
placeholder="输入 JavaScript 代码"
|
||||||
|
class="code-input-field"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-grid">
|
||||||
|
<div class="result-card">
|
||||||
|
<div class="result-header">
|
||||||
|
<span class="result-icon">🌐</span>
|
||||||
|
<span class="result-title">浏览器结果</span>
|
||||||
|
</div>
|
||||||
|
<div class="result-content">
|
||||||
|
{{ browserResult || '点击"在浏览器运行"查看结果' }}
|
||||||
|
</div>
|
||||||
|
<button @click="runInBrowser" class="run-btn">
|
||||||
|
在浏览器运行
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-card">
|
||||||
|
<div class="result-header">
|
||||||
|
<span class="result-icon">🟢</span>
|
||||||
|
<span class="result-title">Node.js 结果</span>
|
||||||
|
</div>
|
||||||
|
<div class="result-content">
|
||||||
|
{{ nodeResult || '需要在 Node.js 环境中运行' }}
|
||||||
|
</div>
|
||||||
|
<button @click="runInNode" class="run-btn" disabled>
|
||||||
|
需要终端运行
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="reset" class="reset-btn">
|
||||||
|
重置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 总结 -->
|
||||||
|
<div class="summary-box">
|
||||||
|
<p><strong>核心区别:</strong></p>
|
||||||
|
<p>浏览器运行时专注于用户界面和网页交互,提供 DOM、BOM、fetch 等前端专用 API。</p>
|
||||||
|
<p>Node.js 运行时专注于服务器端开发,提供文件系统、HTTP 服务器、进程管理等后端专用 API。</p>
|
||||||
|
<p class="highlight">同样的 JavaScript 语法,但能用的 API 完全不同——这就是"环境判断"的重要性。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.runtime-environment-demo {
|
||||||
|
border: 1px solid var(--vp-c-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
margin: 24px 0;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-container {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 2px solid var(--vp-c-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn:hover {
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
border-bottom-color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-label {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-card {
|
||||||
|
padding: 16px;
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-card:hover {
|
||||||
|
border-color: var(--vp-c-brand-1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-description {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-example {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.environment-note {
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(62, 175, 124, 0.1);
|
||||||
|
border-left: 4px solid var(--vp-c-brand-1);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.environment-note strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.environment-note ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.environment-note li {
|
||||||
|
padding: 4px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-comparison-section {
|
||||||
|
border-top: 2px solid var(--vp-c-border);
|
||||||
|
padding-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input-field {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input-field:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.result-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--vp-c-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-content {
|
||||||
|
min-height: 60px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--vp-c-brand-1);
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-btn:hover:not(:disabled) {
|
||||||
|
background: var(--vp-c-brand-2);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-btn:disabled {
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
padding: 10px 24px;
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn:hover {
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-box {
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-left: 4px solid var(--vp-c-brand-1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-box p {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-box p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-box strong {
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-box .highlight {
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(62, 175, 124, 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,652 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const isAnimating = ref(false)
|
||||||
|
const currentStep = ref(0)
|
||||||
|
const syncCode = ref([
|
||||||
|
{ id: 1, code: 'console.log("1")', type: 'sync', output: '1' },
|
||||||
|
{ id: 2, code: 'setTimeout(() => console.log("2"), 0)', type: 'macro', output: '2' },
|
||||||
|
{ id: 3, code: 'Promise.resolve().then(() => console.log("3"))', type: 'micro', output: '3' },
|
||||||
|
{ id: 4, code: 'console.log("4")', type: 'sync', output: '4' },
|
||||||
|
{ id: 5, code: 'setTimeout(() => console.log("5"), 0)', type: 'macro', output: '5' }
|
||||||
|
])
|
||||||
|
const microTaskQueue = ref([])
|
||||||
|
const macroTaskQueue = ref([])
|
||||||
|
const outputLog = ref([])
|
||||||
|
|
||||||
|
const executionSteps = [
|
||||||
|
{ description: '执行 console.log("1")', action: 'execute', output: '1', source: '同步' },
|
||||||
|
{ description: '遇到 setTimeout,将回调加入宏任务队列', action: 'add-macro', task: 'console.log("2")' },
|
||||||
|
{ description: '遇到 Promise.then,将回调加入微任务队列', action: 'add-micro', task: 'console.log("3")' },
|
||||||
|
{ description: '执行 console.log("4")', action: 'execute', output: '4', source: '同步' },
|
||||||
|
{ description: '遇到 setTimeout,将回调加入宏任务队列', action: 'add-macro', task: 'console.log("5")' },
|
||||||
|
{ description: '同步代码执行完毕,检查微任务队列', action: 'check-micro' },
|
||||||
|
{ description: '执行微任务: console.log("3")', action: 'execute-micro', output: '3', source: '微任务' },
|
||||||
|
{ description: '微任务队列为空,检查宏任务队列', action: 'check-macro' },
|
||||||
|
{ description: '执行宏任务: console.log("2")', action: 'execute-macro', output: '2', source: '宏任务' },
|
||||||
|
{ description: '检查微任务队列(空)', action: 'check-micro' },
|
||||||
|
{ description: '执行宏任务: console.log("5")', action: 'execute-macro', output: '5', source: '宏任务' },
|
||||||
|
{ description: '所有任务执行完毕', action: 'done' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
currentStep.value = 0
|
||||||
|
microTaskQueue.value = []
|
||||||
|
macroTaskQueue.value = []
|
||||||
|
outputLog.value = []
|
||||||
|
isAnimating.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStep = () => {
|
||||||
|
if (currentStep.value >= executionSteps.length) return
|
||||||
|
|
||||||
|
const step = executionSteps[currentStep.value]
|
||||||
|
|
||||||
|
switch (step.action) {
|
||||||
|
case 'execute':
|
||||||
|
outputLog.value.push({ output: step.output, source: step.source })
|
||||||
|
break
|
||||||
|
case 'add-macro':
|
||||||
|
macroTaskQueue.value.push({ code: step.task, status: 'pending' })
|
||||||
|
break
|
||||||
|
case 'add-micro':
|
||||||
|
microTaskQueue.value.push({ code: step.task, status: 'pending' })
|
||||||
|
break
|
||||||
|
case 'check-micro':
|
||||||
|
if (microTaskQueue.value.length > 0) {
|
||||||
|
microTaskQueue.value[0].status = 'ready'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'execute-micro':
|
||||||
|
if (microTaskQueue.value.length > 0) {
|
||||||
|
outputLog.value.push({ output: step.output, source: step.source })
|
||||||
|
microTaskQueue.value.shift()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'check-macro':
|
||||||
|
if (macroTaskQueue.value.length > 0) {
|
||||||
|
macroTaskQueue.value[0].status = 'ready'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'execute-macro':
|
||||||
|
if (macroTaskQueue.value.length > 0) {
|
||||||
|
outputLog.value.push({ output: step.output, source: step.source })
|
||||||
|
macroTaskQueue.value.shift()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStep.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
const play = async () => {
|
||||||
|
if (isAnimating.value) return
|
||||||
|
isAnimating.value = true
|
||||||
|
reset()
|
||||||
|
|
||||||
|
while (currentStep.value < executionSteps.length && isAnimating.value) {
|
||||||
|
nextStep()
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
isAnimating.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
isAnimating.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="task-queue-demo">
|
||||||
|
<h3>任务队列:宏任务 vs 微任务</h3>
|
||||||
|
|
||||||
|
<!-- 代码展示 -->
|
||||||
|
<div class="code-section">
|
||||||
|
<h4>代码示例</h4>
|
||||||
|
<div class="code-display">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in syncCode"
|
||||||
|
:key="item.id"
|
||||||
|
class="code-item"
|
||||||
|
:class="{
|
||||||
|
'current': currentStep === index,
|
||||||
|
'executed': currentStep > index && index < 4
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="item-number">{{ item.id }}</span>
|
||||||
|
<span class="item-code" :class="`type-${item.type}`">{{ item.code }}</span>
|
||||||
|
<span v-if="item.type === 'sync'" class="item-tag">同步</span>
|
||||||
|
<span v-else-if="item.type === 'micro'" class="item-tag micro">微任务</span>
|
||||||
|
<span v-else-if="item.type === 'macro'" class="item-tag macro">宏任务</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 执行过程可视化 -->
|
||||||
|
<div class="visualization">
|
||||||
|
<!-- 调用栈 -->
|
||||||
|
<div class="stack-panel">
|
||||||
|
<h4>调用栈 (正在执行)</h4>
|
||||||
|
<div class="stack-content">
|
||||||
|
<div v-if="currentStep < executionSteps.length" class="current-action">
|
||||||
|
{{ executionSteps[currentStep]?.description }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="current-action done">
|
||||||
|
执行完成
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 微任务队列 -->
|
||||||
|
<div class="queue-panel micro">
|
||||||
|
<h4>
|
||||||
|
微任务队列
|
||||||
|
<span class="badge">Microtask</span>
|
||||||
|
</h4>
|
||||||
|
<div class="queue-content">
|
||||||
|
<transition-group name="task-item">
|
||||||
|
<div
|
||||||
|
v-for="(task, index) in microTaskQueue"
|
||||||
|
:key="`micro-${index}`"
|
||||||
|
class="task-item micro"
|
||||||
|
:class="{ 'ready': task.status === 'ready' }"
|
||||||
|
>
|
||||||
|
<div class="task-code">{{ task.code }}</div>
|
||||||
|
<div v-if="task.status === 'ready'" class="task-status">✅ 就绪</div>
|
||||||
|
<div v-else class="task-status">⏳ 等待</div>
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
<div v-if="microTaskQueue.length === 0" class="empty-queue">
|
||||||
|
队列为空
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 宏任务队列 -->
|
||||||
|
<div class="queue-panel macro">
|
||||||
|
<h4>
|
||||||
|
宏任务队列
|
||||||
|
<span class="badge">Macrotask</span>
|
||||||
|
</h4>
|
||||||
|
<div class="queue-content">
|
||||||
|
<transition-group name="task-item">
|
||||||
|
<div
|
||||||
|
v-for="(task, index) in macroTaskQueue"
|
||||||
|
:key="`macro-${index}`"
|
||||||
|
class="task-item macro"
|
||||||
|
:class="{ 'ready': task.status === 'ready' }"
|
||||||
|
>
|
||||||
|
<div class="task-code">{{ task.code }}</div>
|
||||||
|
<div v-if="task.status === 'ready'" class="task-status">✅ 就绪</div>
|
||||||
|
<div v-else class="task-status">⏳ 等待</div>
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
<div v-if="macroTaskQueue.length === 0" class="empty-queue">
|
||||||
|
队列为空
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 输出日志 -->
|
||||||
|
<div class="output-section">
|
||||||
|
<h4>输出日志 (执行顺序)</h4>
|
||||||
|
<div class="output-log">
|
||||||
|
<div v-if="outputLog.length === 0" class="empty-log">
|
||||||
|
等待输出...
|
||||||
|
</div>
|
||||||
|
<transition-group name="output">
|
||||||
|
<div
|
||||||
|
v-for="(log, index) in outputLog"
|
||||||
|
:key="`log-${index}`"
|
||||||
|
class="log-entry"
|
||||||
|
>
|
||||||
|
<span class="log-output">{{ log.output }}</span>
|
||||||
|
<span class="log-source">({{ log.source }})</span>
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 控制按钮 -->
|
||||||
|
<div class="controls">
|
||||||
|
<button @click="play" :disabled="isAnimating" class="btn-play">
|
||||||
|
{{ isAnimating ? '执行中...' : '▶ 自动演示' }}
|
||||||
|
</button>
|
||||||
|
<button @click="nextStep" :disabled="isAnimating || currentStep >= executionSteps.length" class="btn-step">
|
||||||
|
⏭ 单步执行
|
||||||
|
</button>
|
||||||
|
<button @click="stop" :disabled="!isAnimating" class="btn-stop">
|
||||||
|
⏸ 停止
|
||||||
|
</button>
|
||||||
|
<button @click="reset" :disabled="isAnimating" class="btn-reset">
|
||||||
|
🔄 重置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 执行规则 -->
|
||||||
|
<div class="rules-box">
|
||||||
|
<h4>执行顺序规则</h4>
|
||||||
|
<div class="rule-list">
|
||||||
|
<div class="rule-item">
|
||||||
|
<span class="rule-number">1</span>
|
||||||
|
<span class="rule-text">执行所有同步代码</span>
|
||||||
|
</div>
|
||||||
|
<div class="rule-item">
|
||||||
|
<span class="rule-number">2</span>
|
||||||
|
<span class="rule-text">执行微任务队列中的所有任务</span>
|
||||||
|
</div>
|
||||||
|
<div class="rule-item">
|
||||||
|
<span class="rule-number">3</span>
|
||||||
|
<span class="rule-text">执行一个宏任务</span>
|
||||||
|
</div>
|
||||||
|
<div class="rule-item">
|
||||||
|
<span class="rule-number">4</span>
|
||||||
|
<span class="rule-text">重复步骤 2-3</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="highlight">
|
||||||
|
<strong>核心要点:</strong> 微任务优先级高于宏任务。每次执行完一个宏任务后,都会检查并执行所有微任务,然后再执行下一个宏任务。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.task-queue-demo {
|
||||||
|
border: 1px solid var(--vp-c-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
margin: 24px 0;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-display {
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-item.current {
|
||||||
|
background: rgba(62, 175, 124, 0.2);
|
||||||
|
border-left: 3px solid var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-item.executed {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-number {
|
||||||
|
color: #858585;
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-code {
|
||||||
|
flex: 1;
|
||||||
|
color: #d4d4d4;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-code.type-micro {
|
||||||
|
color: #68d391;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-code.type-macro {
|
||||||
|
color: #f687b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-tag {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-tag.micro {
|
||||||
|
background: rgba(104, 217, 145, 0.2);
|
||||||
|
color: #68d391;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-tag.macro {
|
||||||
|
background: rgba(246, 135, 179, 0.2);
|
||||||
|
color: #f687b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visualization {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.visualization {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-panel,
|
||||||
|
.queue-panel {
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
min-height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-panel.micro {
|
||||||
|
border-color: #68d391;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-panel.macro {
|
||||||
|
border-color: #f687b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-content,
|
||||||
|
.queue-content {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-action {
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid var(--vp-c-brand-1);
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-action.done {
|
||||||
|
border-color: #48bb78;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item {
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item.micro {
|
||||||
|
border-color: #68d391;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item.macro {
|
||||||
|
border-color: #f687b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item.ready {
|
||||||
|
animation: pulse 1s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 0 rgba(104, 217, 145, 0.4); }
|
||||||
|
50% { box-shadow: 0 0 0 6px rgba(104, 217, 145, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-code {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-status {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item-enter-active,
|
||||||
|
.task-item-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-queue {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-log {
|
||||||
|
min-height: 60px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-log {
|
||||||
|
color: var(--vp-c-text-3);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--vp-c-brand-1);
|
||||||
|
color: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-output {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-source {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-enter-active,
|
||||||
|
.output-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-play {
|
||||||
|
background: var(--vp-c-brand-1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-play:hover:not(:disabled) {
|
||||||
|
background: var(--vp-c-brand-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-step {
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-step:hover:not(:disabled) {
|
||||||
|
background: var(--vp-c-bg-soft-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-stop {
|
||||||
|
background: #ed8936;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-stop:hover:not(:disabled) {
|
||||||
|
background: #dd6b20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reset {
|
||||||
|
background: #f56565;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reset:hover:not(:disabled) {
|
||||||
|
background: #e53e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-box {
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-left: 4px solid var(--vp-c-brand-1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-number {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--vp-c-brand-1);
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(62, 175, 124, 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight strong {
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,556 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
// 泛型函数演示
|
||||||
|
const inputValue = ref('')
|
||||||
|
const selectedType = ref('number')
|
||||||
|
const result = ref(null)
|
||||||
|
const showResult = ref(false)
|
||||||
|
|
||||||
|
// 泛型数组反转(不使用 TypeScript 泛型语法)
|
||||||
|
function reverseArray(arr) {
|
||||||
|
return [...arr].reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行反转操作
|
||||||
|
const executeReverse = () => {
|
||||||
|
if (!inputValue.value) {
|
||||||
|
result.value = '请输入内容'
|
||||||
|
showResult.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (selectedType.value) {
|
||||||
|
case 'number':
|
||||||
|
const numArray = inputValue.value.split(',').map(n => parseFloat(n.trim())).filter(n => !isNaN(n))
|
||||||
|
result.value = {
|
||||||
|
input: numArray,
|
||||||
|
output: reverseArray(numArray),
|
||||||
|
type: 'number[]'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'string':
|
||||||
|
const strArray = inputValue.value.split(',').map(s => s.trim())
|
||||||
|
result.value = {
|
||||||
|
input: strArray,
|
||||||
|
output: reverseArray(strArray),
|
||||||
|
type: 'string[]'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result.value = { error: '未知类型' }
|
||||||
|
}
|
||||||
|
showResult.value = true
|
||||||
|
} catch (error) {
|
||||||
|
result.value = { error: '输入格式错误' }
|
||||||
|
showResult.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const reset = () => {
|
||||||
|
inputValue.value = ''
|
||||||
|
result.value = null
|
||||||
|
showResult.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例数据
|
||||||
|
const loadExample = (type) => {
|
||||||
|
selectedType.value = type
|
||||||
|
if (type === 'number') {
|
||||||
|
inputValue.value = '1, 2, 3, 4, 5'
|
||||||
|
} else {
|
||||||
|
inputValue.value = '苹果, 香蕉, 橙子, 葡萄'
|
||||||
|
}
|
||||||
|
result.value = null
|
||||||
|
showResult.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="generic-demo">
|
||||||
|
<h3>🔄 泛型 (Generics) 演示</h3>
|
||||||
|
|
||||||
|
<div class="demo-container">
|
||||||
|
<!-- 泛型概念说明 -->
|
||||||
|
<div class="concept-box">
|
||||||
|
<div class="concept-icon">💡</div>
|
||||||
|
<div class="concept-text">
|
||||||
|
<strong>泛型就像"通用模板"</strong> - 可以处理不同类型的数据,同时保持类型安全
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 泛型函数定义 -->
|
||||||
|
<div class="function-definition">
|
||||||
|
<div class="code-header">
|
||||||
|
<span class="typescript-logo">TS</span>
|
||||||
|
<span>泛型函数定义</span>
|
||||||
|
</div>
|
||||||
|
<pre><code class="typescript">// T 是类型变量,使用时才会确定具体类型
|
||||||
|
function identity<T>(arg: T): T {
|
||||||
|
return arg
|
||||||
|
}
|
||||||
|
|
||||||
|
// 泛型数组反转
|
||||||
|
function reverseArray<T>(arr: T[]): T[] {
|
||||||
|
return [...arr].reverse()
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 交互演示 -->
|
||||||
|
<div class="interactive-demo">
|
||||||
|
<div class="demo-controls">
|
||||||
|
<div class="input-group">
|
||||||
|
<label>选择数据类型:</label>
|
||||||
|
<div class="type-selector">
|
||||||
|
<button
|
||||||
|
:class="['type-btn', { active: selectedType === 'number' }]"
|
||||||
|
@click="selectedType = 'number'"
|
||||||
|
>
|
||||||
|
数字数组
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="['type-btn', { active: selectedType === 'string' }]"
|
||||||
|
@click="selectedType = 'string'"
|
||||||
|
>
|
||||||
|
字符串数组
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label>输入数组(逗号分隔):</label>
|
||||||
|
<input
|
||||||
|
v-model="inputValue"
|
||||||
|
type="text"
|
||||||
|
:placeholder="selectedType === 'number' ? '1, 2, 3, 4, 5' : '苹果, 香蕉, 橙子'"
|
||||||
|
class="text-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="example-buttons">
|
||||||
|
<button @click="loadExample('number')" class="btn-example">
|
||||||
|
加载数字示例
|
||||||
|
</button>
|
||||||
|
<button @click="loadExample('string')" class="btn-example">
|
||||||
|
加载字符串示例
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button @click="executeReverse" class="btn-primary">
|
||||||
|
执行反转
|
||||||
|
</button>
|
||||||
|
<button @click="reset" class="btn-secondary">
|
||||||
|
重置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 结果展示 -->
|
||||||
|
<div v-if="showResult" class="result-display">
|
||||||
|
<div class="result-header">
|
||||||
|
<span class="result-icon">📊</span>
|
||||||
|
<span>执行结果</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="result && !result.error" class="result-content">
|
||||||
|
<div class="result-item">
|
||||||
|
<div class="result-label">输入类型:</div>
|
||||||
|
<div class="result-value type-badge">{{ result.type }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-item">
|
||||||
|
<div class="result-label">输入数组:</div>
|
||||||
|
<div class="result-value array-display">
|
||||||
|
[{{ result.input.join(', ') }}]
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-item">
|
||||||
|
<div class="result-label">输出数组:</div>
|
||||||
|
<div class="result-value array-display output">
|
||||||
|
[{{ result.output.join(', ') }}]
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="type-info">
|
||||||
|
<div class="info-icon">✅</div>
|
||||||
|
<div>类型安全:输入 {{ result.type }},输出 {{ result.type }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="error-display">
|
||||||
|
{{ result?.error || result }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 使用示例 -->
|
||||||
|
<div class="usage-examples">
|
||||||
|
<h4>📝 泛型使用示例</h4>
|
||||||
|
<div class="example-grid">
|
||||||
|
<div class="example-card">
|
||||||
|
<div class="example-title">数字数组</div>
|
||||||
|
<pre><code class="typescript">const nums = [1, 2, 3, 4, 5]
|
||||||
|
const reversed = reverseArray<number>(nums)
|
||||||
|
// 结果: [5, 4, 3, 2, 1]
|
||||||
|
// 类型: number[]</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="example-card">
|
||||||
|
<div class="example-title">字符串数组</div>
|
||||||
|
<pre><code class="typescript">const strs = ["a", "b", "c"]
|
||||||
|
const reversed = reverseArray<string>(strs)
|
||||||
|
// 结果: ["c", "b", "a"]
|
||||||
|
// 类型: string[]</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.generic-demo {
|
||||||
|
border: 1px solid var(--vp-c-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin: 24px 0;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concept-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concept-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concept-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-definition {
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-left: 4px solid #3178c6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typescript-logo {
|
||||||
|
background: #3178c6;
|
||||||
|
color: white;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-definition pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-definition code {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interactive-demo {
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-btn:hover {
|
||||||
|
border-color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-btn.active {
|
||||||
|
border-color: var(--vp-c-brand-1);
|
||||||
|
background: var(--vp-c-brand-1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 18px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--vp-c-brand-1);
|
||||||
|
color: white;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--vp-c-brand-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
border: 1px solid var(--vp-c-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--vp-c-bg-soft-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-example {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-example:hover {
|
||||||
|
background: #bfdbfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-display {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-value {
|
||||||
|
flex: 1;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-badge {
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.array-display {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.array-display.output {
|
||||||
|
background: #d1fae5;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f0fdf4;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #166534;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-display {
|
||||||
|
padding: 12px;
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #991b1b;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-examples {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-examples h4 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card {
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-title {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card code {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,445 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
// 用户数据
|
||||||
|
const user = ref({
|
||||||
|
id: 1,
|
||||||
|
name: '张三',
|
||||||
|
email: 'zhangsan@example.com',
|
||||||
|
age: 25
|
||||||
|
})
|
||||||
|
|
||||||
|
// 显示错误信息
|
||||||
|
const showError = ref(false)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
|
||||||
|
const setMessage = (msg, isError = false) => {
|
||||||
|
errorMessage.value = msg
|
||||||
|
showError.value = isError
|
||||||
|
setTimeout(() => {
|
||||||
|
errorMessage.value = ''
|
||||||
|
showError.value = false
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试添加错误类型的属性
|
||||||
|
const addErrorProperty = () => {
|
||||||
|
showError.value = true
|
||||||
|
errorMessage.value = '❌ TypeScript 错误:类型 "string" 不可分配给类型 "number"'
|
||||||
|
setTimeout(() => {
|
||||||
|
showError.value = false
|
||||||
|
errorMessage.value = ''
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新用户
|
||||||
|
const addNewUser = () => {
|
||||||
|
user.value = {
|
||||||
|
id: 2,
|
||||||
|
name: '李四',
|
||||||
|
email: 'lisi@example.com',
|
||||||
|
age: 30
|
||||||
|
}
|
||||||
|
setMessage('✅ 创建新用户成功!类型检查通过', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改用户年龄
|
||||||
|
const modifyAge = () => {
|
||||||
|
user.value.age = user.value.age + 1
|
||||||
|
setMessage(`✅ 年龄更新为 ${user.value.age}`, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const reset = () => {
|
||||||
|
user.value = {
|
||||||
|
id: 1,
|
||||||
|
name: '张三',
|
||||||
|
email: 'zhangsan@example.com',
|
||||||
|
age: 25
|
||||||
|
}
|
||||||
|
errorMessage.value = ''
|
||||||
|
showError.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="interface-demo">
|
||||||
|
<h3>🎯 Interface 接口演示</h3>
|
||||||
|
|
||||||
|
<div class="demo-container">
|
||||||
|
<!-- 接口定义 -->
|
||||||
|
<div class="interface-definition">
|
||||||
|
<div class="code-header">
|
||||||
|
<span class="typescript-logo">TS</span>
|
||||||
|
<span>User Interface 定义</span>
|
||||||
|
</div>
|
||||||
|
<pre><code class="typescript">interface User {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
age: number
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户对象展示 -->
|
||||||
|
<div class="user-display">
|
||||||
|
<div class="user-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="avatar">👤</div>
|
||||||
|
<div class="user-info">
|
||||||
|
<div class="user-name">{{ user.name }}</div>
|
||||||
|
<div class="user-email">{{ user.email }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-details">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">ID:</span>
|
||||||
|
<span class="value">{{ user.id }}</span>
|
||||||
|
<span class="type-badge">number</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="label">年龄:</span>
|
||||||
|
<span class="value">{{ user.age }}</span>
|
||||||
|
<span class="type-badge">number</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误消息显示 -->
|
||||||
|
<div
|
||||||
|
v-if="errorMessage"
|
||||||
|
:class="['message-box', showError ? 'error' : 'success']"
|
||||||
|
>
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="controls">
|
||||||
|
<button @click="modifyAge" class="btn-primary">
|
||||||
|
增加年龄
|
||||||
|
</button>
|
||||||
|
<button @click="addErrorProperty" class="btn-danger">
|
||||||
|
尝试赋值错误类型
|
||||||
|
</button>
|
||||||
|
<button @click="addNewUser" class="btn-secondary">
|
||||||
|
创建新用户
|
||||||
|
</button>
|
||||||
|
<button @click="reset" class="btn-ghost">重置</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 代码示例 -->
|
||||||
|
<div class="code-examples">
|
||||||
|
<div class="example-item">
|
||||||
|
<div class="example-header">✅ 正确使用</div>
|
||||||
|
<pre><code class="typescript">const user: User = {
|
||||||
|
id: 1,
|
||||||
|
name: "张三",
|
||||||
|
email: "zhangsan@example.com",
|
||||||
|
age: 25
|
||||||
|
} // ✅ 类型完全匹配</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="example-item error">
|
||||||
|
<div class="example-header">❌ 错误使用</div>
|
||||||
|
<pre><code class="typescript">const user: User = {
|
||||||
|
id: 1,
|
||||||
|
name: "张三",
|
||||||
|
email: "zhangsan@example.com",
|
||||||
|
age: "25" // ❌ 错误:age 应该是 number,不是 string
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.interface-demo {
|
||||||
|
border: 1px solid var(--vp-c-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin: 24px 0;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interface-definition {
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-left: 4px solid #3178c6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typescript-logo {
|
||||||
|
background: #3178c6;
|
||||||
|
color: white;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interface-definition pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interface-definition code {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-display {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card {
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid var(--vp-c-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-email {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-badge {
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 3px 8px;
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-box {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
animation: slideDown 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-box.error {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #991b1b;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-box.success {
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: #166534;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 18px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #ef4444;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6b7280;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
border: 1px solid var(--vp-c-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost:hover {
|
||||||
|
background: var(--vp-c-bg-soft-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-examples {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.code-examples {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-item {
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-item.error {
|
||||||
|
border-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-header {
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-item.error .example-header {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-item pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-item code {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,402 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const name = ref('张三')
|
||||||
|
const age = ref(25)
|
||||||
|
const isActive = ref(true)
|
||||||
|
const showError = ref(false)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
|
||||||
|
const setMessage = (msg, isError = false) => {
|
||||||
|
errorMessage.value = msg
|
||||||
|
showError.value = isError
|
||||||
|
setTimeout(() => {
|
||||||
|
errorMessage.value = ''
|
||||||
|
showError.value = false
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifyName = () => {
|
||||||
|
// TypeScript 会在编译时检查类型错误
|
||||||
|
// name.value = 123 // 这行会在 TypeScript 中报错
|
||||||
|
name.value = '李四'
|
||||||
|
setMessage('✅ 修改成功!类型检查通过', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifyAgeError = () => {
|
||||||
|
// 演示类型错误
|
||||||
|
showError.value = true
|
||||||
|
errorMessage.value = '❌ TypeScript 错误:不能将类型 "string" 分配给类型 "number"'
|
||||||
|
setTimeout(() => {
|
||||||
|
showError.value = false
|
||||||
|
errorMessage.value = ''
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleActive = () => {
|
||||||
|
isActive.value = !isActive.value
|
||||||
|
setMessage(`✅ 状态切换为 ${isActive.value}`, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
name.value = '张三'
|
||||||
|
age.value = 25
|
||||||
|
isActive.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
showError.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="type-annotation-demo">
|
||||||
|
<h3>📝 TypeScript 类型注解演示</h3>
|
||||||
|
|
||||||
|
<div class="demo-container">
|
||||||
|
<div class="variables-grid">
|
||||||
|
<!-- String 类型 -->
|
||||||
|
<div class="variable-card string-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="type-badge string">string</span>
|
||||||
|
<span class="var-name">name</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-value">{{ name }}</div>
|
||||||
|
<div class="card-code">
|
||||||
|
<code>const name: string = "{{ name }}"</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Number 类型 -->
|
||||||
|
<div class="variable-card number-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="type-badge number">number</span>
|
||||||
|
<span class="var-name">age</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-value">{{ age }}</div>
|
||||||
|
<div class="card-code">
|
||||||
|
<code>const age: number = {{ age }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Boolean 类型 -->
|
||||||
|
<div class="variable-card boolean-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="type-badge boolean">boolean</span>
|
||||||
|
<span class="var-name">isActive</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-value">
|
||||||
|
<span :class="['status-dot', isActive ? 'active' : 'inactive']"></span>
|
||||||
|
{{ isActive ? 'true' : 'false' }}
|
||||||
|
</div>
|
||||||
|
<div class="card-code">
|
||||||
|
<code>const isActive: boolean = {{ isActive }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误消息显示 -->
|
||||||
|
<div
|
||||||
|
v-if="errorMessage"
|
||||||
|
:class="['message-box', showError ? 'error' : 'success']"
|
||||||
|
>
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="controls">
|
||||||
|
<button @click="modifyName" class="btn-primary">
|
||||||
|
修改 name (正确)
|
||||||
|
</button>
|
||||||
|
<button @click="modifyAgeError" class="btn-danger">
|
||||||
|
赋值错误类型
|
||||||
|
</button>
|
||||||
|
<button @click="toggleActive" class="btn-secondary">
|
||||||
|
切换 isActive
|
||||||
|
</button>
|
||||||
|
<button @click="reset" class="btn-ghost">重置</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 代码对比 -->
|
||||||
|
<div class="code-comparison">
|
||||||
|
<div class="code-panel javascript">
|
||||||
|
<div class="panel-header">JavaScript (无类型检查)</div>
|
||||||
|
<pre><code>let name = "张三"
|
||||||
|
name = 123 // ✅ 运行时才会报错(可能很晚才发现)</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="code-panel typescript">
|
||||||
|
<div class="panel-header">TypeScript (编译时检查)</div>
|
||||||
|
<pre><code>let name: string = "张三"
|
||||||
|
name = 123 // ❌ 编译时立即报错(写代码时就发现)</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.type-annotation-demo {
|
||||||
|
border: 1px solid var(--vp-c-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin: 24px 0;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variables-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variable-card {
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variable-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-badge {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-badge.string {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-badge.number {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-badge.boolean {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.var-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.active {
|
||||||
|
background: #10b981;
|
||||||
|
box-shadow: 0 0 8px rgba(16, 185, 129, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.inactive {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-code {
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-code code {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: #d4d4d4;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-box {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
animation: slideDown 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-box.error {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #991b1b;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-box.success {
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: #166534;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 18px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #ef4444;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6b7280;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
border: 1px solid var(--vp-c-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost:hover {
|
||||||
|
background: var(--vp-c-bg-soft-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-comparison {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.code-comparison {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-panel {
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-panel.javascript {
|
||||||
|
border-color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-panel.typescript {
|
||||||
|
border-color: #3178c6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-panel.javascript .panel-header {
|
||||||
|
background: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-panel.typescript .panel-header {
|
||||||
|
background: #3178c6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-panel pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-panel code {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,618 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
// 类型推断演示
|
||||||
|
const codeExamples = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
code: 'let name = "张三"',
|
||||||
|
inferredType: 'string',
|
||||||
|
explanation: 'TypeScript 根据赋值的字符串推断出 name 的类型是 string'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
code: 'let age = 25',
|
||||||
|
inferredType: 'number',
|
||||||
|
explanation: 'TypeScript 根据数字字面量推断出 age 的类型是 number'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
code: 'let isActive = true',
|
||||||
|
inferredType: 'boolean',
|
||||||
|
explanation: 'TypeScript 根据布尔值推断出 isActive 的类型是 boolean'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
code: 'let numbers = [1, 2, 3]',
|
||||||
|
inferredType: 'number[]',
|
||||||
|
explanation: 'TypeScript 推断出这是一个数字数组'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const currentExample = ref(codeExamples.value[0])
|
||||||
|
|
||||||
|
// 显示类型错误
|
||||||
|
const showError = ref(false)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
|
||||||
|
const setMessage = (msg, isError = false) => {
|
||||||
|
errorMessage.value = msg
|
||||||
|
showError.value = isError
|
||||||
|
setTimeout(() => {
|
||||||
|
errorMessage.value = ''
|
||||||
|
showError.value = false
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换示例
|
||||||
|
const selectExample = (example) => {
|
||||||
|
currentExample.value = example
|
||||||
|
errorMessage.value = ''
|
||||||
|
showError.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试类型错误
|
||||||
|
const tryTypeError = () => {
|
||||||
|
showError.value = true
|
||||||
|
errorMessage.value = `❌ TypeScript 错误:不能将类型 "number" 分配给类型 "${currentExample.value.inferredType}"`
|
||||||
|
setTimeout(() => {
|
||||||
|
showError.value = false
|
||||||
|
errorMessage.value = ''
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最佳实践示例
|
||||||
|
const bestPractices = ref([
|
||||||
|
{
|
||||||
|
title: '何时使用类型推断',
|
||||||
|
items: [
|
||||||
|
'变量初始化时有明确的值',
|
||||||
|
'函数返回值可以明显推断',
|
||||||
|
'简单的字面量赋值'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '何时需要显式注解',
|
||||||
|
items: [
|
||||||
|
'函数参数(必须)',
|
||||||
|
'对象或数组的复杂结构',
|
||||||
|
'无法从初始值推断类型',
|
||||||
|
'需要明确的类型约束'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 代码对比
|
||||||
|
const codeComparisons = ref([
|
||||||
|
{
|
||||||
|
scenario: '函数返回值',
|
||||||
|
withInference: 'function add(a: number, b: number) {\n return a + b // 推断为 number\n}',
|
||||||
|
withAnnotation: 'function add(a: number, b: number): number {\n return a + b\n}',
|
||||||
|
recommendation: '推荐使用推断'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: '复杂对象',
|
||||||
|
withInference: 'const user = {\n name: "张三",\n age: 25,\n email: "test@example.com"\n} // 类型自动推断',
|
||||||
|
withAnnotation: 'interface User {\n name: string\n age: number\n email: string\n}\n\nconst user: User = { ... }',
|
||||||
|
recommendation: '复杂结构建议用接口'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="type-inference-demo">
|
||||||
|
<h3>🔮 类型推断演示</h3>
|
||||||
|
|
||||||
|
<div class="demo-container">
|
||||||
|
<!-- 概念说明 -->
|
||||||
|
<div class="concept-section">
|
||||||
|
<div class="concept-card">
|
||||||
|
<div class="concept-icon">🧠</div>
|
||||||
|
<div class="concept-content">
|
||||||
|
<h4>什么是类型推断?</h4>
|
||||||
|
<p>TypeScript 很聪明,它能根据你写的代码自动推断出变量的类型,不需要每次都手动标注。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 示例选择器 -->
|
||||||
|
<div class="example-selector">
|
||||||
|
<h4>选择一个示例看看类型推断是如何工作的:</h4>
|
||||||
|
<div class="examples-grid">
|
||||||
|
<div
|
||||||
|
v-for="example in codeExamples"
|
||||||
|
:key="example.id"
|
||||||
|
:class="['example-card', { active: currentExample.id === example.id }]"
|
||||||
|
@click="selectExample(example)"
|
||||||
|
>
|
||||||
|
<div class="example-code">{{ example.code }}</div>
|
||||||
|
<div class="example-type">→ {{ example.inferredType }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 当前示例详情 -->
|
||||||
|
<div class="current-example">
|
||||||
|
<div class="example-display">
|
||||||
|
<div class="code-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="code-icon">💻</span>
|
||||||
|
<span>代码</span>
|
||||||
|
</div>
|
||||||
|
<pre><code class="typescript">{{ currentExample.code }}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inference-arrow">→</div>
|
||||||
|
|
||||||
|
<div class="type-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="type-icon">🏷️</span>
|
||||||
|
<span>推断的类型</span>
|
||||||
|
</div>
|
||||||
|
<div class="inferred-type">{{ currentExample.inferredType }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="explanation">
|
||||||
|
<div class="explanation-icon">💡</div>
|
||||||
|
<div class="explanation-text">{{ currentExample.explanation }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误消息 -->
|
||||||
|
<div
|
||||||
|
v-if="errorMessage"
|
||||||
|
:class="['message-box', showError ? 'error' : 'success']"
|
||||||
|
>
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="controls">
|
||||||
|
<button @click="tryTypeError" class="btn-danger">
|
||||||
|
尝试类型错误
|
||||||
|
</button>
|
||||||
|
<button @click="showError = false; errorMessage = ''" class="btn-secondary">
|
||||||
|
清除消息
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 最佳实践 -->
|
||||||
|
<div class="best-practices">
|
||||||
|
<h4>📚 最佳实践</h4>
|
||||||
|
<div class="practices-grid">
|
||||||
|
<div
|
||||||
|
v-for="(practice, index) in bestPractices"
|
||||||
|
:key="index"
|
||||||
|
class="practice-card"
|
||||||
|
>
|
||||||
|
<div class="practice-header">{{ practice.title }}</div>
|
||||||
|
<ul class="practice-list">
|
||||||
|
<li v-for="(item, i) in practice.items" :key="i">
|
||||||
|
{{ item }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 代码对比 -->
|
||||||
|
<div class="comparisons">
|
||||||
|
<h4>🔄 类型推断 vs 显式注解</h4>
|
||||||
|
<div
|
||||||
|
v-for="(comparison, index) in codeComparisons"
|
||||||
|
:key="index"
|
||||||
|
class="comparison-item"
|
||||||
|
>
|
||||||
|
<div class="comparison-scenario">{{ comparison.scenario }}</div>
|
||||||
|
<div class="comparison-codes">
|
||||||
|
<div class="comparison-code">
|
||||||
|
<div class="code-label">使用推断</div>
|
||||||
|
<pre><code class="typescript">{{ comparison.withInference }}</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="comparison-code">
|
||||||
|
<div class="code-label">显式注解</div>
|
||||||
|
<pre><code class="typescript">{{ comparison.withAnnotation }}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="comparison-recommendation">
|
||||||
|
<span class="recommendation-icon">✅</span>
|
||||||
|
{{ comparison.recommendation }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.type-inference-demo {
|
||||||
|
border: 1px solid var(--vp-c-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin: 24px 0;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3, h4 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concept-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concept-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concept-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concept-content h4 {
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.concept-content p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-selector {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.examples-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card {
|
||||||
|
padding: 16px;
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card:hover {
|
||||||
|
border-color: var(--vp-c-brand-1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-card.active {
|
||||||
|
border-color: var(--vp-c-brand-1);
|
||||||
|
background: #dbeafe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-code {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-type {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-example {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.example-display {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inference-arrow {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-panel, .type-panel {
|
||||||
|
flex: 1;
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-icon, .type-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-panel pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-panel code {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inference-arrow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 32px;
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inferred-type {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
background: #dbeafe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-box {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
animation: slideDown 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-box.error {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #991b1b;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-box.success {
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: #166534;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 18px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #ef4444;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
border: 1px solid var(--vp-c-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--vp-c-bg-soft-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.best-practices {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practices-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-card {
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-header {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-list li {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comparisons {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comparison-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid var(--vp-c-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--vp-c-bg-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comparison-scenario {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comparison-codes {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.comparison-codes {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comparison-code {
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-label {
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: #374151;
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comparison-code pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comparison-code code {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comparison-recommendation {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f0fdf4;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #166534;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommendation-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -494,6 +494,19 @@ import ThisContextDemo from './components/appendix/javascript-intro/ThisContextD
|
|||||||
import PrototypeDemo from './components/appendix/javascript-intro/PrototypeDemo.vue'
|
import PrototypeDemo from './components/appendix/javascript-intro/PrototypeDemo.vue'
|
||||||
import AsyncDemo from './components/appendix/javascript-intro/AsyncDemo.vue'
|
import AsyncDemo from './components/appendix/javascript-intro/AsyncDemo.vue'
|
||||||
|
|
||||||
|
// JavaScript Runtime Components
|
||||||
|
import RuntimeEnvironmentDemo from './components/appendix/js-runtime/RuntimeEnvironmentDemo.vue'
|
||||||
|
import CallStackDemo from './components/appendix/js-runtime/CallStackDemo.vue'
|
||||||
|
import TaskQueueDemo from './components/appendix/js-runtime/TaskQueueDemo.vue'
|
||||||
|
import MemoryLeakDemo from './components/appendix/js-runtime/MemoryLeakDemo.vue'
|
||||||
|
import GarbageCollectionDemo from './components/appendix/js-runtime/GarbageCollectionDemo.vue'
|
||||||
|
|
||||||
|
// TypeScript Intro Components
|
||||||
|
import TypeAnnotationDemo from './components/appendix/typescript-intro/TypeAnnotationDemo.vue'
|
||||||
|
import InterfaceDemo from './components/appendix/typescript-intro/InterfaceDemo.vue'
|
||||||
|
import GenericDemo from './components/appendix/typescript-intro/GenericDemo.vue'
|
||||||
|
import TypeInferenceDemo from './components/appendix/typescript-intro/TypeInferenceDemo.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
extends: DefaultTheme,
|
extends: DefaultTheme,
|
||||||
Layout,
|
Layout,
|
||||||
@@ -935,6 +948,7 @@ export default {
|
|||||||
app.component('MacroMicroTaskDemo', MacroMicroTaskDemo)
|
app.component('MacroMicroTaskDemo', MacroMicroTaskDemo)
|
||||||
app.component('RenderingPerformanceDemo', RenderingPerformanceDemo)
|
app.component('RenderingPerformanceDemo', RenderingPerformanceDemo)
|
||||||
app.component('RenderingPipelineDemo', RenderingPipelineDemo)
|
app.component('RenderingPipelineDemo', RenderingPipelineDemo)
|
||||||
|
app.component('EventLoopDemo', JSEventLoopDemo) // Alias for browser rendering context
|
||||||
|
|
||||||
// Cache Design Extra Components Registration
|
// Cache Design Extra Components Registration
|
||||||
app.component('CacheArchitectureOverview', CacheArchitectureOverview)
|
app.component('CacheArchitectureOverview', CacheArchitectureOverview)
|
||||||
@@ -986,6 +1000,19 @@ export default {
|
|||||||
app.component('DOMTreeDemo', DOMTreeDemo)
|
app.component('DOMTreeDemo', DOMTreeDemo)
|
||||||
app.component('AsyncRestaurantDemo', AsyncRestaurantDemo)
|
app.component('AsyncRestaurantDemo', AsyncRestaurantDemo)
|
||||||
app.component('JSEventLoopDemo', JSEventLoopDemo)
|
app.component('JSEventLoopDemo', JSEventLoopDemo)
|
||||||
|
|
||||||
|
// JavaScript Runtime Components Registration
|
||||||
|
app.component('RuntimeEnvironmentDemo', RuntimeEnvironmentDemo)
|
||||||
|
app.component('CallStackDemo', CallStackDemo)
|
||||||
|
app.component('TaskQueueDemo', TaskQueueDemo)
|
||||||
|
app.component('MemoryLeakDemo', MemoryLeakDemo)
|
||||||
|
app.component('GarbageCollectionDemo', GarbageCollectionDemo)
|
||||||
|
|
||||||
|
// TypeScript Intro Components Registration
|
||||||
|
app.component('TypeAnnotationDemo', TypeAnnotationDemo)
|
||||||
|
app.component('InterfaceDemo', InterfaceDemo)
|
||||||
|
app.component('GenericDemo', GenericDemo)
|
||||||
|
app.component('TypeInferenceDemo', TypeInferenceDemo)
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|||||||
@@ -3,6 +3,22 @@
|
|||||||
**为什么有些网页流畅如丝,有些却卡成PPT?** 浏览器是怎么把一堆HTML、CSS、JavaScript代码变成你眼前看到的网页的?本章将带你深入浏览器的"车间",理解它的工作流程,从而写出性能更好的网页。
|
**为什么有些网页流畅如丝,有些却卡成PPT?** 浏览器是怎么把一堆HTML、CSS、JavaScript代码变成你眼前看到的网页的?本章将带你深入浏览器的"车间",理解它的工作流程,从而写出性能更好的网页。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
**这篇文章会带你学什么?**
|
||||||
|
|
||||||
|
| 章节 | 内容 | 学完能干嘛 |
|
||||||
|
|-----|------|-----------|
|
||||||
|
| **第 1 章** | 为什么要理解渲染管线 | 理解性能优化的必要性 |
|
||||||
|
| **第 2 章** | 渲染管线的五个阶段 | 掌握浏览器渲染的基本流程 |
|
||||||
|
| **第 3 章** | 构建DOM树和CSSOM树 | 理解HTML和CSS如何被解析 |
|
||||||
|
| **第 4 章** | 构建渲染树 | 知道哪些元素会被渲染 |
|
||||||
|
| **第 5 章** | 布局与重排 | 避免触发昂贵的布局计算 |
|
||||||
|
| **第 6 章** | 绘制与重绘 | 减少不必要的绘制操作 |
|
||||||
|
| **第 7 章** | 合成与GPU加速 | 利用GPU提升动画性能 |
|
||||||
|
| **第 8 章** | 事件循环 | 理解JavaScript的执行机制 |
|
||||||
|
| **第 9 章** | 性能优化实战 | 掌握常用的性能优化技巧 |
|
||||||
|
|
||||||
|
每一章都从"理解原理"开始,不需要你会手写优化代码。遇到性能问题时,随时回来查就行。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 为什么要理解"渲染管线"?
|
## 1. 为什么要理解"渲染管线"?
|
||||||
@@ -933,7 +949,41 @@ lazyImages.forEach(img => imageObserver.observe(img))
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. 总结:渲染管线优化的本质
|
## 10. 你现在应该能识别的性能问题
|
||||||
|
|
||||||
|
理解了浏览器的渲染管线后,你应该能识别以下常见的性能问题:
|
||||||
|
|
||||||
|
| 问题代码 | 问题所在 | 如何描述给AI |
|
||||||
|
|---------|---------|-------------|
|
||||||
|
| `element.style.width = ...` | 在循环中频繁修改宽度 | "这里会触发多次重排,请改用transform或者批量处理" |
|
||||||
|
| `height = element.offsetHeight` | 在写入后立即读取布局属性 | "这是强制同步布局,请分离读写操作" |
|
||||||
|
| `element.className = ...` | 频繁修改class触发样式重新计算 | "用classList.add/remove代替,减少样式计算" |
|
||||||
|
| 动画用`width`/`left` | 触发重排和重绘,性能差 | "改用transform和opacity做动画" |
|
||||||
|
| 给所有元素加`translateZ(0)` | 滥用GPU加速导致内存爆炸 | "只给需要动画的元素开启GPU加速" |
|
||||||
|
| 列表项10000个全渲染 | DOM节点过多导致卡顿 | "实现虚拟滚动,只渲染可见区域" |
|
||||||
|
| scroll事件里直接操作DOM | 触发频率太高导致卡顿 | "用requestAnimationFrame或节流优化" |
|
||||||
|
| `box-shadow`做hover动画 | 复杂的阴影计算很慢 | "改用transform或伪元素,避免动画阴影" |
|
||||||
|
|
||||||
|
**如果你认真读了每一章的"踩坑实录",你还掌握了这些核心概念:**
|
||||||
|
|
||||||
|
- **渲染管线五阶段**:DOM/CSSOM → 渲染树 → 布局 → 绘制 → 合成
|
||||||
|
- **重排 vs 重绘**:重排最昂贵(几何变化),重绘次之(外观变化)
|
||||||
|
- **强制同步布局**:读写交替会导致布局抖动,必须分离
|
||||||
|
- **GPU加速**:transform和opacity由GPU处理,性能最佳
|
||||||
|
- **事件循环**:JavaScript是单线程的,通过任务队列实现异步
|
||||||
|
|
||||||
|
这些概念会帮你快速定位性能瓶颈。
|
||||||
|
|
||||||
|
::: info 💡 遇到性能问题时这样跟AI说
|
||||||
|
- "动画卡顿,检查是否触发了重排或重绘"
|
||||||
|
- "滚动性能差,可能需要节流或requestAnimationFrame"
|
||||||
|
- "列表数据量大时卡顿,需要虚拟滚动"
|
||||||
|
- "频繁修改样式导致性能问题,请用transform优化"
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 总结:渲染管线优化的本质
|
||||||
|
|
||||||
通过本文的学习,我们可以得出以下核心结论:
|
通过本文的学习,我们可以得出以下核心结论:
|
||||||
|
|
||||||
@@ -948,7 +998,7 @@ lazyImages.forEach(img => imageObserver.observe(img))
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. 名词对照表
|
## 12. 名词对照表
|
||||||
|
|
||||||
| 英文术语 | 中文对照 | 解释 |
|
| 英文术语 | 中文对照 | 解释 |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
|
|||||||
@@ -1,18 +1,77 @@
|
|||||||
# 浏览器是一个操作系统
|
# 浏览器是一个操作系统
|
||||||
|
|
||||||
> **学习指南**:本章节无需编程基础。我们将用**"网购"**的生活化比喻,配合**真实的技术过程**,带你一步步理解浏览器如何将一行网址变成丰富多彩的页面。
|
::: tip 前言
|
||||||
|
你每天都在用浏览器——看视频、刷新闻、在线办公。但你有没有想过:**当你在地址栏输入一个网址并按下回车,背后发生了什么?**
|
||||||
|
|
||||||
|
这篇文章会用**"网购"**的生活化比喻,配合**真实的技术过程**,带你一步步理解浏览器如何将一行网址变成丰富多彩的页面。
|
||||||
|
|
||||||
|
读完这篇,你就能:
|
||||||
|
- 理解从输入网址到显示页面的完整流程
|
||||||
|
- 掌握 URL、DNS、TCP、HTTP 等核心概念
|
||||||
|
- 了解浏览器如何渲染页面
|
||||||
|
- 知道静态网站和动态网站的区别
|
||||||
|
|
||||||
|
**无需编程基础**,只需要你平时网购的经验即可。
|
||||||
|
:::
|
||||||
|
|
||||||
|
**这篇文章会带你学什么?**
|
||||||
|
|
||||||
|
| 章节 | 内容 | 核心概念 |
|
||||||
|
|-----|------|---------|
|
||||||
|
| **第 1 章** | URL 解析 | 网址的结构和作用 |
|
||||||
|
| **第 2 章** | DNS 查询 | 域名如何转换成 IP 地址 |
|
||||||
|
| **第 3 章** | TCP 握手 | 如何建立可靠的连接 |
|
||||||
|
| **第 4 章** | HTTP 通信 | 浏览器和服务器如何对话 |
|
||||||
|
| **第 5 章** | 浏览器渲染 | 代码如何变成画面 |
|
||||||
|
| **第 6 章** | 静态 vs 动态 | 网页内容的生成方式 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 0. 引言:当你按下回车键的那一刻
|
## 0. 引言:当你按下回车键的那一刻
|
||||||
|
|
||||||
想象你正在进行一次**网购**。你需要:
|
::: tip 🤔 核心问题
|
||||||
|
**当你在浏览器输入网址并按下回车,后台发生了什么?** 为什么有的网页打开很快,有的很慢?为什么有时候会出现"找不到服务器"的错误?
|
||||||
|
:::
|
||||||
|
|
||||||
1. **填写订单**(选好商品,确认收货地址)
|
### 生活比喻:一次网购之旅
|
||||||
2. **系统查找仓库**(根据店铺名找到具体的发货仓库)
|
|
||||||
3. **建立物流通道**(确保仓库正常营业且能发货)
|
想象你正在进行一次**网购**。整个过程可以分为 5 个步骤:
|
||||||
4. **仓库发货**(快递员把包裹送上门)
|
|
||||||
5. **拆箱体验**(打开包裹,看到心仪的商品)
|
<div style="display: flex; gap: 20px; margin: 20px 0;">
|
||||||
|
<div style="flex: 1; padding: 16px; background: var(--vp-c-bg-alt); border-radius: 12px;">
|
||||||
|
|
||||||
|
**🛒 第 1 步:填写订单**
|
||||||
|
选好商品,确认收货地址
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; padding: 16px; background: var(--vp-c-bg-alt); border-radius: 12px;">
|
||||||
|
|
||||||
|
**🗺️ 第 2 步:查找仓库**
|
||||||
|
系统找到具体的发货仓库
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; padding: 16px; background: var(--vp-c-bg-alt); border-radius: 12px;">
|
||||||
|
|
||||||
|
**📞 第 3 步:建立通道**
|
||||||
|
确认仓库营业且能发货
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 20px; margin: 20px 0;">
|
||||||
|
<div style="flex: 1; padding: 16px; background: var(--vp-c-bg-alt); border-radius: 12px;">
|
||||||
|
|
||||||
|
**🚚 第 4 步:仓库发货**
|
||||||
|
快递员把包裹送上门
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; padding: 16px; background: var(--vp-c-bg-alt); border-radius: 12px;">
|
||||||
|
|
||||||
|
**🎁 第 5 步:拆箱体验**
|
||||||
|
打开包裹,看到心仪的商品
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
**访问网页的过程和网购惊人地相似!**
|
**访问网页的过程和网购惊人地相似!**
|
||||||
|
|
||||||
@@ -20,10 +79,18 @@
|
|||||||
|
|
||||||
<UrlToBrowserQuickStart />
|
<UrlToBrowserQuickStart />
|
||||||
|
|
||||||
|
::: info 💡 核心启示
|
||||||
|
理解浏览器工作原理的关键是:**把复杂的技术过程映射到熟悉的生活场景**。网购的 5 个步骤完美对应了浏览器访问网页的 5 个技术阶段。
|
||||||
|
:::
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 第一步:填写"订单" —— URL 解析
|
## 1. 第一步:填写"订单" —— URL 解析
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**为什么网址要写成这样?** `https://www.example.com:8080/path/page.html?id=123#section` — 这串字符到底有什么含义?
|
||||||
|
:::
|
||||||
|
|
||||||
### 生活比喻:填写购物单
|
### 生活比喻:填写购物单
|
||||||
|
|
||||||
假设你只在订单上写"买鞋子",仓库肯定不知道发哪双。你需要写清楚:
|
假设你只在订单上写"买鞋子",仓库肯定不知道发哪双。你需要写清楚:
|
||||||
@@ -49,12 +116,18 @@
|
|||||||
|
|
||||||
<UrlParserDemo />
|
<UrlParserDemo />
|
||||||
|
|
||||||
> **关键理解**:URL 的存在是为了让**人类**能记住和输入。计算机最终需要的是 **IP 地址**(就像快递员最终需要的是具体的仓库地址,而不是"Nike 官方店"这个名字)。
|
::: info 💡 关键理解
|
||||||
|
URL 的存在是为了让**人类**能记住和输入。计算机最终需要的是 **IP 地址**(就像快递员最终需要的是具体的仓库地址,而不是"Nike 官方店"这个名字)。
|
||||||
|
:::
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 第二步:查"地址簿" —— DNS 查询
|
## 2. 第二步:查"地址簿" —— DNS 查询
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**为什么浏览器能找到网站?** 你输入的是人类可读的域名(如 `baidu.com`),但计算机真正需要的是数字地址(IP)。这中间发生了什么?
|
||||||
|
:::
|
||||||
|
|
||||||
### 生活比喻:查仓库地址
|
### 生活比喻:查仓库地址
|
||||||
|
|
||||||
你下单写的是"Nike 官方店",但物流系统不知道仓库在哪。它需要查地址簿:
|
你下单写的是"Nike 官方店",但物流系统不知道仓库在哪。它需要查地址簿:
|
||||||
@@ -90,12 +163,20 @@
|
|||||||
|
|
||||||
<DnsLookupDemo />
|
<DnsLookupDemo />
|
||||||
|
|
||||||
> **为什么需要这么多层?** 想象一下如果全世界只有一个地址簿,几十亿人同时查,早就崩溃了。分层设计让每个层级只管理自己的"辖区",既高效又可靠。
|
::: info 💡 为什么需要这么多层?
|
||||||
|
想象一下如果全世界只有一个地址簿,几十亿人同时查,早就崩溃了。分层设计让每个层级只管理自己的"辖区",既高效又可靠。
|
||||||
|
|
||||||
|
这就是互联网设计的核心思想:**分布式系统**。
|
||||||
|
:::
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 第三步:打电话确认 —— TCP 三次握手
|
## 3. 第三步:打电话确认 —— TCP 三次握手
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**为什么需要"三次握手"?** 找到服务器地址后,为什么不能直接发送数据?为什么要先进行三次通信?
|
||||||
|
:::
|
||||||
|
|
||||||
### 生活比喻:建立物流通道
|
### 生活比喻:建立物流通道
|
||||||
|
|
||||||
假设物流车直接开到仓库,结果:
|
假设物流车直接开到仓库,结果:
|
||||||
@@ -145,6 +226,10 @@
|
|||||||
|
|
||||||
## 4. 第四步:"买家"和"商家"的对话 —— HTTP 请求与响应
|
## 4. 第四步:"买家"和"商家"的对话 —— HTTP 请求与响应
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**浏览器和服务器在说什么?** 建立连接后,浏览器如何"告诉"服务器它想要什么?服务器又如何"回应"?
|
||||||
|
:::
|
||||||
|
|
||||||
### 生活比喻:仓库发货
|
### 生活比喻:仓库发货
|
||||||
|
|
||||||
物流车到达仓库:"这是订单(HTTP请求),**我要取回商品(网页 HTML 源代码)!**"
|
物流车到达仓库:"这是订单(HTTP请求),**我要取回商品(网页 HTML 源代码)!**"
|
||||||
@@ -225,6 +310,10 @@ Set-Cookie: user_id=xyz789 ← 设置 Cookie
|
|||||||
## 5. 第五步:拆开"包裹" —— 浏览器渲染
|
## 5. 第五步:拆开"包裹" —— 浏览器渲染
|
||||||
|
|
||||||
::: tip 🤔 核心问题
|
::: tip 🤔 核心问题
|
||||||
|
**代码怎么变成画面?** 服务器发来的是枯燥的 HTML/CSS/JavaScript 代码,浏览器如何把它们变成丰富多彩的网页?
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 生活比喻:拆箱与组装
|
||||||
|
|
||||||
你终于收到了快递包裹(HTTP 响应),但打开一看,里面不是现成的家具,而是一堆**零件**(HTML)和一本**组装说明书**(CSS)。作为"买家"(浏览器),你需要亲自动手组装:
|
你终于收到了快递包裹(HTTP 响应),但打开一看,里面不是现成的家具,而是一堆**零件**(HTML)和一本**组装说明书**(CSS)。作为"买家"(浏览器),你需要亲自动手组装:
|
||||||
|
|
||||||
@@ -298,14 +387,18 @@ DOM 树 + CSSOM 树 = **渲染树 (Render Tree)**。
|
|||||||
<BrowserRenderingDemo />
|
<BrowserRenderingDemo />
|
||||||
|
|
||||||
::: info 💡 你知道吗?
|
::: info 💡 你知道吗?
|
||||||
>
|
**布局和绘制**是浏览器最忙碌的时候。网页里的元素越多、结构越复杂,浏览器就需要花更多时间来计算位置和上色。这就是为什么有的复杂网页打开会卡顿的原因。
|
||||||
> **布局和绘制**是浏览器最忙碌的时候。网页里的元素越多、结构越复杂,浏览器就需要花更多时间来计算位置和上色。这就是为什么有的复杂网页打开会卡顿的原因。
|
:::
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5.5 网页是怎么"生成"的?静态网站 vs 动态网站
|
## 5.5 网页是怎么"生成"的?静态网站 vs 动态网站
|
||||||
|
|
||||||
::: tip 🤔 核心问题
|
::: tip 🤔 核心问题
|
||||||
|
**网页内容从哪里来?** 前面我们讲了浏览器如何渲染页面,但服务器上的 HTML 文件是怎么来的?是提前做好还是现做?
|
||||||
|
:::
|
||||||
|
|
||||||
|
前面我们讲的都是浏览器如何"拆开包裹"——把服务器发来的 HTML/CSS/JS 渲染成页面。但你有没有想过一个问题:**服务器上那个 HTML 文件是怎么来的?**
|
||||||
|
|
||||||
答案是:**有两种方式**,这就是静态网站和动态网站的区别。
|
答案是:**有两种方式**,这就是静态网站和动态网站的区别。
|
||||||
|
|
||||||
@@ -381,6 +474,14 @@ DOM 树 + CSSOM 树 = **渲染树 (Render Tree)**。
|
|||||||
## 6. 总结:一次完整的"网购"之旅
|
## 6. 总结:一次完整的"网购"之旅
|
||||||
|
|
||||||
::: tip 🎉 学完本章,你应该能
|
::: tip 🎉 学完本章,你应该能
|
||||||
|
- 解释从输入网址到显示页面的完整流程
|
||||||
|
- 理解 URL、DNS、TCP、HTTP 的作用和关系
|
||||||
|
- 知道浏览器如何渲染页面
|
||||||
|
- 区分静态网站和动态网站
|
||||||
|
- 用生活化比喻向他人解释浏览器工作原理
|
||||||
|
:::
|
||||||
|
|
||||||
|
让我们回顾整个旅程:
|
||||||
|
|
||||||
| 阶段 | 技术术语 | 网购类比 | 核心任务 | 关键技术 |
|
| 阶段 | 技术术语 | 网购类比 | 核心任务 | 关键技术 |
|
||||||
| ----------- | ---------- | -------- | ------------------ | ------------------------------ |
|
| ----------- | ---------- | -------- | ------------------ | ------------------------------ |
|
||||||
@@ -403,6 +504,13 @@ DOM 树 + CSSOM 树 = **渲染树 (Render Tree)**。
|
|||||||
这就是互联网的魅力:**复杂的技术,简单的体验**。
|
这就是互联网的魅力:**复杂的技术,简单的体验**。
|
||||||
|
|
||||||
::: info 💡 进阶学习
|
::: info 💡 进阶学习
|
||||||
|
如果你想深入了解某个环节,可以参考:
|
||||||
|
- **API 开发**:[API 简介](./api-intro.md) - 学习如何设计和使用 API
|
||||||
|
- **前端性能**:[前端性能优化](./frontend-performance.md) - 学习如何优化网页加载速度
|
||||||
|
- **浏览器渲染**:[浏览器渲染管道](./browser-rendering-pipeline.md) - 深入了解渲染细节
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 7. 名词速查表 (Glossary)
|
## 7. 名词速查表 (Glossary)
|
||||||
|
|
||||||
@@ -424,4 +532,13 @@ DOM 树 + CSSOM 树 = **渲染树 (Render Tree)**。
|
|||||||
---
|
---
|
||||||
|
|
||||||
::: tip 🎓 恭喜
|
::: tip 🎓 恭喜
|
||||||
> **恭喜!** 现在当你再次在地址栏输入网址时,你已经能看到屏幕背后的那个忙碌而精彩的数字世界了。
|
现在当你再次在地址栏输入网址并按下回车时,你已经能看到屏幕背后的那个忙碌而精彩的数字世界了。
|
||||||
|
|
||||||
|
你理解了:
|
||||||
|
- 为什么有时候网页打不开(DNS 解析失败、服务器宕机)
|
||||||
|
- 为什么有的网页快、有的慢(网络延迟、服务器性能、页面复杂度)
|
||||||
|
- 浏览器是如何把代码变成画面的(渲染管道)
|
||||||
|
|
||||||
|
**这就是理解技术原理的价值** — 遇到问题时,你能知道从哪里找原因,而不是束手无策。
|
||||||
|
:::
|
||||||
|
:::
|
||||||
|
|||||||
@@ -1,69 +1,101 @@
|
|||||||
# 前端框架对比(React / Vue / Svelte / Angular)
|
# 前端框架深度指南
|
||||||
::: tip 🎯 核心问题
|
|
||||||
**为什么网页越来越复杂?前端技术为什么要不断演进?** 这个问题会带你理解从简单网页到现代 Web 应用的技术演变之路。
|
::: tip 前言
|
||||||
|
你已经学会了 HTML、CSS 和 JavaScript 基础,能做出简单的网页了。但随着网页功能越来越复杂,你可能会发现:用原生 JavaScript 写代码变得很难维护,改一处要动很多地方,多人协作时经常冲突。
|
||||||
|
|
||||||
|
这就是我们需要前端框架的原因——它让代码更有条理、更易维护、更高效开发。在 vibecoding 里,AI 会帮你写大部分代码。但你至少得能看懂不同框架的代码风格,知道它们的优缺点,这样 AI 才能帮你选择最合适的技术栈。
|
||||||
|
|
||||||
|
读完这篇,你就能:
|
||||||
|
- 理解前端技术为什么要不断演进
|
||||||
|
- 知道 Vue、React、Svelte、Angular 各有什么特点
|
||||||
|
- 懂得"数据驱动"、"组件化"这些核心概念
|
||||||
|
- 能根据项目选择合适的框架
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
**这篇文章会带你学什么?**
|
||||||
|
|
||||||
|
| 章节 | 内容 | 学完能干嘛 |
|
||||||
|
|-----|------|-----------|
|
||||||
|
| **第 1 章** | 为什么要关注前端演进 | 明白技术演进是为了解决什么问题 |
|
||||||
|
| **第 2 章** | 静态网页时代 | 了解最早期的网页开发方式 |
|
||||||
|
| **第 3 章** | jQuery 时代 | 理解"命令式"编程的痛点 |
|
||||||
|
| **第 4 章** | Vue/React 时代 | 掌握"声明式"和"数据驱动"思想 |
|
||||||
|
| **第 5 章** | 渲染策略 | 知道 CSR、SSR、SSG 的区别和适用场景 |
|
||||||
|
| **第 6 章** | 工程化工具 | 理解 Webpack、Vite 等构建工具的作用 |
|
||||||
|
|
||||||
|
每一章都从"为什么需要这个技术"开始,让你理解技术演进背后的逻辑。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 为什么要关注前端演进史?
|
## 1. 为什么要关注前端演进史?
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**为什么网页越来越复杂?前端技术为什么要不断演进?** 这个问题会带你理解从简单网页到现代 Web 应用的技术演变之路。
|
||||||
|
:::
|
||||||
|
|
||||||
### 1.1 从"电子海报"到"桌面应用"
|
### 1.1 从"电子海报"到"桌面应用"
|
||||||
|
|
||||||
想象一下你在街上看到的**海报**:
|
想象一下你在街上看到的**海报**:
|
||||||
|
|
||||||
- ✅ 有内容(文字、图片)
|
- ✅ 有内容(文字、图片)
|
||||||
- ✅ 有设计(颜色、排版)
|
- ✅ 有设计(颜色、排版)
|
||||||
- ❌ 但你跟它说话,它不会回应
|
- ❌ 但你跟它说话,它不会回应
|
||||||
- ❌ 你点击某个地方,不会发生什么
|
- ❌ 你点击某个地方,不会发生什么
|
||||||
|
|
||||||
**最早的网页**就是这样的"电子海报":只能看、不能改、内容固定。
|
**最早的网页**就是这样的"电子海报":只能看、不能改、内容固定。
|
||||||
|
|
||||||
**现代网页**完全不同了。它们像**桌面应用**(VS Code、Figma):
|
**现代网页**完全不同了。它们像**桌面应用**(VS Code、Figma):
|
||||||
|
|
||||||
- ✅ 可以编辑文档、画图、玩游戏
|
- ✅ 可以编辑文档、画图、玩游戏
|
||||||
- ✅ 实时响应你的每个操作
|
- ✅ 实时响应你的每个操作
|
||||||
- ✅ 甚至可以离线工作
|
- ✅ 甚至可以离线工作
|
||||||
|
|
||||||
**这种转变的核心原因: 网页的功能越来越复杂,需要更高效的技术和开发方式。**
|
**这种转变的核心原因:网页的功能越来越复杂,需要更高效的技术和开发方式。**
|
||||||
|
|
||||||
### 1.2 一个生活的比喻:盖房子
|
### 1.2 一个生活的比喻:盖房子
|
||||||
|
|
||||||
前端技术的演进,就像盖房子方式的进化:
|
前端技术的演进,就像盖房子方式的进化:
|
||||||
|
|
||||||
| 时代 | 🏠 盖房比喻 | 实际特点 | 优缺点 |
|
| 时代 | 🏠 盖房比喻 | 实际特点 | 优缺点 |
|
||||||
| --------- | ------------------ | ---------------------------- | --------------------------- |
|
|------|-----------|---------|--------|
|
||||||
| **2000s** | **贴海报** | 静态网页,写好 HTML 就行 | ✅ 简单 ❌ 不能互动 |
|
| **2000s** | **贴海报** | 静态网页,写好 HTML 就行 | ✅ 简单 ❌ 不能互动 |
|
||||||
| **2010s** | **请工人手动装修** | jQuery 时代,手动操作每个元素 | ✅ 能互动 ❌ 代码乱、难维护 |
|
| **2010s** | **请工人手动装修** | jQuery 时代,手动操作每个元素 | ✅ 能互动 ❌ 代码乱、难维护 |
|
||||||
| **2020s** | **用乐高搭房子** | Vue/React 时代,组件化开发 | ✅ 高效、可维护 ❌ 学习曲线 |
|
| **2020s** | **用乐高搭房子** | Vue/React 时代,组件化开发 | ✅ 高效、可维护 ❌ 学习曲线 |
|
||||||
|
|
||||||
::: tip 💡 从表格中你能看到什么?
|
::: tip 💡 从表格中你能看到什么?
|
||||||
|
|
||||||
**阶段一 → 阶段二**: 从"不能动"到"能动"。这是质的飞跃——网页开始有交互,但代价是代码变得混乱。
|
**阶段一 → 阶段二**:从"不能动"到"能动"。这是质的飞跃——网页开始有交互,但代价是代码变得混乱。
|
||||||
|
|
||||||
**阶段二 → 阶段三**: 从"能用"到"好用"。组件化让代码像积木一样可复用,大幅提升开发效率。
|
**阶段二 → 阶段三**:从"能用"到"好用"。组件化让代码像积木一样可复用,大幅提升开发效率。
|
||||||
|
|
||||||
**核心思想**: 技术演进不是"为了新而新",而是为了解决上一个阶段的痛点。
|
**核心思想**:技术演进不是"为了新而新",而是为了解决上一个阶段的痛点。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 第一阶段:静态网页与"切图"(2000s)
|
---
|
||||||
|
|
||||||
|
## 2. 第一阶段:静态网页与"切图"(2000s)
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**最早的网页是什么样的?为什么那时候不需要框架?** 理解这个阶段的局限性,才能明白后来技术演进的必要性。
|
||||||
|
:::
|
||||||
|
|
||||||
<FrontendEvolutionDemo />
|
<FrontendEvolutionDemo />
|
||||||
|
|
||||||
### 2.1 这个时代是什么样的?
|
### 2.1 这个时代是什么样的?
|
||||||
|
|
||||||
**开发方式**:
|
**开发方式**:
|
||||||
|
|
||||||
- 写几个 HTML 文件
|
- 写几个 HTML 文件
|
||||||
- 内嵌一些 CSS 和 JavaScript
|
- 内嵌一些 CSS 和 JavaScript
|
||||||
- 直接把文件拖到浏览器就能看效果
|
- 直接把文件拖到浏览器就能看效果
|
||||||
- 上传文件夹到服务器就完成部署
|
- 上传文件夹到服务器就完成部署
|
||||||
|
|
||||||
**特点**:
|
**特点**:
|
||||||
|
|
||||||
- ✅ **优点**: 简单直接,没有学习成本,写完就能跑
|
- ✅ **优点**:简单直接,没有学习成本,写完就能跑
|
||||||
- ❌ **缺点**: 无法实现复杂交互,代码一多就乱
|
- ❌ **缺点**:无法实现复杂交互,代码一多就乱
|
||||||
|
|
||||||
::: details 查看当时的项目结构
|
::: details 查看当时的项目结构
|
||||||
|
|
||||||
@@ -80,63 +112,69 @@ project/
|
|||||||
└── images/
|
└── images/
|
||||||
```
|
```
|
||||||
|
|
||||||
**遇到的问题**:
|
**遇到的问题**:
|
||||||
|
|
||||||
1. **全局变量污染**: 所有变量都在全局命名空间,容易互相覆盖
|
1. **全局变量污染**:所有变量都在全局命名空间,容易互相覆盖
|
||||||
2. **依赖管理混乱**: 必须按正确顺序加载 JS 文件,否则会报错
|
2. **依赖管理混乱**:必须按正确顺序加载 JS 文件,否则会报错
|
||||||
3. **代码难以复用**: 想复用某个功能,只能复制粘贴
|
3. **代码难以复用**:想复用某个功能,只能复制粘贴
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### 2.2 "切图"是什么?
|
### 2.2 "切图"是什么?
|
||||||
|
|
||||||
<SliceRequestDemo />
|
你可能听说过"切图"这个词。它是早期前端的主要工作:
|
||||||
|
|
||||||
你可能听说过"切图"这个词。它是早期前端的主要工作:
|
**什么是切图?**
|
||||||
|
|
||||||
**什么是切图?**
|
|
||||||
|
|
||||||
设计师用 Photoshop 设计好页面 → 前端把设计切成小图片 → 用 HTML 把图片拼成页面
|
设计师用 Photoshop 设计好页面 → 前端把设计切成小图片 → 用 HTML 把图片拼成页面
|
||||||
|
|
||||||
**为什么这么慢?**
|
**为什么这么慢?**
|
||||||
|
|
||||||
网页上的每张小图片,浏览器都要发一次**网络请求**。请求越多,加载越慢。
|
网页上的每张小图片,浏览器都要发一次**网络请求**。请求越多,加载越慢。
|
||||||
|
|
||||||
::: tip 💡 雪碧图(Sprite)
|
👇 **动手试试看**:观察图片请求对加载性能的影响
|
||||||
|
|
||||||
为了减少请求数,出现了"雪碧图"技术:把很多小图合成一张大图。
|
<SliceRequestDemo />
|
||||||
|
|
||||||
优点是请求数变少,缺点是制作和维护都很麻烦。
|
::: tip 💡 雪碧图(Sprite)
|
||||||
|
|
||||||
这个阶段的教训:**请求太多是性能大敌**。
|
为了减少请求数,出现了"雪碧图"技术:把很多小图合成一张大图。
|
||||||
|
|
||||||
|
优点是请求数变少,缺点是制作和维护都很麻烦。
|
||||||
|
|
||||||
|
这个阶段的教训:**请求太多是性能大敌**。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 第二阶段:jQuery 时代 - "手动搬砖"(2010s)
|
---
|
||||||
|
|
||||||
### 3.1 为什么需要 jQuery?
|
## 3. 第二阶段:jQuery 时代 - "手动搬砖"(2010s)
|
||||||
|
|
||||||
随着网页变复杂,原生 JavaScript 的问题暴露出来:
|
::: tip 🤔 核心问题
|
||||||
|
**为什么需要 jQuery?它解决了什么问题,又带来了什么新问题?** 理解 jQuery 的局限性,才能明白 Vue/React 的价值。
|
||||||
|
:::
|
||||||
|
|
||||||
- ❌ **API 繁琐**: 简单的操作也要写很多代码
|
### 3.1 为什么需要 jQuery?
|
||||||
- ❌ **浏览器兼容**: 不同浏览器的 API 不一样,要写很多兼容代码
|
|
||||||
- ❌ **选择器弱**: 找元素很麻烦
|
|
||||||
|
|
||||||
**jQuery** 诞生了。它让 JavaScript 变得简单:
|
随着网页变复杂,原生 JavaScript 的问题暴露出来:
|
||||||
|
|
||||||
|
- ❌ **API 繁琐**:简单的操作也要写很多代码
|
||||||
|
- ❌ **浏览器兼容**:不同浏览器的 API 不一样,要写很多兼容代码
|
||||||
|
- ❌ **选择器弱**:找元素很麻烦
|
||||||
|
|
||||||
|
**jQuery** 诞生了。它让 JavaScript 变得简单:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 原生 JavaScript (繁琐)
|
// 原生 JavaScript(繁琐)
|
||||||
const element = document.getElementById('title')
|
const element = document.getElementById('title')
|
||||||
|
|
||||||
// jQuery (简洁)
|
// jQuery(简洁)
|
||||||
const element = $('#title')
|
const element = $('#title')
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.2 jQuery 的思路:亲手改页面
|
### 3.2 jQuery 的思路:亲手改页面
|
||||||
|
|
||||||
<JQueryVsStateDemo />
|
jQuery 的核心思路是**命令式**:你告诉浏览器"怎么做"。
|
||||||
|
|
||||||
jQuery 的核心思路是**命令式**: 你告诉浏览器"怎么做"。
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 找到标题元素
|
// 找到标题元素
|
||||||
@@ -149,11 +187,15 @@ $('#submit-btn').attr('disabled', true)
|
|||||||
$('ul').append('<li>新项目</li>')
|
$('ul').append('<li>新项目</li>')
|
||||||
```
|
```
|
||||||
|
|
||||||
**问题**: 你需要记住页面上有哪些元素,每次数据变化都要手动更新所有相关元素。
|
**问题**:你需要记住页面上有哪些元素,每次数据变化都要手动更新所有相关元素。
|
||||||
|
|
||||||
|
👇 **动手试试看**:对比 jQuery 和数据驱动的方式
|
||||||
|
|
||||||
|
<JQueryVsStateDemo />
|
||||||
|
|
||||||
::: warning ⚠️ jQuery 的痛点
|
::: warning ⚠️ jQuery 的痛点
|
||||||
|
|
||||||
想象你在做一个购物车:
|
想象你在做一个购物车:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 用户点击"添加到购物车"
|
// 用户点击"添加到购物车"
|
||||||
@@ -165,32 +207,30 @@ function addToCart() {
|
|||||||
$('#cart-page-count').text(cartCount) // 购物车页面
|
$('#cart-page-count').text(cartCount) // 购物车页面
|
||||||
$('#checkout-price').text(calculatePrice()) // 结算按钮
|
$('#checkout-price').text(calculatePrice()) // 结算按钮
|
||||||
|
|
||||||
// 如果漏了一个地方,页面就不一致了!
|
// 如果漏了一个地方,页面就不一致了!
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**这就是"手动搬砖"的代价**: 容易出错,难以维护。
|
**这就是"手动搬砖"的代价**:容易出错,难以维护。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### 3.3 移动端普及:响应式设计的出现
|
### 3.3 移动端普及:响应式设计的出现
|
||||||
|
|
||||||
这个阶段还有一个重要变化:**手机和平板开始流行**。
|
这个阶段还有一个重要变化:**手机和平板开始流行**。
|
||||||
|
|
||||||
<ResponsiveGridDemo />
|
网页必须适配不同屏幕。这需要**响应式布局**:同一套 HTML/CSS,自动根据屏幕宽度变换布局。
|
||||||
|
|
||||||
网页必须适配不同屏幕。这需要**响应式布局**: 同一套 HTML/CSS,自动根据屏幕宽度变换布局。
|
**响应式布局的核心:媒体查询(Media Query)**
|
||||||
|
|
||||||
**响应式布局的核心: 媒体查询 (Media Query)**
|
|
||||||
|
|
||||||
```css
|
```css
|
||||||
/* 电脑屏幕(大于 640px) */
|
/* 电脑屏幕(大于 640px) */
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 手机屏幕(小于 640px) */
|
/* 手机屏幕(小于 640px) */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.container {
|
.container {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -198,35 +238,43 @@ function addToCart() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
👇 **动手试试看**:调整浏览器宽度,观察响应式布局的效果
|
||||||
|
|
||||||
|
<ResponsiveGridDemo />
|
||||||
|
|
||||||
::: tip 💡 响应式就像"智能相框"
|
::: tip 💡 响应式就像"智能相框"
|
||||||
|
|
||||||
想象你在不同房间看同一张照片:
|
想象你在不同房间看同一张照片:
|
||||||
|
|
||||||
- 在**大客厅**(电脑屏幕),照片可以摆大一些,旁边还能放其他装饰品
|
- 在**大客厅**(电脑屏幕),照片可以摆大一些,旁边还能放其他装饰品
|
||||||
- 在**小卧室**(手机屏幕),照片需要缩小,其他装饰品要收起来
|
- 在**小卧室**(手机屏幕),照片需要缩小,其他装饰品要收起来
|
||||||
|
|
||||||
**响应式布局**就是"智能相框",它会自动根据房间大小调整展示方式。
|
**响应式布局**就是"智能相框",它会自动根据房间大小调整展示方式。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 第三阶段:从"手动搬砖"到"数据驱动"(Vue/React)
|
---
|
||||||
|
|
||||||
### 4.1 为什么需要新框架?
|
## 4. 第三阶段:从"手动搬砖"到"数据驱动"(Vue/React)
|
||||||
|
|
||||||
jQuery 时代的问题积累到一定程度:
|
::: tip 🤔 核心问题
|
||||||
|
**为什么需要 Vue/React?它们和 jQuery 的本质区别是什么?** 理解"声明式"和"数据驱动",是掌握现代前端框架的关键。
|
||||||
|
:::
|
||||||
|
|
||||||
- **代码一多就乱**: 到处都是 DOM 操作,难以维护
|
### 4.1 为什么需要新框架?
|
||||||
- **容易出 bug**: 漏更新一个地方,页面就不一致
|
|
||||||
- **协作困难**: 多人修改同一个文件,容易冲突
|
|
||||||
|
|
||||||
**Vue / React** 的核心思路:**只改数据,页面自动更新**。
|
jQuery 时代的问题积累到一定程度:
|
||||||
|
|
||||||
### 4.2 Vue/React 的思路:声明式 UI
|
- **代码一多就乱**:到处都是 DOM 操作,难以维护
|
||||||
|
- **容易出 bug**:漏更新一个地方,页面就不一致
|
||||||
|
- **协作困难**:多人修改同一个文件,容易冲突
|
||||||
|
|
||||||
<ImperativeVsDeclarativeDemo />
|
**Vue / React** 的核心思路:**只改数据,页面自动更新**。
|
||||||
|
|
||||||
**jQuery (命令式)**:
|
### 4.2 Vue/React 的思路:声明式 UI
|
||||||
|
|
||||||
|
**jQuery(命令式)**:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 你要告诉浏览器每一步怎么做
|
// 你要告诉浏览器每一步怎么做
|
||||||
@@ -235,7 +283,7 @@ $('#title').css('color', 'red')
|
|||||||
$('#title').show()
|
$('#title').show()
|
||||||
```
|
```
|
||||||
|
|
||||||
**Vue (声明式)**:
|
**Vue(声明式)**:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 你只需告诉浏览器"要显示什么"
|
// 你只需告诉浏览器"要显示什么"
|
||||||
@@ -248,213 +296,409 @@ data() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
👇 **动手试试看**:对比命令式和声明式的区别
|
||||||
|
|
||||||
|
<ImperativeVsDeclarativeDemo />
|
||||||
|
|
||||||
::: tip 💡 命令式 vs 声明式
|
::: tip 💡 命令式 vs 声明式
|
||||||
|
|
||||||
就像画一幅画:
|
就像画一幅画:
|
||||||
|
|
||||||
- **命令式**: 你告诉画家"拿起笔,蘸红颜料,在坐标(10,10)画一个圈"
|
- **命令式**:你告诉画家"拿起笔,蘸红颜料,在坐标(10,10)画一个圈"
|
||||||
- **声明式**: 你直接给画家一张照片,"给我画成这样"
|
- **声明式**:你直接给画家一张照片,"给我画成这样"
|
||||||
|
|
||||||
Vue/React 就是"声明式": 你描述"页面长什么样",框架负责"怎么把它画出来"。
|
Vue/React 就是"声明式":你描述"页面长什么样",框架负责"怎么把它画出来"。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### 4.3 组件化:像搭乐高一样写页面
|
### 4.3 组件化:像搭乐高一样写页面
|
||||||
|
|
||||||
**Vue / React** 最强大的特性是**组件化**: 把页面拆成一个个独立的"积木"。
|
**Vue / React** 最强大的特性是**组件化**:把页面拆成一个个独立的"积木"。
|
||||||
|
|
||||||
想象一下你在搭乐高:
|
想象一下你在搭乐高:
|
||||||
|
|
||||||
- 你不需要"从头开始雕刻每一块积木"(从头写 HTML/CSS)
|
- 你不需要"从头开始雕刻每一块积木"(从头写 HTML/CSS)
|
||||||
- 你只需要"按说明书把积木拼在一起"(把组件组合起来)
|
- 你只需要"按说明书把积木拼在一起"(把组件组合起来)
|
||||||
- 每个积木都是**独立的**,你可以在不同的套装里**重复使用**
|
- 每个积木都是**独立的**,你可以在不同的套装里**重复使用**
|
||||||
|
|
||||||
**组件的好处**:
|
**组件的好处**:
|
||||||
|
|
||||||
- **复用**: 写一个"商品卡片"组件,可以用 100 次
|
- **复用**:写一个"商品卡片"组件,可以用 100 次
|
||||||
- **封装**: 组件内部的状态不影响别人
|
- **封装**:组件内部的状态不影响别人
|
||||||
- **维护**: 修改一个组件,所有用到它的地方都会更新
|
- **维护**:修改一个组件,所有用到它的地方都会更新
|
||||||
|
|
||||||
### 4.4 SPA:单页应用的诞生
|
::: info 💡 识别技巧
|
||||||
|
- 看到 `<ComponentName />` → 这是一个组件
|
||||||
|
- 看到 `import xxx from './xxx.vue'` → 在导入一个组件
|
||||||
|
- 看到 `props: {...}` → 组件接收的参数
|
||||||
|
- 看到 `emit('xxx')` → 组件向父组件发送事件
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 4.4 SPA:单页应用的诞生
|
||||||
|
|
||||||
|
**Vue / React** 时代还有一个重要变化:**从 MPA 到 SPA**。
|
||||||
|
|
||||||
|
**MPA(Multi-Page Application)**:
|
||||||
|
|
||||||
|
- 点一个链接 → 整页刷新 → 显示新页面
|
||||||
|
- 就像**翻书**:每翻一页都要把旧书合上、去书架拿新书
|
||||||
|
|
||||||
|
**SPA(Single-Page Application)**:
|
||||||
|
|
||||||
|
- 点一个链接 → 只刷新内容区域 → 页面不刷新
|
||||||
|
- 就像**同一本书里换章节**:只擦掉旧内容、写上新内容
|
||||||
|
|
||||||
|
👇 **动手试试看**:体验 MPA 和 SPA 的区别
|
||||||
|
|
||||||
<RoutingModeDemo />
|
<RoutingModeDemo />
|
||||||
|
|
||||||
**Vue / React** 时代还有一个重要变化:**从 MPA 到 SPA**。
|
**SPA 的优点**:
|
||||||
|
|
||||||
**MPA (Multi-Page Application)**:
|
- ✅ **体验丝滑**:页面切换快
|
||||||
|
- ✅ **状态好管理**:输入的内容、滚动位置都在
|
||||||
- 点一个链接 → 整页刷新 → 显示新页面
|
- ❌ **首屏可能慢**:需要先下载 JavaScript
|
||||||
- 就像**翻书**: 每翻一页都要把旧书合上、去书架拿新书
|
- ❌ **SEO 要额外处理**:搜索引擎可能抓不到内容(需要 SSR/SSG)
|
||||||
|
|
||||||
**SPA (Single-Page Application)**:
|
|
||||||
|
|
||||||
- 点一个链接 → 只刷新内容区域 → 页面不刷新
|
|
||||||
- 就像**同一本书里换章节**: 只擦掉旧内容、写上新内容
|
|
||||||
|
|
||||||
**SPA 的优点**:
|
|
||||||
|
|
||||||
- ✅ **体验丝滑**: 页面切换快
|
|
||||||
- ✅ **状态好管理**: 输入的内容、滚动位置都在
|
|
||||||
- ❌ **首屏可能慢**: 需要先下载 JavaScript
|
|
||||||
- ❌ **SEO 要额外处理**: 搜索引擎可能抓不到内容(需要 SSR/SSG)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 渲染策略:从 CSR 到 SSR/SSG
|
---
|
||||||
|
|
||||||
|
## 5. 渲染策略:从 CSR 到 SSR/SSG
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**页面是在服务器生成,还是在浏览器生成?** 不同渲染策略各有优劣,选择合适的策略对性能和 SEO 至关重要。
|
||||||
|
:::
|
||||||
|
|
||||||
|
**CSR(Client-Side Rendering)客户端渲染**:
|
||||||
|
|
||||||
|
- 浏览器下载 JavaScript → 执行代码 → 生成页面
|
||||||
|
- 优点:交互流畅,服务器压力小
|
||||||
|
- 缺点:首屏慢,不利于 SEO
|
||||||
|
|
||||||
|
**SSR(Server-Side Rendering)服务端渲染**:
|
||||||
|
|
||||||
|
- 服务器生成 HTML → 发给浏览器 → 浏览器直接显示
|
||||||
|
- 优点:首屏快,利于 SEO
|
||||||
|
- 缺点:服务器压力大,实现复杂
|
||||||
|
|
||||||
|
**SSG(Static Site Generation)静态站点生成**:
|
||||||
|
|
||||||
|
- 构建时生成所有页面的 HTML
|
||||||
|
- 优点:极快,完全静态,CDN 友好
|
||||||
|
- 缺点:不适合动态内容
|
||||||
|
|
||||||
|
👇 **动手试试看**:对比不同渲染策略的特点
|
||||||
|
|
||||||
<RenderingStrategyDemo />
|
<RenderingStrategyDemo />
|
||||||
|
|
||||||
## 6. 第四阶段:工程化与构建工具(2015s-2020s)
|
::: info 💡 如何选择?
|
||||||
|
- **内容网站**(博客、文档):优先 SSG
|
||||||
### 6.1 为什么需要"工程化"?
|
- **需要 SEO 的动态网站**(电商、新闻):使用 SSR
|
||||||
|
- **后台管理系统**:使用 CSR
|
||||||
前端项目越来越大,不能再靠"手动引入脚本"。
|
- **混合需求**:考虑 Nuxt/Next.js 的混合渲染
|
||||||
|
|
||||||
**工程化**就是用工具和规范,让开发更高效、代码更可靠、协作更顺畅。
|
|
||||||
|
|
||||||
::: tip 💡 工程化 = 从"手工作坊"到"现代化工厂"
|
|
||||||
|
|
||||||
想象一下你在家做饭 vs 开餐厅:
|
|
||||||
|
|
||||||
- **在家做饭**: 想吃什么就做什么,很自由
|
|
||||||
- **开餐厅**: 需要标准化的菜谱、规范的操作流程、统一的原材料采购
|
|
||||||
|
|
||||||
前端开发也一样:
|
|
||||||
|
|
||||||
- **小项目**: 怎么写都行
|
|
||||||
- **大项目**: 需要统一的代码规范、自动化工具、标准化流程
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 6.2 构建工具:Webpack → Vite
|
|
||||||
|
|
||||||
**Webpack** (传统):
|
|
||||||
|
|
||||||
- 工作方式:**先打包,后服务**
|
|
||||||
- 启动时: 打包所有代码 → 启动服务器
|
|
||||||
- 问题:**慢**。项目越大,启动越慢(可能要等 30 秒)
|
|
||||||
|
|
||||||
**Vite** (现代):
|
|
||||||
|
|
||||||
- 工作方式:**按需编译**
|
|
||||||
- 启动时: 不打包,直接启动服务器
|
|
||||||
- 浏览器请求哪个文件,就实时编译哪个
|
|
||||||
- 优势:**快**。通常 1 秒内启动
|
|
||||||
|
|
||||||
| 对比项 | Webpack | Vite | 提升 |
|
|
||||||
| -------- | ------- | ------ | ------------ |
|
|
||||||
| 冷启动 | 30s+ | <1s | **快 30 倍** |
|
|
||||||
| 热更新 | 3-5s | <100ms | **快 30 倍** |
|
|
||||||
| 配置文件 | 几百行 | 几十行 | **大幅简化** |
|
|
||||||
|
|
||||||
::: tip 💡 为什么 Vite 这么快?
|
|
||||||
|
|
||||||
**Webpack** 就像**整备家当搬家**:先把所有东西打包,再出门。
|
|
||||||
|
|
||||||
**Vite** 就像**轻装旅行**:只带必需品,用到什么再买什么。
|
|
||||||
|
|
||||||
在开发环境,大多数时候你只需要修改几个文件,Vite 只编译这几个文件,当然快。
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. 总结:演进的本质
|
## 6. 第四阶段:工程化与构建工具(2015s-2020s)
|
||||||
|
|
||||||
前端技术的演进,本质上是在解决两个问题:
|
::: tip 🤔 核心问题
|
||||||
|
**为什么前端需要"工程化"?构建工具到底在做什么?** 理解工程化,才能看懂现代前端项目的工作流程。
|
||||||
|
:::
|
||||||
|
|
||||||
### 7.1 效率:从手动到自动
|
### 6.1 为什么需要"工程化"?
|
||||||
|
|
||||||
| 时代 | 开发方式 | 效率 |
|
前端项目越来越大,不能再靠"手动引入脚本"。
|
||||||
| --------- | ------------------------ | ---------- |
|
|
||||||
| **2000s** | 手写 HTML/CSS/JS | ⭐ |
|
|
||||||
| **2010s** | jQuery + 手动 DOM 操作 | ⭐⭐ |
|
|
||||||
| **2020s** | Vue/React + 数据驱动 | ⭐⭐⭐ |
|
|
||||||
| **现在** | 组件化 + 工程化 + 自动化 | ⭐⭐⭐⭐⭐ |
|
|
||||||
|
|
||||||
### 7.2 规模:从个人到团队
|
**工程化**就是用工具和规范,让开发更高效、代码更可靠、协作更顺畅。
|
||||||
|
|
||||||
| 时代 | 项目规模 | 协作方式 |
|
::: tip 💡 工程化 = 从"手工作坊"到"现代化工厂"
|
||||||
| --------- | ---------- | ----------------------- |
|
|
||||||
| **2000s** | 几个文件 | 单人就能维护 |
|
想象一下你在家做饭 vs 开餐厅:
|
||||||
| **2010s** | 几十个文件 | 小团队,容易冲突 |
|
|
||||||
| **2020s** | 几百个文件 | 中团队,需要规范 |
|
- **在家做饭**:想吃什么就做什么,很自由
|
||||||
| **现在** | 几千个文件 | 大团队,需要完整工程体系 |
|
- **开餐厅**:需要标准化的菜谱、规范的操作流程、统一的原材料采购
|
||||||
|
|
||||||
|
前端开发也一样:
|
||||||
|
|
||||||
|
- **小项目**:怎么写都行
|
||||||
|
- **大项目**:需要统一的代码规范、自动化工具、标准化流程
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 6.2 构建工具:Webpack → Vite
|
||||||
|
|
||||||
|
**Webpack**(传统):
|
||||||
|
|
||||||
|
- 工作方式:**先打包,后服务**
|
||||||
|
- 启动时:打包所有代码 → 启动服务器
|
||||||
|
- 问题:**慢**。项目越大,启动越慢(可能要等 30 秒)
|
||||||
|
|
||||||
|
**Vite**(现代):
|
||||||
|
|
||||||
|
- 工作方式:**按需编译**
|
||||||
|
- 启动时:不打包,直接启动服务器
|
||||||
|
- 浏览器请求哪个文件,就实时编译哪个
|
||||||
|
- 优势:**快**。通常 1 秒内启动
|
||||||
|
|
||||||
|
| 对比项 | Webpack | Vite | 提升 |
|
||||||
|
|--------|---------|------|------|
|
||||||
|
| 冷启动 | 30s+ | <1s | **快 30 倍** |
|
||||||
|
| 热更新 | 3-5s | <100ms | **快 30 倍** |
|
||||||
|
| 配置文件 | 几百行 | 几十行 | **大幅简化** |
|
||||||
|
|
||||||
|
::: tip 💡 为什么 Vite 这么快?
|
||||||
|
|
||||||
|
**Webpack** 就像**整备家当搬家**:先把所有东西打包,再出门。
|
||||||
|
|
||||||
|
**Vite** 就像**轻装旅行**:只带必需品,用到什么再买什么。
|
||||||
|
|
||||||
|
在开发环境,大多数时候你只需要修改几个文件,Vite 只编译这几个文件,当然快。
|
||||||
|
:::
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 学习路线图
|
---
|
||||||
|
|
||||||
### 8.1 如果你是零基础
|
## 7. 主流框架对比
|
||||||
|
|
||||||
**第 1 步: HTML/CSS/JavaScript 基础**
|
::: tip 🤔 核心问题
|
||||||
|
**Vue、React、Svelte、Angular 各有什么特点?如何选择适合自己的框架?** 了解它们的设计理念和使用场景,才能做出明智的选择。
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 7.1 四大框架对比
|
||||||
|
|
||||||
|
| 特性 | Vue | React | Svelte | Angular |
|
||||||
|
|------|-----|-------|--------|---------|
|
||||||
|
| **设计理念** | 渐进式框架 | UI 库 | 编译时框架 | 完整平台 |
|
||||||
|
| **学习曲线** | ⭐⭐ 简单 | ⭐⭐⭐ 中等 | ⭐⭐ 简单 | ⭐⭐⭐⭐ 陡峭 |
|
||||||
|
| **性能** | 快 | 快 | **极快** | 快 |
|
||||||
|
| **生态系统** | 完善 | **最完善** | 成长中 | 完善 |
|
||||||
|
| **包大小** | 小 | 中等 | **最小** | 大 |
|
||||||
|
| **适合场景** | 中小型项目 | 大型项目 | 性能要求高 | 企业级应用 |
|
||||||
|
| **公司支持** | 尤雨溪(独立) | Meta | 社区 | Google |
|
||||||
|
|
||||||
|
### 7.2 Vue:渐进式框架
|
||||||
|
|
||||||
|
**核心理念**:渐进式采用,可以只用一部分,也可以用全家桶
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div>{{ message }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
message: 'Hello Vue'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- ✅ 学习曲线平缓,中文文档完善
|
||||||
|
- ✅ 模板语法直观,易于理解
|
||||||
|
- ✅ 单文件组件(.vue)结构清晰
|
||||||
|
- ✅ 适合快速开发
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- ❌ 大型项目的状态管理需要额外学习 Vuex/Pinia
|
||||||
|
- ❌ 灵活性略逊于 React
|
||||||
|
|
||||||
|
**适用场景**:
|
||||||
|
- 中小型 Web 应用
|
||||||
|
- 快速原型开发
|
||||||
|
- 中文团队(文档友好)
|
||||||
|
|
||||||
|
### 7.3 React:UI 库
|
||||||
|
|
||||||
|
**核心理念**:只负责视图层,其他问题交给社区
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
function App() {
|
||||||
|
const [message, setMessage] = useState('Hello React')
|
||||||
|
return <div>{message}</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- ✅ 生态系统最完善,组件库丰富
|
||||||
|
- ✅ JSX 语法灵活,表达能力强大
|
||||||
|
- ✅ 虚拟 DOM 性能优秀
|
||||||
|
- ✅ 适合大型项目
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- ❌ 学习曲线较陡,需要掌握额外概念
|
||||||
|
- ❌ 需要自己选择和搭配各种库
|
||||||
|
- ❌ JSX 需要编译,不能直接在浏览器运行
|
||||||
|
|
||||||
|
**适用场景**:
|
||||||
|
- 大型复杂应用
|
||||||
|
- 需要丰富生态的项目
|
||||||
|
- 跨平台开发(React Native)
|
||||||
|
|
||||||
|
### 7.4 Svelte:编译时框架
|
||||||
|
|
||||||
|
**核心理念**:没有虚拟 DOM,编译时将组件转换为高效的原生代码
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script>
|
||||||
|
let message = 'Hello Svelte'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>{message}</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- ✅ **性能最优**(无虚拟 DOM 运行时开销)
|
||||||
|
- ✅ 包体积最小
|
||||||
|
- ✅ 语法简单直观
|
||||||
|
- ✅ 响应式系统天然支持
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- ❌ 生态相对较小
|
||||||
|
- ❌ 社区规模不如 Vue/React
|
||||||
|
- ❌ 第三方库较少
|
||||||
|
|
||||||
|
**适用场景**:
|
||||||
|
- 性能要求极高的应用
|
||||||
|
- 包体积敏感的项目
|
||||||
|
- 愿意尝试新技术的团队
|
||||||
|
|
||||||
|
### 7.5 Angular:完整平台
|
||||||
|
|
||||||
|
**核心理念**:提供完整的解决方案,开箱即用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
template: '<div>{{ message }}</div>'
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
message = 'Hello Angular'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- ✅ 功能完整,路由、HTTP、表单全都有
|
||||||
|
- ✅ TypeScript 原生支持
|
||||||
|
- ✅ 适合大型团队和项目
|
||||||
|
- ✅ 代码规范统一
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- ❌ 学习曲线陡峭
|
||||||
|
- ❌ 概念多,复杂度高
|
||||||
|
- ❌ 包体积大
|
||||||
|
- ❌ 不适合小型项目
|
||||||
|
|
||||||
|
**适用场景**:
|
||||||
|
- 大型企业级应用
|
||||||
|
- 需要严格规范的团队
|
||||||
|
- 已有 TypeScript 技术栈的项目
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 总结:演进的本质
|
||||||
|
|
||||||
|
前端技术的演进,本质上是在解决两个问题:
|
||||||
|
|
||||||
|
### 8.1 效率:从手动到自动
|
||||||
|
|
||||||
|
| 时代 | 开发方式 | 效率 |
|
||||||
|
|------|---------|------|
|
||||||
|
| **2000s** | 手写 HTML/CSS/JS | ⭐ |
|
||||||
|
| **2010s** | jQuery + 手动 DOM 操作 | ⭐⭐ |
|
||||||
|
| **2020s** | Vue/React + 数据驱动 | ⭐⭐⭐ |
|
||||||
|
| **现在** | 组件化 + 工程化 + 自动化 | ⭐⭐⭐⭐⭐ |
|
||||||
|
|
||||||
|
### 8.2 规模:从个人到团队
|
||||||
|
|
||||||
|
| 时代 | 项目规模 | 协作方式 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| **2000s** | 几个文件 | 单人就能维护 |
|
||||||
|
| **2010s** | 几十个文件 | 小团队,容易冲突 |
|
||||||
|
| **2020s** | 几百个文件 | 中团队,需要规范 |
|
||||||
|
| **现在** | 几千个文件 | 大团队,需要完整工程体系 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 学习路线图
|
||||||
|
|
||||||
|
### 9.1 如果你是零基础
|
||||||
|
|
||||||
|
**第 1 步:HTML/CSS/JavaScript 基础**
|
||||||
|
|
||||||
- 理解网页的三大基石
|
- 理解网页的三大基石
|
||||||
- 能写出简单的静态页面
|
- 能写出简单的静态页面
|
||||||
|
|
||||||
**第 2 步: 学习一个框架(Vue 推荐)**
|
**第 2 步:学习一个框架(Vue 推荐)**
|
||||||
|
|
||||||
- 理解"数据驱动"的思想
|
- 理解"数据驱动"的思想
|
||||||
- 掌握组件化开发
|
- 掌握组件化开发
|
||||||
|
|
||||||
**第 3 步: 实战项目**
|
**第 3 步:实战项目**
|
||||||
|
|
||||||
- 做一个完整的单页应用
|
- 做一个完整的单页应用
|
||||||
- 熟悉路由、状态管理、API 调用
|
- 熟悉路由、状态管理、API 调用
|
||||||
|
|
||||||
### 8.2 如果你有基础
|
### 9.2 如果你有基础
|
||||||
|
|
||||||
**进阶方向**:
|
**进阶方向**:
|
||||||
|
|
||||||
- **工程化**: 学习 Vite/Webpack,理解构建流程
|
- **工程化**:学习 Vite/Webpack,理解构建流程
|
||||||
- **性能优化**: 学习懒加载、代码分割、缓存策略
|
- **性能优化**:学习懒加载、代码分割、缓存策略
|
||||||
- **TypeScript**: 为代码加上类型,提升可靠性
|
- **TypeScript**:为代码加上类型,提升可靠性
|
||||||
- **服务端渲染**: 学习 Nuxt/Next.js,解决 SEO 和首屏问题
|
- **服务端渲染**:学习 Nuxt/Next.js,解决 SEO 和首屏问题
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. 名词速查表 (Glossary)
|
## 10. 你现在应该能识别的代码
|
||||||
|
|
||||||
| 名词 | 英文 | 用人话解释 |
|
通过阅读本章,你应该能够:
|
||||||
| ---------------- | ----------------------- | --------------------------------------------- |
|
|
||||||
| **DOM** | Document Object Model | 文档对象模型。用对象树表示页面,可被 JS 读写。 |
|
- ✅ 理解前端技术演进的脉络和原因
|
||||||
| **jQuery** | - | 早期流行的 JS 库,简化了 DOM 操作。 |
|
- ✅ 区分 Vue、React、Svelte、Angular 的特点
|
||||||
| **Vue/React** | - | 现代前端框架,采用数据驱动和组件化开发。 |
|
- ✅ 理解"命令式"和"声明式"的区别
|
||||||
| **组件** | Component | 可复用的 UI 单元,如按钮、卡片、导航栏。 |
|
- ✅ 掌握"数据驱动"的核心思想
|
||||||
| **MPA** | Multi-Page Application | 多页应用。每次跳转都重新加载整个页面。 |
|
- ✅ 知道组件化开发的价值
|
||||||
| **SPA** | Single-Page Application | 单页应用。只加载一次,后续切换不刷新页面。 |
|
- ✅ 了解 CSR、SSR、SSG 的适用场景
|
||||||
| **路由** | Routing | 管理页面之间切换的规则和过程。 |
|
- ✅ 理解构建工具(Webpack、Vite)的作用
|
||||||
| **SSR** | Server-Side Rendering | 服务端渲染。服务器生成 HTML 后发给浏览器。 |
|
- ✅ 能根据项目选择合适的框架和技术栈
|
||||||
| **SSG** | Static Site Generation | 静态站点生成。构建时预渲染页面为静态 HTML。 |
|
|
||||||
| **Webpack** | - | 传统打包工具,先打包后服务。 |
|
::: info 💡 实际应用
|
||||||
| **Vite** | - | 现代构建工具,按需编译,速度极快。 |
|
当你用 AI 做项目时,你可以这样告诉它:
|
||||||
| **响应式** | Responsive Design | 页面自动适配不同屏幕尺寸的设计。 |
|
|
||||||
| **媒体查询** | Media Query | CSS 的条件判断,根据屏幕宽度应用不同样式。 |
|
- "这是一个需要 SEO 的博客网站,用 Nuxt(Vue 的 SSR 框架)"
|
||||||
| **命令式** | Imperative | 告诉程序"怎么做"。 |
|
- "这是一个后台管理系统,用 Vue + Element Plus,不需要 SSR"
|
||||||
| **声明式** | Declarative | 告诉程序"要什么"。 |
|
- "这是一个性能要求高的 Web 应用,考虑使用 Svelte"
|
||||||
| **数据驱动** | Data-Driven | 只修改数据,界面自动更新。 |
|
- "项目已经用 React 了,继续用 React 生态的库"
|
||||||
| **Tree Shaking** | - | 摇树优化。自动移除未使用的代码,减小包体积。 |
|
:::
|
||||||
| **代码分割** | Code Splitting | 把代码分成多个小块,按需加载。 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 总结
|
## 名词速查表
|
||||||
|
|
||||||
前端技术的演进,本质上是**从"手工"到"工业化"的进化**:
|
| 名词 | 英文 | 用人话解释 |
|
||||||
|
|------|------|-----------|
|
||||||
- **2000s**: 手工时代,简单直接
|
| **DOM** | Document Object Model | 文档对象模型。用对象树表示页面,可被 JS 读写。 |
|
||||||
- **2010s**: 工具化时代,开始有框架
|
| **jQuery** | - | 早期流行的 JS 库,简化了 DOM 操作。 |
|
||||||
- **2020s**: 工业化时代,组件化 + 工程化
|
| **Vue/React** | - | 现代前端框架,采用数据驱动和组件化开发。 |
|
||||||
- **现在**: 智能化时代,AI 辅助开发
|
| **组件** | Component | 可复用的 UI 单元,如按钮、卡片、导航栏。 |
|
||||||
|
| **MPA** | Multi-Page Application | 多页应用。每次跳转都重新加载整个页面。 |
|
||||||
理解这个演进,你就能:
|
| **SPA** | Single-Page Application | 单页应用。只加载一次,后续切换不刷新页面。 |
|
||||||
|
| **路由** | Routing | 管理页面之间切换的规则和过程。 |
|
||||||
- 知道为什么要有 Vue/React
|
| **SSR** | Server-Side Rendering | 服务端渲染。服务器生成 HTML 后发给浏览器。 |
|
||||||
- 理解"数据驱动"的价值
|
| **SSG** | Static Site Generation | 静态站点生成。构建时预渲染页面为静态 HTML。 |
|
||||||
- 明白工程化的必要性
|
| **CSR** | Client-Side Rendering | 客户端渲染。浏览器通过 JS 生成页面。 |
|
||||||
- 快速上手新技术
|
| **Webpack** | - | 传统打包工具,先打包后服务。 |
|
||||||
|
| **Vite** | - | 现代构建工具,按需编译,速度极快。 |
|
||||||
**下一步建议**:
|
| **响应式** | Responsive Design | 页面自动适配不同屏幕尺寸的设计。 |
|
||||||
|
| **媒体查询** | Media Query | CSS 的条件判断,根据屏幕宽度应用不同样式。 |
|
||||||
- 如果你想快速上手,学习 **Vue 3** (推荐) 或 **React**
|
| **命令式** | Imperative | 告诉程序"怎么做"。 |
|
||||||
- 如果你想深入理解,学习 **Vite** 构建流程
|
| **声明式** | Declarative | 告诉程序"要什么"。 |
|
||||||
- 如果你想提升代码质量,学习 **TypeScript**
|
| **数据驱动** | Data-Driven | 只修改数据,界面自动更新。 |
|
||||||
|
| **Tree Shaking** | - | 摇树优化。自动移除未使用的代码,减小包体积。 |
|
||||||
祝你学习愉快!
|
| **代码分割** | Code Splitting | 把代码分成多个小块,按需加载。 |
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,599 @@
|
|||||||
# JavaScript 运行时
|
# JavaScript 运行时深度指南
|
||||||
|
|
||||||
> 待实现
|
::: tip 前言
|
||||||
|
你已经学会了 JavaScript 的基本语法,但你是否想过:
|
||||||
|
- 代码到底在哪里运行?
|
||||||
|
- 为什么同样的代码在浏览器和 Node.js 中行为不一样?
|
||||||
|
- 为什么有时代码会"卡住",有时却能"并行"执行?
|
||||||
|
|
||||||
|
这篇文章会带你深入了解 JavaScript 的运行时环境,包括事件循环、调用栈、内存管理等。读完这篇,你就能理解代码为什么按某个顺序执行,快速定位异步相关的 bug,优化代码性能并避免内存泄漏。
|
||||||
|
:::
|
||||||
|
|
||||||
|
**这篇文章会带你学什么?**
|
||||||
|
|
||||||
|
| 章节 | 内容 | 学完能干嘛 |
|
||||||
|
|-----|------|-----------|
|
||||||
|
| **第 1 章** | 运行时概述 | 理解 JavaScript 代码在哪里运行 |
|
||||||
|
| **第 2 章** | 浏览器运行时 | 知道浏览器提供了哪些 Web API |
|
||||||
|
| **第 3 章** | Node.js 运行时 | 了解服务器端的 JavaScript 环境 |
|
||||||
|
| **第 4 章** | 事件循环深入 | 掌握宏任务和微任务的执行顺序 |
|
||||||
|
| **第 5 章** | 调用栈与内存 | 理解代码执行过程和内存管理 |
|
||||||
|
| **第 6 章** | 实战技巧 | 优化性能、调试内存泄漏 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 运行时概述
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**什么是"运行时"?** JavaScript 只是一门语言,为什么同样的代码在不同环境中会有不同的行为?
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 1.1 运行时是什么
|
||||||
|
|
||||||
|
**运行时 = JavaScript 引擎 + 环境提供的 API**
|
||||||
|
|
||||||
|
如果把 JavaScript 比作"编程语言",那么运行时就是"操作系统"——它决定了你的代码能做什么、不能做什么。
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ JavaScript 代码 │
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ JavaScript 引擎 (V8) │ ← 负责解析和执行代码
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ 运行时环境 (浏览器/Node.js) │ ← 提供额外能力
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**一个比喻:JavaScript 是"普通话",运行时是"城市"**
|
||||||
|
|
||||||
|
- JavaScript 语法(普通话)哪里都一样
|
||||||
|
- 但不同城市提供的设施不一样:
|
||||||
|
- 浏览器 = 有 DOM、window、fetch(就像城市有商场、图书馆)
|
||||||
|
- Node.js = 有 fs、http、path(就像城市有工厂、高速公路)
|
||||||
|
|
||||||
|
### 1.2 两大主流运行时
|
||||||
|
|
||||||
|
| 特性 | 浏览器 | Node.js |
|
||||||
|
|------|--------|---------|
|
||||||
|
| **主要用途** | 网页交互、用户界面 | 服务器端应用、命令行工具 |
|
||||||
|
| **全局对象** | `window` | `global` |
|
||||||
|
| **DOM API** | ✅ 支持 | ❌ 不支持 |
|
||||||
|
| **文件系统** | ❌ 受限 | ✅ 完整支持 |
|
||||||
|
| **模块系统** | ES Modules | CommonJS + ES Modules |
|
||||||
|
| **定时器** | `setTimeout`, `setInterval` | `setTimeout`, `setInterval` |
|
||||||
|
| **网络请求** | `fetch`, `XMLHttpRequest` | `http`, `https` 模块 |
|
||||||
|
|
||||||
|
👇 **动手试试看**:对比浏览器和 Node.js 的环境差异
|
||||||
|
|
||||||
|
<RuntimeEnvironmentDemo />
|
||||||
|
|
||||||
|
::: info 💡 核心启示
|
||||||
|
运行时决定了你能用什么 API。在浏览器能用的 DOM API,在 Node.js 里用不了;在 Node.js 能用的文件 API,在浏览器里也用不了。这就是为什么有些代码需要"环境判断"。
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 浏览器运行时
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**浏览器提供了哪些能力让 JavaScript 操作网页?**
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 2.1 浏览器运行时的组成
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ JavaScript 引擎 │
|
||||||
|
│ (V8 / SpiderMonkey) │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Web APIs │
|
||||||
|
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ DOM │ │ BOM │ │ Network │ │
|
||||||
|
│ │ 操作网页 │ │ 操作浏览器 │ │ 网络请求 │ │
|
||||||
|
│ └─────────┘ └──────────┘ └──────────┘ │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 事件循环 (Event Loop) │
|
||||||
|
│ 负责协调代码执行、事件处理、任务调度 │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Web APIs 的三大类
|
||||||
|
|
||||||
|
**1. DOM API - 操作网页内容**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 查找元素
|
||||||
|
const title = document.querySelector('h1')
|
||||||
|
|
||||||
|
// 修改内容
|
||||||
|
title.textContent = '新标题'
|
||||||
|
|
||||||
|
// 添加样式
|
||||||
|
title.style.color = 'red'
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. BOM API - 操作浏览器**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 页面跳转
|
||||||
|
window.location.href = 'https://example.com'
|
||||||
|
|
||||||
|
// 浏览器存储
|
||||||
|
localStorage.setItem('key', 'value')
|
||||||
|
|
||||||
|
// 浏览器历史
|
||||||
|
history.back()
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Network API - 网络请求**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 发送 HTTP 请求
|
||||||
|
fetch('/api/data')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => console.log(data))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 浏览器特有的事件机制
|
||||||
|
|
||||||
|
浏览器运行时最强大的功能之一是"事件驱动"——代码不需要一直运行,而是等用户操作时才执行。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
console.log('按钮被点击了')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**常见事件类型:**
|
||||||
|
|
||||||
|
| 事件类型 | 触发时机 | 实际场景 |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| `click` | 鼠标点击 | 按钮交互 |
|
||||||
|
| `input` | 输入框内容变化 | 实时搜索 |
|
||||||
|
| `scroll` | 页面滚动 | 懒加载 |
|
||||||
|
| `load` | 资源加载完成 | 初始化数据 |
|
||||||
|
| `error` | 发生错误 | 错误处理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Node.js 运行时
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**JavaScript 能在服务器端运行,靠的是什么?**
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 3.1 Node.js 的组成
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ JavaScript 引擎 │
|
||||||
|
│ (V8) │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Node.js 内置模块 │
|
||||||
|
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ fs │ │ http │ │ path │ │
|
||||||
|
│ │ 文件操作 │ │ 网络服务器 │ │ 路径处理 │ │
|
||||||
|
│ └─────────┘ └──────────┘ └──────────┘ │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ libuv 事件循环库 │
|
||||||
|
│ 跨平台的异步 I/O 支持 │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Node.js 特有能力
|
||||||
|
|
||||||
|
**1. 文件系统操作**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
|
// 读取文件
|
||||||
|
fs.readFile('./data.txt', 'utf8', (err, data) => {
|
||||||
|
if (err) throw err
|
||||||
|
console.log(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 写入文件
|
||||||
|
fs.writeFile('./output.txt', 'Hello', (err) => {
|
||||||
|
if (err) throw err
|
||||||
|
console.log('写入成功')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. HTTP 服务器**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const http = require('http')
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html' })
|
||||||
|
res.end('<h1>Hello World</h1>')
|
||||||
|
})
|
||||||
|
|
||||||
|
server.listen(3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. 模块系统**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// CommonJS (Node.js 默认)
|
||||||
|
const fs = require('fs')
|
||||||
|
module.exports = { myFunction }
|
||||||
|
|
||||||
|
// ES Modules (现代方式)
|
||||||
|
import fs from 'fs'
|
||||||
|
export { myFunction }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 浏览器 vs Node.js 对比
|
||||||
|
|
||||||
|
| 特性 | 浏览器 | Node.js |
|
||||||
|
|------|--------|---------|
|
||||||
|
| **入口文件** | HTML 文件 | JavaScript 文件 |
|
||||||
|
| **全局对象** | `window`, `document` | `global`, `process` |
|
||||||
|
| **模块加载** | `<script>` 标签 | `require()` / `import` |
|
||||||
|
| **安全性** | 沙箱环境,受限 | 可以访问系统资源 |
|
||||||
|
| **用途** | 用户界面 | 后端服务、工具 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 事件循环深入
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**JavaScript 是单线程的,为什么能做到"不阻塞"?**
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 4.1 事件循环是什么
|
||||||
|
|
||||||
|
**事件循环 = JavaScript 的"任务调度中心"**
|
||||||
|
|
||||||
|
JavaScript 是单线程的,一次只能做一件事。但事件循环让它看起来能"同时"做很多事。
|
||||||
|
|
||||||
|
**核心机制:**
|
||||||
|
|
||||||
|
1. **执行同步代码** (调用栈)
|
||||||
|
2. **处理异步任务** (任务队列)
|
||||||
|
3. **等待新任务** (循环往复)
|
||||||
|
|
||||||
|
```
|
||||||
|
调用栈 任务队列
|
||||||
|
┌─────────┐ ┌──────────┐
|
||||||
|
│ 任务 1 │ │ 宏任务 1 │
|
||||||
|
│ 任务 2 │ ←──────────── │ 宏任务 2 │
|
||||||
|
│ 任务 3 │ 执行完一个 │ 宏任务 3 │
|
||||||
|
└─────────┘ 就取下一个 └──────────┘
|
||||||
|
↓ ↑
|
||||||
|
└────────────────────────┘
|
||||||
|
事件循环不断检查
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 宏任务 vs 微任务
|
||||||
|
|
||||||
|
这是面试和实际开发中最容易搞混的概念!
|
||||||
|
|
||||||
|
**宏任务 (Macrotask):**
|
||||||
|
- `setTimeout`, `setInterval`
|
||||||
|
- I/O 操作
|
||||||
|
- UI 渲染
|
||||||
|
|
||||||
|
**微任务 (Microtask):**
|
||||||
|
- `Promise.then`
|
||||||
|
- `MutationObserver`
|
||||||
|
- `queueMicrotask`
|
||||||
|
|
||||||
|
**执行顺序:同步代码 → 微任务 → 宏任务**
|
||||||
|
|
||||||
|
👇 **动手试试看**:观察宏任务和微任务的执行顺序
|
||||||
|
|
||||||
|
<TaskQueueDemo />
|
||||||
|
|
||||||
|
### 4.3 经典面试题
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
console.log('1')
|
||||||
|
|
||||||
|
setTimeout(() => console.log('2'), 0)
|
||||||
|
|
||||||
|
Promise.resolve().then(() => console.log('3'))
|
||||||
|
|
||||||
|
console.log('4')
|
||||||
|
|
||||||
|
// 输出: 1, 4, 3, 2
|
||||||
|
```
|
||||||
|
|
||||||
|
**为什么是这个顺序?**
|
||||||
|
|
||||||
|
1. 执行同步代码:`console.log('1')`,`console.log('4')` → 输出 1, 4
|
||||||
|
2. 检查微任务队列:`Promise.then` → 输出 3
|
||||||
|
3. 检查宏任务队列:`setTimeout` → 输出 2
|
||||||
|
|
||||||
|
::: info 💡 实战技巧
|
||||||
|
- 如果想让代码尽快执行,用微任务 (`Promise.then`)
|
||||||
|
- 如果想延迟执行,用宏任务 (`setTimeout`)
|
||||||
|
- 永远不要混用太多异步操作,否则会陷入"回调地狱"
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 调用栈与内存
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**代码是怎么被执行的?变量存在哪里?什么时候被回收?**
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 5.1 调用栈:函数执行的"足迹"
|
||||||
|
|
||||||
|
**调用栈 = 记录函数调用的"笔记本"**
|
||||||
|
|
||||||
|
每次调用一个函数,就会在栈上新增一条记录;函数执行完,记录就被移除。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function a() {
|
||||||
|
b()
|
||||||
|
}
|
||||||
|
|
||||||
|
function b() {
|
||||||
|
c()
|
||||||
|
}
|
||||||
|
|
||||||
|
function c() {
|
||||||
|
console.log('执行完毕')
|
||||||
|
}
|
||||||
|
|
||||||
|
a()
|
||||||
|
```
|
||||||
|
|
||||||
|
**调用栈的变化:**
|
||||||
|
|
||||||
|
```
|
||||||
|
步骤 1: 调用 a()
|
||||||
|
┌─────────┐
|
||||||
|
│ a │
|
||||||
|
└─────────┘
|
||||||
|
|
||||||
|
步骤 2: a() 调用 b()
|
||||||
|
┌─────────┐
|
||||||
|
│ b │
|
||||||
|
│ a │
|
||||||
|
└─────────┘
|
||||||
|
|
||||||
|
步骤 3: b() 调用 c()
|
||||||
|
┌─────────┐
|
||||||
|
│ c │
|
||||||
|
│ b │
|
||||||
|
│ a │
|
||||||
|
└─────────┘
|
||||||
|
|
||||||
|
步骤 4: c() 执行完,依次弹出
|
||||||
|
┌─────────┐
|
||||||
|
│ b │
|
||||||
|
│ a │
|
||||||
|
└─────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
👇 **动手试试看**:观察调用栈的变化
|
||||||
|
|
||||||
|
<CallStackDemo />
|
||||||
|
|
||||||
|
### 5.2 内存管理:垃圾去哪儿了
|
||||||
|
|
||||||
|
JavaScript 有"自动垃圾回收"机制——你不需要手动释放内存,引擎会帮你做。
|
||||||
|
|
||||||
|
**垃圾回收的原理:标记-清除算法**
|
||||||
|
|
||||||
|
1. **标记阶段**:从"根"开始,找到所有能访问的变量
|
||||||
|
2. **清除阶段**:没被标记的变量就是"垃圾",会被回收
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 垃圾回收示例
|
||||||
|
let obj1 = { name: '对象1' }
|
||||||
|
let obj2 = { name: '对象2' }
|
||||||
|
|
||||||
|
// obj1 被重新赋值,原来的对象失去了引用
|
||||||
|
obj1 = null // 原来的 { name: '对象1' } 会被回收
|
||||||
|
|
||||||
|
// obj2 还在使用中,不会被回收
|
||||||
|
console.log(obj2.name)
|
||||||
|
```
|
||||||
|
|
||||||
|
👇 **动手试试看**:观察垃圾回收的过程
|
||||||
|
|
||||||
|
<GarbageCollectionDemo />
|
||||||
|
|
||||||
|
### 5.3 内存泄漏:忘记清理的后果
|
||||||
|
|
||||||
|
**内存泄漏 = 该释放的内存没释放,越积越多**
|
||||||
|
|
||||||
|
常见原因:
|
||||||
|
|
||||||
|
**1. 全局变量太多**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ 错误:全局变量不会被回收
|
||||||
|
globalCache = []
|
||||||
|
|
||||||
|
function addItem(item) {
|
||||||
|
globalCache.push(item)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 事件监听没移除**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ 错误:监听器没移除
|
||||||
|
button.addEventListener('click', handleClick)
|
||||||
|
|
||||||
|
// ✅ 正确:不需要时移除监听
|
||||||
|
button.removeEventListener('click', handleClick)
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. 闭包引用大对象**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ 错误:闭包一直引用大对象,不会被回收
|
||||||
|
function createHandler() {
|
||||||
|
const bigData = new Array(1000000).fill('data')
|
||||||
|
return function() {
|
||||||
|
console.log('处理中')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = createHandler() // bigData 一直存在于内存中
|
||||||
|
```
|
||||||
|
|
||||||
|
👇 **动手试试看**:观察内存泄漏是如何发生的
|
||||||
|
|
||||||
|
<MemoryLeakDemo />
|
||||||
|
|
||||||
|
::: info 💡 实战技巧
|
||||||
|
- **定期检查**:打开浏览器 DevTools → Memory → Take Heap Snapshot,查看内存占用
|
||||||
|
- **避免全局变量**:尽量用 `const` 和 `let`,不用 `var`
|
||||||
|
- **及时清理**:事件监听、定时器用完要移除
|
||||||
|
- **弱引用**:用 `WeakMap` 和 `WeakSet` 存储对象引用
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 实战技巧
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**怎么写出高性能的 JavaScript 代码?遇到问题怎么调试?**
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 6.1 性能优化技巧
|
||||||
|
|
||||||
|
**1. 减少重排重绘**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ 错误:每次循环都触发重排
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
element.style.top = i + 'px'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 正确:批量修改
|
||||||
|
element.style.transform = `translateY(${position}px)`
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 使用事件委托**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ 错误:给每个按钮都添加监听
|
||||||
|
buttons.forEach(btn => {
|
||||||
|
btn.addEventListener('click', handleClick)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ✅ 正确:只给父元素添加一个监听
|
||||||
|
container.addEventListener('click', (e) => {
|
||||||
|
if (e.target.matches('.button')) {
|
||||||
|
handleClick(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. 防抖和节流**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 防抖:用户停止输入后再执行
|
||||||
|
function debounce(fn, delay) {
|
||||||
|
let timer
|
||||||
|
return function(...args) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
timer = setTimeout(() => fn.apply(this, args), delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 节流:限制执行频率
|
||||||
|
function throttle(fn, delay) {
|
||||||
|
let lastTime = 0
|
||||||
|
return function(...args) {
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - lastTime >= delay) {
|
||||||
|
fn.apply(this, args)
|
||||||
|
lastTime = now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 调试技巧
|
||||||
|
|
||||||
|
**1. 用 DevTools 查看调用栈**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function a() {
|
||||||
|
b()
|
||||||
|
}
|
||||||
|
|
||||||
|
function b() {
|
||||||
|
c()
|
||||||
|
}
|
||||||
|
|
||||||
|
function c() {
|
||||||
|
debugger // 在这里暂停,查看调用栈
|
||||||
|
}
|
||||||
|
|
||||||
|
a()
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 用 `console.trace()` 追踪执行路径**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function trackExecution() {
|
||||||
|
console.trace('执行路径')
|
||||||
|
// 会输出完整的调用栈
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. 用 Performance 分析性能**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
performance.mark('start')
|
||||||
|
|
||||||
|
// 执行一些代码
|
||||||
|
for (let i = 0; i < 10000; i++) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
performance.mark('end')
|
||||||
|
performance.measure('循环性能', 'start', 'end')
|
||||||
|
|
||||||
|
const measure = performance.getEntriesByName('循环性能')[0]
|
||||||
|
console.log(`执行时间: ${measure.duration}ms`)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 常见问题速查
|
||||||
|
|
||||||
|
| 问题 | 可能原因 | 解决方案 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| **内存占用高** | 内存泄漏、缓存太多 | 检查全局变量、移除监听器 |
|
||||||
|
| **页面卡顿** | 长任务阻塞主线程 | 拆分任务、用 Web Workers |
|
||||||
|
| **事件不触发** | 监听器没绑定、元素不存在 | 检查 DOM 加载时机 |
|
||||||
|
| **异步顺序错乱** | 混用宏任务和微任务 | 统一用 Promise 或 async/await |
|
||||||
|
| **定时器不准** | 主线程阻塞 | 用 Web Workers 或 requestAnimationFrame |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
你现在应该能理解:
|
||||||
|
|
||||||
|
- **运行时 = 引擎 + 环境 API**,不同运行时提供不同能力
|
||||||
|
- **事件循环**负责协调同步代码、微任务、宏任务的执行顺序
|
||||||
|
- **调用栈**记录函数执行过程,**栈溢出**是因为递归太深
|
||||||
|
- **垃圾回收**自动清理不用的变量,但要注意**内存泄漏**
|
||||||
|
- **性能优化**的关键是减少重排重绘、合理使用异步
|
||||||
|
|
||||||
|
::: info 💡 遇到问题时这样跟 AI 说
|
||||||
|
- "这个函数执行太慢,帮我看看怎么优化性能"
|
||||||
|
- "内存占用一直在涨,可能是内存泄漏,帮我检查一下"
|
||||||
|
- "异步操作顺序不对,应该是先 A 再 B,现在是 A 和 B 几乎同时开始"
|
||||||
|
- "事件监听器没有触发,检查一下元素是否已经加载到 DOM"
|
||||||
|
:::
|
||||||
|
|||||||
@@ -1,3 +1,823 @@
|
|||||||
# TypeScript:给 JS 加上类型系统
|
# TypeScript 深度指南
|
||||||
|
|
||||||
> 待实现
|
::: tip 前言
|
||||||
|
你已经会写 JavaScript 了,但可能遇到过这些问题:
|
||||||
|
- 变量赋值了错误类型,运行时才发现
|
||||||
|
- 对象属性写错了名字,调试半天
|
||||||
|
- 函数参数类型不对,改来改去
|
||||||
|
|
||||||
|
TypeScript 就是在代码运行前帮你发现这些问题的工具。读完这篇,你就能理解 TypeScript 为什么能提升代码质量,看懂类型注解、接口、泛型等核心概念,在 vibecoding 中更好地利用 AI 生成的代码。
|
||||||
|
:::
|
||||||
|
|
||||||
|
**这篇文章会带你学什么?**
|
||||||
|
|
||||||
|
| 章节 | 内容 | 学完能干嘛 |
|
||||||
|
|-----|------|-----------|
|
||||||
|
| **第 1 章** | TypeScript 是什么 | 明白它和 JavaScript 的关系 |
|
||||||
|
| **第 2 章** | 基础类型注解 | 知道怎么给变量标注类型 |
|
||||||
|
| **第 3 章** | 对象类型与接口 | 定义数据结构的类型 |
|
||||||
|
| **第 4 章** | 函数类型 | 给函数参数和返回值标注类型 |
|
||||||
|
| **第 5 章** | 泛型 | 编写可复用的类型安全代码 |
|
||||||
|
| **第 6 章** | 类型推断与实用技巧 | 知道何时需要显式注解 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. TypeScript 是什么
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**JavaScript 已经够用了,为什么还需要 TypeScript?** 多学一门语法值得吗?
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 1.1 从"运行时出错"到"编译时发现"
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 20px; margin: 20px 0;">
|
||||||
|
<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;">
|
||||||
|
|
||||||
|
**🔴 JavaScript 的痛点**
|
||||||
|
- 运行时才发现类型错误
|
||||||
|
- 拼写错误难以察觉
|
||||||
|
- 重构时容易遗漏
|
||||||
|
- IDE 提示不够准确
|
||||||
|
|
||||||
|
*就像没有拼写检查的文档编辑器*
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; padding: 16px; border: 1px solid #e4e7ed; border-radius: 12px;">
|
||||||
|
|
||||||
|
**✅ TypeScript 的优势**
|
||||||
|
- 写代码时就发现错误
|
||||||
|
- 智能提示更准确
|
||||||
|
- 重构更安全
|
||||||
|
- 代码更易维护
|
||||||
|
|
||||||
|
*就像有拼写检查和语法高亮的编辑器*
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
**用一句话理解两者的关系:**
|
||||||
|
|
||||||
|
| 技术 | 比喻 | 作用 |
|
||||||
|
|------|------|------|
|
||||||
|
| **JavaScript** | 原始材料 | 可以直接运行的代码 |
|
||||||
|
| **TypeScript** | 蓝图 + 质检 | 给 JavaScript 加类型检查,最后编译成 JavaScript |
|
||||||
|
|
||||||
|
### 1.2 为什么 vibecoding 也需要 TypeScript?
|
||||||
|
|
||||||
|
::: warning AI 写代码也会出错
|
||||||
|
一位开发者用 AI 生成了一个用户管理功能。AI 写的 JavaScript 代码能运行,但有个问题:用户年龄应该是数字,但有时候会被错误地赋值为字符串。
|
||||||
|
|
||||||
|
结果在计算"是否成年"时,字符串 "25" 被当成字符串处理,导致判断失败。这个 bug 隐藏了很久,直到某个用户输入了非数字字符才暴露出来。
|
||||||
|
|
||||||
|
如果用 TypeScript,这段代码在写的时候就会报错:`不能将类型 "string" 分配给类型 "number"`。
|
||||||
|
|
||||||
|
**这就是 TypeScript 的价值——在 AI 写错类型时,你能第一时间发现。**
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 1.3 TypeScript 实际上是这样的
|
||||||
|
|
||||||
|
TypeScript 不是一门全新的语言,它只是 JavaScript 的"超集":
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 这是有效的 JavaScript,也是有效的 TypeScript
|
||||||
|
const name = "张三"
|
||||||
|
const age = 25
|
||||||
|
function greet(user) {
|
||||||
|
return `Hello ${user}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这是 TypeScript 特有的类型注解
|
||||||
|
const name2: string = "李四"
|
||||||
|
const age2: number = 30
|
||||||
|
function greet2(user: string): string {
|
||||||
|
return `Hello ${user}`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键理解:**
|
||||||
|
- 所有 JavaScript 代码都是有效的 TypeScript 代码
|
||||||
|
- TypeScript 添加了可选的**类型注解**
|
||||||
|
- TypeScript 最终会编译成 JavaScript 运行
|
||||||
|
|
||||||
|
::: info 💡 核心启示
|
||||||
|
TypeScript 不会改变代码的运行方式,它只是在编译时帮你检查类型是否正确。**你可以渐进地采用 TypeScript**——从给关键变量添加类型开始。
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 基础类型注解
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**怎么告诉 TypeScript 一个变量应该是什么类型?** 类型注解的语法是怎样的?
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 2.1 类型注解语法
|
||||||
|
|
||||||
|
类型注解就是在变量名后面加上`: 类型`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 语法:变量名: 类型 = 值
|
||||||
|
const name: string = "张三"
|
||||||
|
let age: number = 25
|
||||||
|
let isStudent: boolean = true
|
||||||
|
```
|
||||||
|
|
||||||
|
👇 **动手试试看**:给变量添加类型注解
|
||||||
|
|
||||||
|
<TypeAnnotationDemo />
|
||||||
|
|
||||||
|
::: details 🔍 为什么有些地方不需要类型注解?
|
||||||
|
TypeScript 可以根据赋值自动推断类型:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 这些不需要类型注解,TypeScript 能自动推断
|
||||||
|
const name = "张三" // 推断为 string
|
||||||
|
const age = 25 // 推断为 number
|
||||||
|
const isActive = true // 推断为 boolean
|
||||||
|
|
||||||
|
// 这些情况需要显式注解
|
||||||
|
let data // ❌ 错误:不能推断类型
|
||||||
|
let data: any // ✅ 可以,但失去了类型检查的好处
|
||||||
|
|
||||||
|
function add(a, b) { // ❌ 参数类型不明确
|
||||||
|
return a + b
|
||||||
|
}
|
||||||
|
|
||||||
|
function add2(a: number, b: number): number { // ✅ 类型明确
|
||||||
|
return a + b
|
||||||
|
}
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 2.2 基本类型
|
||||||
|
|
||||||
|
TypeScript 支持所有 JavaScript 的基本类型:
|
||||||
|
|
||||||
|
| 类型 | 说明 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `string` | 字符串 | `"hello"`, `'你好'` |
|
||||||
|
| `number` | 数字(整数和小数) | `42`, `3.14` |
|
||||||
|
| `boolean` | 布尔值 | `true`, `false` |
|
||||||
|
| `null` / `undefined` | 空值 | `null`, `undefined` |
|
||||||
|
| `array` | 数组 | `number[]`, `string[]` |
|
||||||
|
| `object` | 对象 | `{ name: string; age: number }` |
|
||||||
|
|
||||||
|
**数组类型的两种写法:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 写法 1:类型[](更常用)
|
||||||
|
const numbers: number[] = [1, 2, 3, 4, 5]
|
||||||
|
const names: string[] = ["张三", "李四", "王五"]
|
||||||
|
|
||||||
|
// 写法 2:Array<类型>
|
||||||
|
const numbers2: Array<number> = [1, 2, 3, 4, 5]
|
||||||
|
const names2: Array<string> = ["张三", "李四", "王五"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**特殊类型:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// any:任意类型(慎用,相当于关闭类型检查)
|
||||||
|
let data: any = 42
|
||||||
|
data = "现在可以是字符串"
|
||||||
|
data = { name: "张三" } // 也可以是对象
|
||||||
|
|
||||||
|
// unknown:类型安全的 any
|
||||||
|
let value: unknown = 42
|
||||||
|
// if (typeof value === "number") {
|
||||||
|
// console.log(value + 10) // 需要先检查类型才能用
|
||||||
|
// }
|
||||||
|
|
||||||
|
// void:没有返回值
|
||||||
|
function log(message: string): void {
|
||||||
|
console.log(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// never:永远不会返回
|
||||||
|
function error(message: string): never {
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
::: info 💡 识别技巧
|
||||||
|
- 看到 `: string` → 这是 string 类型的注解
|
||||||
|
- 看到 `: number[]` → 这是数字数组的注解
|
||||||
|
- 看到 `: void` → 这个函数没有返回值
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 对象类型与接口
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**怎么定义一个对象的类型?** 对象的属性应该是什么类型?
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 3.1 接口(Interface):定义对象的"形状"
|
||||||
|
|
||||||
|
接口是 TypeScript 中定义对象类型的主要方式:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 定义一个 User 接口
|
||||||
|
interface User {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
age?: number // 可选属性
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用接口
|
||||||
|
const user: User = {
|
||||||
|
id: 1,
|
||||||
|
name: "张三",
|
||||||
|
email: "zhangsan@example.com",
|
||||||
|
age: 25
|
||||||
|
}
|
||||||
|
|
||||||
|
// age 是可选的,可以不提供
|
||||||
|
const user2: User = {
|
||||||
|
id: 2,
|
||||||
|
name: "李四",
|
||||||
|
email: "lisi@example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
👇 **动手试试看**:创建符合接口定义的对象
|
||||||
|
|
||||||
|
<InterfaceDemo />
|
||||||
|
|
||||||
|
::: details 🔍 接口的其他特性
|
||||||
|
```typescript
|
||||||
|
// 只读属性
|
||||||
|
interface User {
|
||||||
|
readonly id: number // id 创建后不能修改
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const user: User = {
|
||||||
|
id: 1,
|
||||||
|
name: "张三"
|
||||||
|
}
|
||||||
|
|
||||||
|
user.id = 2 // ❌ 错误:不能修改只读属性
|
||||||
|
user.name = "李四" // ✅ 可以修改
|
||||||
|
|
||||||
|
// 函数类型
|
||||||
|
interface User {
|
||||||
|
name: string
|
||||||
|
greet: () => string // greet 是一个函数,返回 string
|
||||||
|
}
|
||||||
|
|
||||||
|
const user: User = {
|
||||||
|
name: "张三",
|
||||||
|
greet: () => "Hello"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 继承接口
|
||||||
|
interface Admin extends User {
|
||||||
|
permissions: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const admin: Admin = {
|
||||||
|
name: "管理员",
|
||||||
|
greet: () => "Hello Admin",
|
||||||
|
permissions: ["read", "write", "delete"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 3.2 类型别名(Type Alias)
|
||||||
|
|
||||||
|
除了接口,还可以用 `type` 定义类型别名:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 类型别名
|
||||||
|
type User = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 联合类型
|
||||||
|
type Status = "pending" | "success" | "error"
|
||||||
|
|
||||||
|
const status: Status = "success" // ✅
|
||||||
|
// const status2: Status = "failed" // ❌ 错误:不在联合类型中
|
||||||
|
|
||||||
|
// 交叉类型(合并多个类型)
|
||||||
|
type User = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Timestamp = {
|
||||||
|
createdAt: Date
|
||||||
|
updatedAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserWithTimestamp = User & Timestamp
|
||||||
|
|
||||||
|
const user: UserWithTimestamp = {
|
||||||
|
id: 1,
|
||||||
|
name: "张三",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**接口 vs 类型别名:**
|
||||||
|
|
||||||
|
| 特性 | interface | type |
|
||||||
|
|------|-----------|------|
|
||||||
|
| 扩展 | `extends` | `&` 交叉类型 |
|
||||||
|
| 重复声明 | 会自动合并 | 会报错 |
|
||||||
|
| 适用场景 | 对象形状、类 | 联合类型、交叉类型、基本类型别名 |
|
||||||
|
|
||||||
|
::: info 💡 识别技巧
|
||||||
|
- 看到 `interface` → 这是定义对象类型
|
||||||
|
- 看到 `type` → 这是创建类型别名
|
||||||
|
- 看到 `?` → 这是可选属性
|
||||||
|
- 看到 `readonly` → 这是只读属性
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 函数类型
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**怎么给函数的参数和返回值标注类型?**
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 4.1 参数类型与返回值类型
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 完整的函数类型注解
|
||||||
|
function add(a: number, b: number): number {
|
||||||
|
return a + b
|
||||||
|
}
|
||||||
|
|
||||||
|
// 箭头函数
|
||||||
|
const multiply = (a: number, b: number): number => {
|
||||||
|
return a * b
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有返回值
|
||||||
|
function log(message: string): void {
|
||||||
|
console.log(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回多种类型(联合类型)
|
||||||
|
function parseInput(input: string): number | string {
|
||||||
|
const num = parseFloat(input)
|
||||||
|
return isNaN(num) ? input : num
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 可选参数与默认参数
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 可选参数(用 ? 标记)
|
||||||
|
function greet(name: string, title?: string): string {
|
||||||
|
return title ? `${title} ${name}` : name
|
||||||
|
}
|
||||||
|
|
||||||
|
greet("张三") // "张三"
|
||||||
|
greet("张三", "先生") // "先生 张三"
|
||||||
|
|
||||||
|
// 默认参数
|
||||||
|
function greet2(name: string, title: string = "朋友"): string {
|
||||||
|
return `${title} ${name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
greet2("李四") // "朋友 李四"
|
||||||
|
greet2("李四", "博士") // "博士 李四"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 函数类型作为参数
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 接受函数作为参数
|
||||||
|
function calculate(
|
||||||
|
a: number,
|
||||||
|
b: number,
|
||||||
|
operation: (x: number, y: number) => number
|
||||||
|
): number {
|
||||||
|
return operation(a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate(10, 5, (x, y) => x + y) // 15
|
||||||
|
calculate(10, 5, (x, y) => x * y) // 50
|
||||||
|
|
||||||
|
// 更清晰的写法:先定义函数类型
|
||||||
|
type Operation = (x: number, y: number) => number
|
||||||
|
|
||||||
|
function calculate2(
|
||||||
|
a: number,
|
||||||
|
b: number,
|
||||||
|
operation: Operation
|
||||||
|
): number {
|
||||||
|
return operation(a, b)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
::: info 💡 识别技巧
|
||||||
|
- 看到 `(a: number, b: number) => number` → 这是函数类型,描述参数和返回值
|
||||||
|
- 看到 `: void` → 函数没有返回值
|
||||||
|
- 看到 `?` → 参数是可选的
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 泛型
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**怎么编写能处理多种类型、但保持类型安全的代码?**
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 5.1 泛型的基本概念
|
||||||
|
|
||||||
|
泛型让你在定义函数、接口或类时,不预先指定具体的类型,而是在使用时再指定:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 泛型函数:T 是类型变量
|
||||||
|
function identity<T>(arg: T): T {
|
||||||
|
return arg
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用时明确指定类型
|
||||||
|
const num1 = identity<number>(42) // 类型是 number
|
||||||
|
const str1 = identity<string>("hello") // 类型是 string
|
||||||
|
|
||||||
|
// 类型推断:TypeScript 能自动推断
|
||||||
|
const num2 = identity(42) // 推断为 number
|
||||||
|
const str2 = identity("hello") // 推断为 string
|
||||||
|
```
|
||||||
|
|
||||||
|
👇 **动手试试看**:使用泛型处理不同类型的数据
|
||||||
|
|
||||||
|
<GenericDemo />
|
||||||
|
|
||||||
|
### 5.2 泛型约束
|
||||||
|
|
||||||
|
限制泛型必须满足某些条件:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 约束 T 必须有 length 属性
|
||||||
|
interface HasLength {
|
||||||
|
length: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function logLength<T extends HasLength>(arg: T): void {
|
||||||
|
console.log(arg.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
logLength("hello") // ✅ 字符串有 length
|
||||||
|
logLength([1, 2, 3]) // ✅ 数组有 length
|
||||||
|
// logLength(42) // ❌ 数字没有 length 属性
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 泛型接口和类
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 泛型接口
|
||||||
|
interface Box<T> {
|
||||||
|
value: T
|
||||||
|
getValue(): T
|
||||||
|
}
|
||||||
|
|
||||||
|
const numberBox: Box<number> = {
|
||||||
|
value: 42,
|
||||||
|
getValue: () => 42
|
||||||
|
}
|
||||||
|
|
||||||
|
const stringBox: Box<string> = {
|
||||||
|
value: "hello",
|
||||||
|
getValue: () => "hello"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 泛型类
|
||||||
|
class Storage<T> {
|
||||||
|
private items: T[] = []
|
||||||
|
|
||||||
|
add(item: T): void {
|
||||||
|
this.items.push(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
get(index: number): T {
|
||||||
|
return this.items[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const numberStorage = new Storage<number>()
|
||||||
|
numberStorage.add(1)
|
||||||
|
numberStorage.add(2)
|
||||||
|
// numberStorage.add("string") // ❌ 错误
|
||||||
|
|
||||||
|
const stringStorage = new Storage<string>()
|
||||||
|
stringStorage.add("hello")
|
||||||
|
// stringStorage.add(1) // ❌ 错误
|
||||||
|
```
|
||||||
|
|
||||||
|
::: info 💡 识别技巧
|
||||||
|
- 看到 `<T>` → 这是泛型类型变量
|
||||||
|
- 看到 `<T extends SomeType>` → 泛型约束
|
||||||
|
- 看到 `Array<T>` 或 `Promise<T>` → 内置泛型类型
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 类型推断与实用技巧
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**什么时候需要显式类型注解?什么时候可以依赖推断?**
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 6.1 类型推断
|
||||||
|
|
||||||
|
TypeScript 能根据上下文自动推断类型:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 变量初始化时的推断
|
||||||
|
const name = "张三" // 推断为 string
|
||||||
|
const age = 25 // 推断为 number
|
||||||
|
const isActive = true // 推断为 boolean
|
||||||
|
|
||||||
|
// 数组推断
|
||||||
|
const numbers = [1, 2, 3] // 推断为 number[]
|
||||||
|
const mixed = [1, "hello", true] // 推断为 (number | string | boolean)[]
|
||||||
|
|
||||||
|
// 函数返回值推断
|
||||||
|
function add(a: number, b: number) {
|
||||||
|
return a + b // 推断返回值为 number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
👇 **动手试试看**:观察 TypeScript 如何推断类型
|
||||||
|
|
||||||
|
<TypeInferenceDemo />
|
||||||
|
|
||||||
|
### 6.2 何时使用显式类型注解
|
||||||
|
|
||||||
|
::: details 推荐使用类型推断的场景
|
||||||
|
```typescript
|
||||||
|
// ✅ 推荐:简单的字面量赋值
|
||||||
|
const count = 0
|
||||||
|
const name = "张三"
|
||||||
|
const isActive = true
|
||||||
|
|
||||||
|
// ✅ 推荐:函数返回值可以推断
|
||||||
|
function getUserId(user: User) {
|
||||||
|
return user.id // 推断为 number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: details 推荐使用显式注解的场景
|
||||||
|
```typescript
|
||||||
|
// ✅ 推荐:函数参数(必须)
|
||||||
|
function add(a: number, b: number) {
|
||||||
|
return a + b
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 推荐:对象属性类型不明确
|
||||||
|
const user: {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
metadata: Record<string, any>
|
||||||
|
} = {
|
||||||
|
id: 1,
|
||||||
|
name: "张三",
|
||||||
|
metadata: {} // 可能推断为 {},需要明确指定
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 推荐:函数返回类型复杂
|
||||||
|
function getUser(): User | null {
|
||||||
|
// ...
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 推荐:公共 API
|
||||||
|
export function calculateTotal(prices: number[]): number {
|
||||||
|
return prices.reduce((sum, price) => sum + price, 0)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 6.3 类型守卫
|
||||||
|
|
||||||
|
在运行时检查类型:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// typeof 类型守卫
|
||||||
|
function processValue(value: string | number) {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
// 这里 TypeScript 知道 value 是 string
|
||||||
|
console.log(value.toUpperCase())
|
||||||
|
} else {
|
||||||
|
// 这里 TypeScript 知道 value 是 number
|
||||||
|
console.log(value * 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// instanceof 类型守卫
|
||||||
|
class Dog {
|
||||||
|
bark() {
|
||||||
|
console.log("汪汪")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Cat {
|
||||||
|
meow() {
|
||||||
|
console.log("喵喵")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeSound(animal: Dog | Cat) {
|
||||||
|
if (animal instanceof Dog) {
|
||||||
|
animal.bark() // TypeScript 知道这是 Dog
|
||||||
|
} else {
|
||||||
|
animal.meow() // TypeScript 知道这是 Cat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义类型守卫
|
||||||
|
interface User {
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUser(value: any): value is User {
|
||||||
|
return (
|
||||||
|
typeof value === "object" &&
|
||||||
|
value !== null &&
|
||||||
|
typeof value.name === "string" &&
|
||||||
|
typeof value.email === "string"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function processValue(value: unknown) {
|
||||||
|
if (isUser(value)) {
|
||||||
|
// 这里 value 是 User
|
||||||
|
console.log(value.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 实用工具类型
|
||||||
|
|
||||||
|
TypeScript 提供了一些内置的工具类型:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Partial:将所有属性变为可选
|
||||||
|
interface User {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PartialUser = Partial<User>
|
||||||
|
// 等价于:{ id?: number; name?: string; email?: string }
|
||||||
|
|
||||||
|
// Required:将所有属性变为必需
|
||||||
|
type RequiredUser = Required<PartialUser>
|
||||||
|
// 等价于:{ id: number; name: number; email: string }
|
||||||
|
|
||||||
|
// Pick:只保留指定的属性
|
||||||
|
type UserBasicInfo = Pick<User, "id" | "name">
|
||||||
|
// 等价于:{ id: number; name: string }
|
||||||
|
|
||||||
|
// Omit:排除指定的属性
|
||||||
|
type UserWithoutEmail = Omit<User, "email">
|
||||||
|
// 等价于:{ id: number; name: string }
|
||||||
|
|
||||||
|
// Record:创建对象类型
|
||||||
|
type UserRoles = Record<string, boolean>
|
||||||
|
// 等价于:{ [key: string]: boolean }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 实战技巧:在 vibecoding 中使用 TypeScript
|
||||||
|
|
||||||
|
::: tip 🤔 核心问题
|
||||||
|
**怎么在 AI 辅助开发中更好地利用 TypeScript?**
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 7.1 让 AI 生成类型安全代码
|
||||||
|
|
||||||
|
**❌ 不好的提示词:**
|
||||||
|
```
|
||||||
|
帮我写一个用户管理功能
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ 好的提示词:**
|
||||||
|
```
|
||||||
|
帮我写一个用户管理功能,使用 TypeScript。
|
||||||
|
|
||||||
|
数据结构定义如下:
|
||||||
|
interface User {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
age: number
|
||||||
|
}
|
||||||
|
|
||||||
|
需要实现:
|
||||||
|
1. 获取用户列表:返回 User[]
|
||||||
|
2. 创建用户:接受 Partial<User>,返回 User
|
||||||
|
3. 更新用户:接受 id 和 Partial<User>,返回 User
|
||||||
|
4. 删除用户:接受 id,返回 void
|
||||||
|
|
||||||
|
请确保所有函数都有完整的类型注解。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 看懂 TypeScript 错误信息
|
||||||
|
|
||||||
|
**常见错误及含义:**
|
||||||
|
|
||||||
|
| 错误信息 | 含义 | 解决方法 |
|
||||||
|
|---------|------|---------|
|
||||||
|
| `Type 'X' is not assignable to type 'Y'` | 类型 X 不能赋值给类型 Y | 检查类型是否匹配,或进行类型转换 |
|
||||||
|
| `Property 'X' does not exist on type 'Y'` | 类型 Y 上不存在属性 X | 检查属性名拼写,或定义该属性 |
|
||||||
|
| `Argument of type 'X' is not assignable to parameter of type 'Y'` | 参数类型不匹配 | 检查函数调用时的参数类型 |
|
||||||
|
| `Type 'X' is missing the following properties from type 'Y'` | 类型 X 缺少类型 Y 的某些属性 | 补全缺失的属性 |
|
||||||
|
|
||||||
|
### 7.3 渐进式采用 TypeScript
|
||||||
|
|
||||||
|
如果你有一个 JavaScript 项目,可以渐进地迁移到 TypeScript:
|
||||||
|
|
||||||
|
1. **第一步:将文件重命名为 `.ts`**
|
||||||
|
```bash
|
||||||
|
# 从 utils.js 改为 utils.ts
|
||||||
|
mv utils.js utils.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **第二步:修复明显的类型错误**
|
||||||
|
```typescript
|
||||||
|
// 如果报错:Parameter 'a' implicitly has an 'any' type
|
||||||
|
// 添加类型注解
|
||||||
|
function add(a: number, b: number) {
|
||||||
|
return a + b
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **第三步:逐步添加类型定义**
|
||||||
|
```typescript
|
||||||
|
// 先用 any 快速修复
|
||||||
|
function processUser(user: any) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后续再完善类型
|
||||||
|
interface User {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function processUser(user: User) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **第四步:启用更严格的类型检查**
|
||||||
|
```json
|
||||||
|
// tsconfig.json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true, // 启用严格模式
|
||||||
|
"noImplicitAny": true, // 禁止隐式 any
|
||||||
|
"strictNullChecks": true // 严格空值检查
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 你现在应该能识别的代码
|
||||||
|
|
||||||
|
- 看到 `: string` → 这是 string 类型的注解
|
||||||
|
- 看到 `: number[]` → 这是数字数组的注解
|
||||||
|
- 看到 `interface User` → 这是定义对象类型
|
||||||
|
- 看到 `type User =` → 这是类型别名
|
||||||
|
- 看到 `<T>` → 这是泛型
|
||||||
|
- 看到 `extends` → 接口继承或泛型约束
|
||||||
|
- 看到 `?` → 可选属性
|
||||||
|
- 看到 `readonly` → 只读属性
|
||||||
|
- 看到 `|` → 联合类型
|
||||||
|
- 看到 `&` → 交叉类型
|
||||||
|
|
||||||
|
**如果你认真读了每章的"深入"部分,你还掌握了这些核心概念:**
|
||||||
|
|
||||||
|
- **类型注解**:明确告诉 TypeScript 变量的类型
|
||||||
|
- **接口**:定义对象的结构和类型
|
||||||
|
- **泛型**:编写可复用的类型安全代码
|
||||||
|
- **类型推断**:TypeScript 自动推断类型
|
||||||
|
- **类型守卫**:运行时检查类型
|
||||||
|
- **工具类型**:Partial、Required、Pick、Omit 等
|
||||||
|
|
||||||
|
::: info 💡 遇到问题时这样跟 AI 说
|
||||||
|
- "这个函数的类型注解应该怎么写?参数是 X,返回值是 Y"
|
||||||
|
- "帮我定义一个接口,描述这个数据结构:..."
|
||||||
|
- "这个 TypeScript 错误是什么意思?怎么修复?"
|
||||||
|
- "如何给这个泛型函数添加约束,确保 T 必须有某个属性?"
|
||||||
|
:::
|
||||||
|
|||||||
Reference in New Issue
Block a user