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

refactor(demos): improve variable box, scope, and type annotation demos
style(demos): update visual styles and animations for better UX
docs(browser-as-os): restructure content with tables and practical examples
feat(demos): add new TypeScript and runtime environment demos
This commit is contained in:
sanbuphy
2026-02-17 01:39:59 +08:00
parent 9ee3312569
commit 47377646df
20 changed files with 8097 additions and 1310 deletions
@@ -76,15 +76,19 @@
<div class="code-line">obj2.x = 20</div>
<div class="code-line result">// obj1.x = 20 (变了!)</div>
</div>
<div class="visual-box">
<div class="ref-box">
<div>obj1 </div>
<div class="memory-box">{x: 20}</div>
</div>
<div class="arrow">指向同一位置</div>
<div class="ref-box">
<div>obj2 </div>
<div class="visual-box ref-visual">
<div class="ref-boxes">
<div class="ref-var-box">
<div class="ref-var-name">obj1</div>
<div class="ref-var-arrow"></div>
</div>
<div class="ref-var-box">
<div class="ref-var-name">obj2</div>
<div class="ref-var-arrow"></div>
</div>
</div>
<div class="arrow down-arrow">指向同一位置</div>
<div class="memory-box">{x: 20}</div>
</div>
</div>
</div>
@@ -469,6 +473,40 @@ const convertType = () => {
text-align: center;
}
/* 修复引用类型可视化 */
.ref-visual {
flex-direction: column;
gap: 0.75rem;
}
.ref-boxes {
display: flex;
gap: 2rem;
justify-content: center;
}
.ref-var-box {
display: flex;
align-items: center;
gap: 0.5rem;
}
.ref-var-name {
font-weight: 600;
color: var(--vp-c-text-1);
font-family: monospace;
}
.ref-var-arrow {
color: var(--vp-c-brand);
font-weight: bold;
}
.down-arrow {
color: var(--vp-c-brand);
font-size: 0.8rem;
}
.ref-types-list {
display: flex;
flex-direction: column;
@@ -1,140 +1,106 @@
<script setup>
import { ref } from 'vue'
// 基本类型
const basicStep = ref(0)
const basicA = ref(10)
const basicB = ref(null)
const basicStep = ref(0)
const basicMessage = ref('')
// 引用类型
const obj1Data = ref({ name: '张三', age: 25 })
const obj2Exists = ref(false)
const obj2Age = ref('')
const refMessage = ref('')
const refStep = ref(0)
const objData = ref({ age: 25 })
const basicCopy = () => {
basicB.value = basicA.value
basicStep.value = 1
basicMessage.value = '✅ 基本类型复制的是值本身'
}
const basicCopy = () => { basicB.value = basicA.value; basicStep.value = 1 }
const basicModify = () => { basicB.value = 20; basicStep.value = 2 }
const basicReset = () => { basicStep.value = 0; basicB.value = null }
const basicModify = () => {
if (basicB.value === null) {
basicMessage.value = '⚠️ 请先复制'
return
}
basicB.value = 20
basicMessage.value = '✅ 修改 b 不影响 a'
}
const refCopy = () => {
obj2Exists.value = true
obj2Age.value = obj1Data.value.age
refMessage.value = '⚠️ 两个变量指向同一份数据'
}
const refModify = () => {
if (!obj2Exists.value) {
refMessage.value = '⚠️ 请先复制'
return
}
obj1Data.value.age = 30
obj2Age.value = 30
refMessage.value = '❌ 两个变量指向同一份数据,改了一个另一个也变了!'
}
const refSpreadCopy = () => {
obj2Exists.value = true
obj2Age.value = 25
refMessage.value = '✅ 用展开运算符创建真正的副本,现在互不影响'
}
const refCopy = () => { refStep.value = 1 }
const refModify = () => { objData.value.age = 30; refStep.value = 2 }
const refSpread = () => { refStep.value = 3 }
const refReset = () => { refStep.value = 0; objData.value.age = 25 }
</script>
<template>
<div class="reference-demo">
<h3> vs 引用</h3>
<div class="demo-container">
<div class="demo-title">🔄 vs 引用</div>
<div class="compare-grid">
<!-- 左侧基本类型 -->
<div class="demo-section basic-section">
<h4>基本类型复制值</h4>
<div class="visualization">
<div class="box" :class="{ 'active': basicA !== null }">
<div class="box-label">a</div>
<div class="box-value">{{ basicA }}</div>
</div>
<div class="arrow" v-if="basicStep >= 1">
<div class="arrow-line"></div>
<div class="arrow-head"></div>
</div>
<div class="box" :class="{ 'active': basicB !== null }" v-if="basicStep >= 1">
<div class="box-label">b</div>
<div class="box-value">{{ basicB }}</div>
<div class="compare-box">
<div class="box-header blue">基本类型复制值</div>
<div class="memory-area">
<div class="vars-row">
<div class="var-item" :class="{ active: basicStep >= 0 }">
<span class="var-label">a</span>
<span class="var-val">{{ basicA }}</span>
</div>
<div class="var-item" :class="{ active: basicStep >= 1, changed: basicStep >= 2 }">
<span class="var-label">b</span>
<span class="var-val">{{ basicB ?? '?' }}</span>
</div>
</div>
<div class="copy-arrow" v-if="basicStep >= 1"> 复制值</div>
</div>
<div class="message" v-if="basicMessage">{{ basicMessage }}</div>
<div class="controls">
<button @click="basicCopy" class="btn-primary">let b = a复制</button>
<button @click="basicModify" class="btn-secondary">b = 20</button>
<div class="result-text" :class="basicStep === 2 ? 'success' : 'info'">
{{ basicStep === 0 ? '点击复制' : basicStep === 1 ? 'b 得到 10' : '✅ 修改 b 不影响 a' }}
</div>
<div class="btn-group">
<button @click="basicCopy" :disabled="basicStep >= 1">复制</button>
<button @click="basicModify" :disabled="basicStep !== 1"> b</button>
<button @click="basicReset" class="reset">重置</button>
</div>
</div>
<!-- 右侧引用类型 -->
<div class="demo-section reference-section">
<h4>引用类型复制地址</h4>
<div class="visualization">
<!-- 数据区 -->
<div class="data-area">
<div class="data-label">数据区</div>
<div class="data-content">
<div>{{ `{ name: "${obj1Data.name}", age: ${obj1Data.age} }` }}</div>
<div class="compare-box">
<div class="box-header orange">引用类型复制地址</div>
<div class="memory-area">
<div class="vars-row">
<div class="var-item" :class="{ active: refStep >= 0 }">
<span class="var-label">obj1</span>
<span class="var-addr">0x001</span>
</div>
<div class="var-item" :class="{ active: refStep >= 1 }">
<span class="var-label">obj2</span>
<span class="var-addr">{{ refStep >= 1 ? '0x001' : '?' }}</span>
</div>
</div>
<div class="pointers">
<div class="pointer">
<div class="pointer-label">obj1</div>
<div class="arrow-line-down"></div>
</div>
<div class="pointer" v-if="obj2Exists">
<div class="pointer-label">obj2</div>
<div class="arrow-line-down"></div>
</div>
<div class="data-box" :class="{ changed: refStep === 2, copied: refStep === 3 }">
<div class="data-addr">0x001</div>
<div class="data-content">{ age: {{ objData.age }} }</div>
</div>
<div class="copy-arrow" v-if="refStep >= 1">指向同一地址</div>
</div>
<div class="message" v-if="refMessage">{{ refMessage }}</div>
<div class="controls">
<button @click="refCopy" class="btn-primary">let obj2 = obj1复制</button>
<button @click="refModify" class="btn-danger">obj2.age = 30</button>
<button @click="refSpreadCopy" class="btn-success">用展开运算符创建副本</button>
<div class="result-text" :class="refStep === 2 ? 'warning' : refStep === 3 ? 'success' : 'info'">
{{ refStep === 0 ? '点击复制' : refStep === 1 ? '共享地址' : refStep === 2 ? '⚠️ 一改全变' : '✅ 已分离' }}
</div>
<div class="btn-group">
<button @click="refCopy" :disabled="refStep >= 1">复制</button>
<button @click="refModify" :disabled="refStep !== 1">修改</button>
<button @click="refSpread" :disabled="refStep !== 2">展开</button>
<button @click="refReset" class="reset">重置</button>
</div>
</div>
</div>
<div class="code-display">
<h4>代码</h4>
<pre><code>// 基本类型
let a = {{ basicA }}
{{ basicB !== null ? `let b = ${basicB}` : '' }}
{{ basicB !== null ? `b = ${basicB}` : '' }}
console.log(a) // {{ basicA }}
// 引用类型
let obj1 = { name: "{{ obj1Data.name }}", age: {{ obj1Data.age }} }
{{ obj2Exists ? 'let obj2 = obj1 // 指向同一份数据' : '' }}
{{ obj2Exists ? `obj2.age = ${obj1Data.age}` : '' }}
console.log(obj1.age) // {{ obj1Data.age }}
</code></pre>
<div class="code-compare">
<div class="code-col">
<div class="code-title">基本类型</div>
<pre><code>let a = 10
let b = a // b=10
b = 20 // a还是10</code></pre>
</div>
<div class="code-col">
<div class="code-title">引用类型</div>
<pre><code>let obj1 = {age:25}
let obj2 = obj1
obj2.age=30 // obj1也变了!
// 用 {...obj1} 复制</code></pre>
</div>
</div>
</div>
</template>
@@ -143,235 +109,221 @@ console.log(obj1.age) // {{ obj1Data.age }}
.reference-demo {
border: 1px solid var(--vp-c-border);
border-radius: 12px;
padding: 24px;
margin: 24px 0;
padding: 20px;
margin: 16px 0;
background: var(--vp-c-bg);
}
h3 {
margin: 0 0 20px 0;
font-size: 18px;
.demo-title {
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 16px;
}
h4 {
margin: 0 0 16px 0;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
}
.demo-container {
.compare-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-bottom: 24px;
gap: 16px;
margin-bottom: 16px;
}
@media (max-width: 768px) {
.demo-container {
.compare-grid {
grid-template-columns: 1fr;
}
}
.demo-section {
border: 1px dashed var(--vp-c-border);
border-radius: 8px;
padding: 20px;
.compare-box {
border: 1px solid var(--vp-c-border);
border-radius: 10px;
padding: 16px;
background: var(--vp-c-bg-soft);
}
.visualization {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
margin-bottom: 16px;
min-height: 120px;
.box-header {
font-size: 13px;
font-weight: 600;
padding: 6px 10px;
border-radius: 6px;
margin-bottom: 12px;
color: white;
}
.box {
width: 80px;
height: 80px;
border: 2px solid var(--vp-c-border);
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.box-header.blue {
background: #3b82f6;
}
.box-header.orange {
background: #f59e0b;
}
.memory-area {
background: var(--vp-c-bg);
transition: all 0.3s ease;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.box.active {
border-color: var(--vp-c-brand-1);
box-shadow: 0 0 0 3px rgba(62, 175, 124, 0.1);
}
.box-label {
font-size: 12px;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 4px;
}
.box-value {
font-size: 24px;
font-weight: 600;
font-family: 'Courier New', monospace;
color: var(--vp-c-brand-1);
}
.arrow {
.vars-row {
display: flex;
align-items: center;
gap: 4px;
}
.arrow-line {
width: 40px;
height: 2px;
background: var(--vp-c-brand-1);
}
.arrow-head {
font-size: 24px;
color: var(--vp-c-brand-1);
}
.data-area {
text-align: center;
}
.data-label {
font-size: 12px;
font-weight: 600;
color: var(--vp-c-text-2);
gap: 12px;
justify-content: center;
margin-bottom: 8px;
}
.data-content {
border: 2px solid var(--vp-c-brand-1);
border-radius: 8px;
padding: 12px;
background: var(--vp-c-bg);
font-family: 'Courier New', monospace;
font-size: 12px;
color: var(--vp-c-text-1);
}
.pointers {
.var-item {
display: flex;
gap: 24px;
margin-top: 12px;
flex-direction: column;
align-items: center;
padding: 8px 16px;
border: 2px solid var(--vp-c-border);
border-radius: 6px;
opacity: 0.4;
transition: all 0.3s;
}
.pointer {
text-align: center;
.var-item.active {
opacity: 1;
border-color: #3b82f6;
}
.pointer-label {
font-size: 12px;
font-weight: 600;
.var-item.changed {
border-color: #10b981;
background: #ecfdf5;
}
.var-label {
font-size: 11px;
color: var(--vp-c-text-2);
margin-bottom: 4px;
}
.arrow-line-down {
width: 2px;
height: 30px;
background: var(--vp-c-brand-1);
margin: 0 auto;
.var-val, .var-addr {
font-size: 18px;
font-weight: 600;
font-family: monospace;
color: #3b82f6;
}
.message {
.var-addr {
color: #8b5cf6;
font-size: 14px;
}
.copy-arrow {
text-align: center;
font-size: 11px;
color: var(--vp-c-text-2);
padding: 4px;
}
.data-box {
border: 2px solid #8b5cf6;
border-radius: 6px;
padding: 8px;
text-align: center;
background: #f3e8ff;
margin-top: 8px;
}
.data-box.changed {
border-color: #ef4444;
background: #fee2e2;
}
.data-box.copied {
border-color: #10b981;
background: #d1fae5;
}
.data-addr {
font-size: 10px;
color: #6b7280;
}
.data-content {
font-family: monospace;
font-size: 13px;
color: #374151;
}
.result-text {
text-align: center;
padding: 8px;
border-radius: 6px;
font-size: 12px;
margin-bottom: 12px;
font-size: 13px;
font-weight: 500;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.controls {
.result-text.info {
background: #f3f4f6;
color: #4b5563;
}
.result-text.success {
background: #d1fae5;
color: #065f46;
}
.result-text.warning {
background: #fee2e2;
color: #991b1b;
}
.btn-group {
display: flex;
gap: 8px;
gap: 6px;
flex-wrap: wrap;
justify-content: center;
}
button {
.btn-group button {
padding: 6px 12px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
}
button:active {
transform: scale(0.95);
}
.btn-primary {
background: var(--vp-c-brand-1);
background: #3b82f6;
color: white;
}
.btn-primary:hover {
background: var(--vp-c-brand-2);
.btn-group button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.btn-secondary {
background: var(--vp-c-bg-soft);
.btn-group button.reset {
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-border);
}
.btn-secondary:hover {
background: var(--vp-c-bg-soft-hover);
.code-compare {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.btn-danger {
background: #f56565;
color: white;
}
.btn-danger:hover {
background: #e53e3e;
}
.btn-success {
background: #38a169;
color: white;
}
.btn-success:hover {
background: #2f855a;
}
.code-display {
.code-col {
background: #1e1e1e;
border-radius: 8px;
padding: 16px;
overflow-x: auto;
padding: 12px;
}
.code-display h4 {
color: #d4d4d4;
margin-bottom: 12px;
.code-title {
color: #9ca3af;
font-size: 11px;
margin-bottom: 8px;
}
.code-display pre {
.code-col pre {
margin: 0;
}
.code-display code {
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
.code-col code {
font-family: monospace;
font-size: 11px;
line-height: 1.5;
color: #d4d4d4;
}
</style>
@@ -1,7 +1,7 @@
<script setup>
import { ref } from 'vue'
const activeScope = ref('block') // 'global', 'function', 'block'
const activeScope = ref('global')
const explanation = ref('')
const scopes = [
@@ -9,31 +9,36 @@ const scopes = [
id: 'global',
name: '全局作用域',
color: '#a0aec0',
variables: ['appName = "Todo"'],
canSee: ['appName']
vars: [{ name: 'appName', value: '"Todo"', own: true }]
},
{
id: 'function',
name: '函数 greet() 作用域',
color: '#4299e1',
variables: ['message = "你好"'],
canSee: ['appName', 'message']
vars: [
{ name: 'appName', value: '"Todo"', own: false, from: '全局' },
{ name: 'message', value: '"你好"', own: true }
]
},
{
id: 'block',
name: 'if 块作用域',
color: '#38a169',
variables: ['greeting = message + appName'],
canSee: ['appName', 'message', 'greeting']
vars: [
{ name: 'appName', value: '"Todo"', own: false, from: '全局' },
{ name: 'message', value: '"你好"', own: false, from: '函数' },
{ name: 'greeting', value: 'message+appName', own: true }
]
}
]
const updateExplanation = () => {
const scope = scopes.find(s => s.id === activeScope.value)
if (scope) {
const visible = scope.canSee.map(v => `${v}`).join('、')
explanation.value = `在这个位置,你能使用这些变量:${visible}`
const texts = {
global: '在全局作用域,只能使用全局变量 appName',
function: '在函数作用域,可以使用自己的 message 和全局的 appName(作用域链查找)',
block: '在块级作用域,可以使用自己的 greeting,以及外层的 message 和 appName'
}
explanation.value = texts[activeScope.value]
}
updateExplanation()
@@ -41,67 +46,54 @@ updateExplanation()
<template>
<div class="scope-demo">
<h3>作用域变量的"可见范围"</h3>
<h3>🔍 作用域变量的"可见范围"</h3>
<div class="scopes-container">
<!-- 全局作用域 -->
<div
class="scope global-scope"
:class="{ 'active': activeScope === 'global' }"
@click="activeScope = 'global'; updateExplanation()"
<div class="scope-selector">
<button
v-for="scope in scopes"
:key="scope.id"
@click="activeScope = scope.id; updateExplanation()"
class="scope-btn"
:class="{ active: activeScope === scope.id }"
:style="{ borderColor: scope.color }"
>
<div class="scope-header">全局作用域</div>
<div class="scope-content">
<div class="variable" :class="{ 'visible': activeScope === 'global', 'dimmed': activeScope !== 'global' }">
appName = "Todo"
{{ scope.name }}
</button>
</div>
<div class="scope-visual">
<!-- 作用域层级图 -->
<div class="scope-levels">
<div
v-for="(scope, index) in scopes"
:key="scope.id"
class="level"
:class="{ active: activeScope === scope.id, dimmed: activeScope !== scope.id }"
:style="{ borderLeftColor: scope.color }"
>
<div class="level-header" :style="{ color: scope.color }">
{{ scope.name }}
</div>
<!-- 函数作用域 -->
<div class="nested-scope">
<div class="level-vars">
<div
class="scope function-scope"
:class="{ 'active': activeScope === 'function' }"
@click.stop="activeScope = 'function'; updateExplanation()"
v-for="v in scope.vars"
:key="v.name"
class="var-tag"
:class="{ own: v.own, inherited: !v.own }"
>
<div class="scope-header">函数 greet() 作用域</div>
<div class="scope-content">
<div class="variable" :class="{ 'visible': ['global', 'function'].includes(activeScope), 'dimmed': !['global', 'function'].includes(activeScope) }">
appName = "Todo"
</div>
<div class="variable" :class="{ 'visible': activeScope === 'function', 'dimmed': activeScope !== 'function' }">
message = "你好"
</div>
<!-- 块级作用域 -->
<div class="nested-scope">
<div
class="scope block-scope"
:class="{ 'active': activeScope === 'block' }"
@click.stop="activeScope = 'block'; updateExplanation()"
>
<div class="scope-header">if 块作用域</div>
<div class="scope-content">
<div class="variable" :class="{ 'visible': true, 'dimmed': false }">
appName = "Todo"
</div>
<div class="variable" :class="{ 'visible': true, 'dimmed': false }">
message = "你好"
</div>
<div class="variable" :class="{ 'visible': activeScope === 'block', 'dimmed': activeScope !== 'block' }">
greeting = message + appName
</div>
</div>
</div>
</div>
</div>
<span class="var-name">{{ v.name }}</span>
<span class="var-value">= {{ v.value }}</span>
<span v-if="!v.own" class="var-from"> {{ v.from }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="explanation" v-if="explanation">
{{ explanation }}
<!-- 说明 -->
<div class="explanation-box">
<div class="explanation-title">💡 当前位置可见的变量</div>
<div class="explanation-text">{{ explanation }}</div>
</div>
</div>
<div class="code-display">
@@ -112,7 +104,7 @@ function greet() {
const message = "你好" // 函数作用域
if (true) {
const greeting = message + appName // 块级作用域 ✅ 能看到外层的
const greeting = message + appName // 块级作用域
console.log(greeting)
}
@@ -138,112 +130,133 @@ h3 {
color: var(--vp-c-text-1);
}
.scopes-container {
.scope-selector {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.scope {
border: 3px solid var(--vp-c-border);
border-radius: 12px;
padding: 20px;
.scope-btn {
padding: 10px 16px;
border: 2px solid var(--vp-c-border);
border-radius: 8px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
transition: all 0.2s ease;
}
.scope-btn:hover {
background: var(--vp-c-bg-soft);
}
.scope:hover {
border-color: var(--vp-c-brand-1);
transform: scale(1.02);
.scope-btn.active {
background: var(--vp-c-brand-soft);
border-color: var(--vp-c-brand);
}
.scope.active {
border-width: 4px;
box-shadow: 0 0 0 4px rgba(62, 175, 124, 0.1);
.scope-visual {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.global-scope {
border-color: #a0aec0;
@media (max-width: 768px) {
.scope-visual {
grid-template-columns: 1fr;
}
}
.global-scope.active {
border-color: #a0aec0;
box-shadow: 0 0 0 4px rgba(160, 174, 192, 0.2);
}
.function-scope {
border-color: #4299e1;
}
.function-scope.active {
border-color: #4299e1;
box-shadow: 0 0 0 4px rgba(66, 153, 225, 0.2);
}
.block-scope {
border-color: #38a169;
}
.block-scope.active {
border-color: #38a169;
box-shadow: 0 0 0 4px rgba(56, 161, 105, 0.2);
}
.scope-header {
font-size: 12px;
font-weight: 600;
margin-bottom: 12px;
text-align: center;
}
.scope-content {
.scope-levels {
display: flex;
flex-direction: column;
gap: 12px;
}
.level {
border-left: 4px solid;
padding: 12px 16px;
background: var(--vp-c-bg-soft);
border-radius: 0 8px 8px 0;
transition: all 0.3s ease;
}
.level.active {
background: var(--vp-c-bg);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.level.dimmed {
opacity: 0.6;
}
.level-header {
font-weight: 600;
font-size: 14px;
margin-bottom: 8px;
}
.level-vars {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.nested-scope {
.var-tag {
display: flex;
justify-content: center;
margin-top: 12px;
}
.variable {
padding: 8px 12px;
border-radius: 6px;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 4px;
font-size: 13px;
font-family: 'Courier New', monospace;
font-size: 12px;
font-weight: 500;
background: var(--vp-c-bg);
transition: all 0.3s ease;
}
.variable.visible {
color: var(--vp-c-text-1);
.var-tag.own {
background: var(--vp-c-brand-soft);
border: 1px solid var(--vp-c-brand);
}
.var-tag.inherited {
background: var(--vp-c-bg-alt);
border: 1px dashed var(--vp-c-border);
}
.var-name {
font-weight: 600;
}
.variable.dimmed {
color: var(--vp-c-text-3);
opacity: 0.5;
}
.explanation {
text-align: center;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
font-weight: 500;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
.var-value {
color: var(--vp-c-text-2);
}
.var-from {
font-size: 11px;
color: var(--vp-c-text-3);
font-style: italic;
}
.explanation-box {
background: var(--vp-c-brand-soft);
border-radius: 8px;
padding: 16px;
}
.explanation-title {
font-weight: 600;
color: var(--vp-c-brand);
margin-bottom: 8px;
font-size: 14px;
}
.explanation-text {
color: var(--vp-c-text-1);
font-size: 14px;
line-height: 1.6;
}
.code-display {
@@ -270,15 +283,4 @@ h3 {
line-height: 1.6;
color: #d4d4d4;
}
@media (max-width: 768px) {
.scopes-container {
flex-direction: column;
align-items: center;
}
.scope {
min-width: 280px;
}
}
</style>
@@ -5,85 +5,80 @@ const name = ref('张三')
const age = ref(25)
const isStudent = ref(true)
const showMessage = ref('')
const showSuccess = ref(false)
const messageType = ref('')
let messageTimer = null
const clearMessage = () => {
showMessage.value = ''
messageType.value = ''
}
const setMessage = (msg, type) => {
if (messageTimer) clearTimeout(messageTimer)
showMessage.value = msg
messageType.value = type
messageTimer = setTimeout(() => clearMessage(), 2000)
}
const modifyAge = () => {
age.value = 26
showMessage.value = '✅ let 变量可以修改'
showSuccess.value = true
setTimeout(() => {
showMessage.value = ''
showSuccess.value = false
}, 2000)
setMessage('✅ let 可以修改', 'success')
}
const modifyName = () => {
showMessage.value = '❌ const 不能重新赋值'
showSuccess.value = false
setTimeout(() => {
showMessage.value = ''
}, 2000)
setMessage('❌ const 不能改', 'error')
}
const reset = () => {
name.value = '张三'
age.value = 25
isStudent.value = true
showMessage.value = ''
clearMessage()
}
const codeLines = ref([
`const name = "张三"`,
`let age = 25`,
`const isStudent = true`
])
const executeCode = ref([])
</script>
<template>
<div class="variable-box-demo">
<h3>变量就像带名字的盒子</h3>
<div class="demo-header">
<span class="title">📦 变量就像带名字的盒子</span>
</div>
<div class="boxes-container">
<!-- 盒子 1: const name -->
<div class="variable-box const-box">
<div class="box-label">const name</div>
<div class="boxes-row">
<div class="var-box" :class="{ error: messageType === 'error' }">
<div class="box-tag const">const</div>
<div class="box-name">name</div>
<div class="box-value">{{ name }}</div>
<div class="box-icon">🔒</div>
<div class="box-lock">🔒</div>
</div>
<!-- 盒子 2: let age -->
<div class="variable-box let-box" :class="{ 'success': showSuccess && age === 26 }">
<div class="box-label">let age</div>
<div class="var-box" :class="{ success: messageType === 'success' }">
<div class="box-tag let">let</div>
<div class="box-name">age</div>
<div class="box-value">{{ age }}</div>
<div class="box-icon">🔓</div>
<div class="box-lock">🔓</div>
</div>
<!-- 盒子 3: const isStudent -->
<div class="variable-box const-box">
<div class="box-label">const isStudent</div>
<div class="var-box">
<div class="box-tag const">const</div>
<div class="box-name">isStudent</div>
<div class="box-value">{{ isStudent }}</div>
<div class="box-icon">🔒</div>
<div class="box-lock">🔒</div>
</div>
</div>
<div class="message-bubble" :class="{ 'error': !showSuccess, 'success': showSuccess }" v-if="showMessage">
<div class="message" v-if="showMessage" :class="messageType">
{{ showMessage }}
</div>
<div class="controls">
<button @click="modifyAge" class="btn-primary">修改 age 26</button>
<button @click="modifyName" class="btn-danger">修改 name 为李四</button>
<button @click="reset" class="btn-secondary">重置</button>
<button @click="modifyAge" class="btn btn-primary">修改 age</button>
<button @click="modifyName" class="btn btn-danger">修改 name</button>
<button @click="reset" class="btn btn-secondary">重置</button>
</div>
<div class="code-display">
<pre><code>const name = "张三"
let age = 25
const isStudent = true
{{ executeCode.join('\n') }}</code></pre>
<div class="code-snippet">
<code>const name = "{{ name }}"</code>
<code>let age = {{ age }}</code>
</div>
</div>
</template>
@@ -92,42 +87,59 @@ const isStudent = true
.variable-box-demo {
border: 1px solid var(--vp-c-border);
border-radius: 12px;
padding: 24px;
margin: 24px 0;
padding: 20px;
margin: 16px 0;
background: var(--vp-c-bg);
}
h3 {
margin: 0 0 16px 0;
font-size: 18px;
.demo-header {
margin-bottom: 16px;
}
.title {
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.boxes-container {
.boxes-row {
display: flex;
gap: 16px;
justify-content: center;
margin-bottom: 20px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.variable-box {
position: relative;
width: 120px;
height: 120px;
.var-box {
width: 100px;
height: 100px;
border: 2px solid var(--vp-c-border);
border-radius: 12px;
border-radius: 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
background: var(--vp-c-bg);
transition: all 0.3s ease;
}
.variable-box.success {
border-color: #3eaf7c;
animation: pulse 0.5s ease;
.var-box.error {
border-color: #ef4444;
background: #fef2f2;
animation: shake 0.4s ease;
}
.var-box.success {
border-color: #10b981;
background: #ecfdf5;
animation: pulse 0.4s ease;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-4px); }
75% { transform: translateX(4px); }
}
@keyframes pulse {
@@ -135,144 +147,107 @@ h3 {
50% { transform: scale(1.05); }
}
.box-label {
.box-tag {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: var(--vp-c-brand-1);
top: -10px;
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 600;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.let-box .box-label {
background: #42b983;
.box-tag.const {
background: #3b82f6;
}
.box-tag.let {
background: #10b981;
}
.box-name {
font-size: 13px;
color: var(--vp-c-text-2);
margin-bottom: 4px;
}
.box-value {
font-size: 24px;
font-size: 20px;
font-weight: 600;
font-family: 'Courier New', monospace;
font-family: monospace;
color: var(--vp-c-text-1);
margin-bottom: 8px;
}
.box-icon {
font-size: 16px;
.box-lock {
position: absolute;
bottom: 8px;
font-size: 12px;
}
.message-bubble {
.message {
text-align: center;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
padding: 10px;
border-radius: 6px;
margin-bottom: 12px;
font-size: 13px;
font-weight: 500;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
.message.error {
background: #fef2f2;
color: #dc2626;
}
.message-bubble.error {
background: #fee;
color: #c00;
}
.message-bubble.success {
background: #e8f5e9;
color: #2e7d32;
.message.success {
background: #ecfdf5;
color: #059669;
}
.controls {
display: flex;
gap: 12px;
gap: 8px;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 20px;
margin-bottom: 12px;
}
button {
padding: 8px 16px;
.btn {
padding: 8px 14px;
border: none;
border-radius: 6px;
font-size: 14px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
button:active {
transform: scale(0.95);
transition: all 0.2s;
}
.btn-primary {
background: var(--vp-c-brand-1);
background: #3b82f6;
color: white;
}
.btn-primary:hover {
background: var(--vp-c-brand-2);
}
.btn-danger {
background: #f56565;
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #e53e3e;
}
.btn-secondary {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-border);
}
.btn-secondary:hover {
background: var(--vp-c-bg-soft-hover);
}
.code-display {
.code-snippet {
background: #1e1e1e;
border-radius: 8px;
padding: 16px;
overflow-x: auto;
border-radius: 6px;
padding: 10px 14px;
display: flex;
flex-direction: column;
gap: 4px;
}
.code-display pre {
margin: 0;
}
.code-display code {
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
.code-snippet code {
font-family: monospace;
font-size: 12px;
color: #d4d4d4;
}
@media (max-width: 640px) {
.boxes-container {
flex-direction: column;
align-items: center;
}
.variable-box {
width: 200px;
}
.controls {
flex-direction: column;
}
button {
width: 100%;
}
}
</style>
@@ -0,0 +1,542 @@
<script setup>
import { ref } from 'vue'
const isAnimating = ref(false)
const currentStep = ref(0)
const callStack = ref([])
const output = ref([])
const codeSteps = [
{ action: 'push', function: 'main', description: '调用 main()', code: 'main()' },
{ action: 'push', function: 'a', description: 'main() 调用 a()', code: 'function a() {' },
{ action: 'push', function: 'b', description: 'a() 调用 b()', code: 'function b() {' },
{ action: 'push', function: 'c', description: 'b() 调用 c()', code: 'function c() {' },
{ action: 'log', function: 'c', description: 'c() 执行 console.log', code: 'console.log("执行完毕")', output: '执行完毕' },
{ action: 'pop', function: 'c', description: 'c() 执行完成,从栈中弹出', code: '}' },
{ action: 'pop', function: 'b', description: 'b() 执行完成,从栈中弹出', code: '}' },
{ action: 'pop', function: 'a', description: 'a() 执行完成,从栈中弹出', code: '}' },
{ action: 'pop', function: 'main', description: 'main() 执行完成,从栈中弹出', code: '}' }
]
const reset = () => {
currentStep.value = 0
callStack.value = []
output.value = []
isAnimating.value = false
}
const nextStep = () => {
if (currentStep.value >= codeSteps.length) return
const step = codeSteps[currentStep.value]
if (step.action === 'push') {
callStack.value.push({
function: step.function,
code: step.code,
active: true
})
// 标记之前的为非活动
callStack.value.forEach((item, index) => {
if (index < callStack.value.length - 1) {
item.active = false
}
})
} else if (step.action === 'pop') {
callStack.value.pop()
// 标记新的顶部为活动
if (callStack.value.length > 0) {
callStack.value[callStack.value.length - 1].active = true
}
} else if (step.action === 'log') {
output.value.push(step.output)
}
currentStep.value++
}
const play = async () => {
if (isAnimating.value) return
isAnimating.value = true
reset()
while (currentStep.value < codeSteps.length && isAnimating.value) {
nextStep()
await new Promise(resolve => setTimeout(resolve, 1200))
}
isAnimating.value = false
}
const stop = () => {
isAnimating.value = false
}
</script>
<template>
<div class="call-stack-demo">
<h3>调用栈:函数执行的足迹</h3>
<div class="demo-layout">
<!-- 代码显示 -->
<div class="code-section">
<h4>代码</h4>
<div class="code-display">
<div
v-for="(step, index) in codeSteps"
:key="index"
class="code-line"
:class="{
'current': currentStep === index,
'executed': currentStep > index
}"
>
<span class="line-number">{{ index + 1 }}</span>
<span class="line-code">{{ step.code }}</span>
</div>
</div>
</div>
<!-- 调用栈可视化 -->
<div class="stack-section">
<h4>调用栈</h4>
<div class="stack-container">
<div class="stack-base">
<div class="stack-label">栈底</div>
</div>
<div class="stack-frames">
<transition-group name="stack-frame">
<div
v-for="(frame, index) in callStack"
:key="`${frame.function}-${index}`"
class="stack-frame"
:class="{ 'active': frame.active }"
:style="{ bottom: `${index * 60}px` }"
>
<div class="frame-function">{{ frame.function }}()</div>
<div class="frame-code">{{ frame.code }}</div>
</div>
</transition-group>
<div v-if="callStack.length === 0" class="empty-stack">
栈为空
</div>
</div>
<div class="stack-top">
<div class="stack-label">栈顶</div>
</div>
</div>
<div class="stack-explanation">
<p><strong>当前状态:</strong></p>
<p v-if="currentStep < codeSteps.length">
{{ codeSteps[currentStep]?.description }}
</p>
<p v-else>
执行完成
</p>
</div>
</div>
</div>
<!-- 输出显示 -->
<div class="output-section">
<h4>输出</h4>
<div class="output-container">
<div v-if="output.length === 0" class="empty-output">
等待输出...
</div>
<transition-group name="output">
<div
v-for="(log, index) in output"
:key="`log-${index}`"
class="output-line"
>
{{ log }}
</div>
</transition-group>
</div>
</div>
<!-- 控制按钮 -->
<div class="controls">
<button @click="play" :disabled="isAnimating" class="btn-play">
{{ isAnimating ? '执行中...' : '▶ 自动演示' }}
</button>
<button @click="nextStep" :disabled="isAnimating || currentStep >= codeSteps.length" class="btn-step">
单步执行
</button>
<button @click="stop" :disabled="!isAnimating" class="btn-stop">
停止
</button>
<button @click="reset" :disabled="isAnimating" class="btn-reset">
🔄 重置
</button>
</div>
<!-- 说明 -->
<div class="explanation-box">
<p><strong>调用栈工作原理:</strong></p>
<ul>
<li>每次调用函数,就会在栈上"压入"一个新的"栈帧"</li>
<li>栈帧记录了函数的执行状态局部变量等信息</li>
<li>函数执行完毕,栈帧就会从栈上"弹出"</li>
<li>栈是"后进先出"(LIFO)的数据结构</li>
<li>如果递归太深,会导致"栈溢出"错误</li>
</ul>
<p class="highlight">
调用栈就像一摞盘子:最后放上去的盘子最先被取走每个函数就是一个盘子,执行完就取走,然后继续执行下面的函数
</p>
</div>
</div>
</template>
<style scoped>
.call-stack-demo {
border: 1px solid var(--vp-c-border);
border-radius: 12px;
padding: 24px;
margin: 24px 0;
background: var(--vp-c-bg);
}
h3 {
margin: 0 0 20px 0;
font-size: 18px;
font-weight: 600;
color: var(--vp-c-text-1);
}
h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
}
.demo-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-bottom: 20px;
}
@media (max-width: 1024px) {
.demo-layout {
grid-template-columns: 1fr;
}
}
.code-section,
.stack-section {
border: 2px solid var(--vp-c-border);
border-radius: 8px;
padding: 16px;
background: var(--vp-c-bg-soft);
}
.code-display {
background: #1e1e1e;
border-radius: 6px;
padding: 12px;
font-family: 'Courier New', monospace;
}
.code-line {
display: flex;
gap: 12px;
padding: 6px 8px;
border-radius: 4px;
transition: all 0.3s ease;
}
.code-line.current {
background: rgba(62, 175, 124, 0.2);
border-left: 3px solid var(--vp-c-brand-1);
}
.code-line.executed {
opacity: 0.5;
}
.line-number {
color: #858585;
font-size: 12px;
min-width: 20px;
text-align: right;
user-select: none;
}
.line-code {
color: #d4d4d4;
font-size: 13px;
}
.stack-container {
position: relative;
height: 350px;
background: var(--vp-c-bg);
border-radius: 8px;
padding: 12px;
margin-top: 12px;
}
.stack-base,
.stack-top {
display: flex;
justify-content: center;
padding: 8px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
margin-bottom: 8px;
}
.stack-top {
margin-top: 8px;
margin-bottom: 0;
}
.stack-label {
font-size: 12px;
font-weight: 600;
color: var(--vp-c-text-2);
}
.stack-frames {
position: relative;
flex: 1;
}
.stack-frame {
position: absolute;
left: 12px;
right: 12px;
padding: 12px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-border);
border-radius: 6px;
transition: all 0.4s ease;
}
.stack-frame.active {
border-color: var(--vp-c-brand-1);
background: rgba(62, 175, 124, 0.1);
box-shadow: 0 0 0 3px rgba(62, 175, 124, 0.1);
}
.stack-frame-enter-active,
.stack-frame-leave-active {
transition: all 0.4s ease;
}
.stack-frame-enter-from {
opacity: 0;
transform: translateY(-20px);
}
.stack-frame-leave-to {
opacity: 0;
transform: translateY(20px);
}
.frame-function {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-brand-1);
margin-bottom: 4px;
font-family: 'Courier New', monospace;
}
.frame-code {
font-size: 11px;
color: var(--vp-c-text-2);
font-family: 'Courier New', monospace;
}
.empty-stack {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--vp-c-text-3);
font-size: 14px;
}
.stack-explanation {
margin-top: 12px;
padding: 12px;
background: rgba(62, 175, 124, 0.1);
border-radius: 6px;
}
.stack-explanation p {
margin: 0;
font-size: 13px;
line-height: 1.6;
color: var(--vp-c-text-1);
}
.stack-explanation strong {
color: var(--vp-c-brand-1);
}
.output-section {
margin-bottom: 20px;
}
.output-container {
min-height: 60px;
padding: 12px;
border: 2px solid var(--vp-c-border);
border-radius: 8px;
background: var(--vp-c-bg);
}
.empty-output {
color: var(--vp-c-text-3);
font-size: 14px;
}
.output-line {
padding: 8px 12px;
margin-bottom: 8px;
background: var(--vp-c-brand-1);
color: white;
border-radius: 6px;
font-size: 14px;
font-family: 'Courier New', monospace;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.output-enter-active,
.output-leave-active {
transition: all 0.3s ease;
}
.output-enter-from {
opacity: 0;
transform: translateY(-10px);
}
.output-leave-to {
opacity: 0;
transform: translateY(10px);
}
.controls {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
button:active {
transform: scale(0.95);
}
.btn-play {
background: var(--vp-c-brand-1);
color: white;
}
.btn-play:hover:not(:disabled) {
background: var(--vp-c-brand-2);
}
.btn-step {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.btn-step:hover:not(:disabled) {
background: var(--vp-c-bg-soft-hover);
}
.btn-stop {
background: #ed8936;
color: white;
}
.btn-stop:hover:not(:disabled) {
background: #dd6b20;
}
.btn-reset {
background: #f56565;
color: white;
}
.btn-reset:hover:not(:disabled) {
background: #e53e3e;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.explanation-box {
background: var(--vp-c-bg-soft);
border-left: 4px solid var(--vp-c-brand-1);
border-radius: 8px;
padding: 16px;
}
.explanation-box p {
margin: 0 0 12px 0;
font-size: 14px;
line-height: 1.6;
color: var(--vp-c-text-1);
}
.explanation-box p:last-child {
margin-bottom: 0;
}
.explanation-box strong {
color: var(--vp-c-brand-1);
}
.explanation-box ul {
margin: 12px 0;
padding-left: 20px;
}
.explanation-box li {
margin-bottom: 8px;
font-size: 14px;
color: var(--vp-c-text-2);
}
.explanation-box .highlight {
padding: 12px;
background: rgba(62, 175, 124, 0.1);
border-radius: 6px;
font-weight: 500;
color: var(--vp-c-brand-1);
}
</style>
@@ -0,0 +1,752 @@
<script setup>
import { ref } from 'vue'
const phase = ref('mark')
const isAnimating = ref(false)
const currentStep = ref(0)
const objects = ref([
{ id: 1, name: 'obj1', color: '#68d391', marked: false, collected: false },
{ id: 2, name: 'obj2', color: '#4299e1', marked: false, collected: false },
{ id: 3, name: 'obj3', color: '#ed8936', marked: false, collected: false },
{ id: 4, name: 'obj4', color: '#f687b3', marked: false, collected: false },
{ id: 5, name: 'obj5', color: '#a3bffa', marked: false, collected: false },
{ id: 6, name: 'obj6', color: '#fc8181', marked: false, collected: false }
])
const references = ref([
{ from: 'root', to: 1, active: false },
{ from: 1, to: 2, active: false },
{ from: 1, to: 3, active: false },
{ from: 3, to: 4, active: false }
])
const phases = [
{ name: 'mark', label: '标记阶段', description: '从根对象开始,标记所有可达对象' },
{ name: 'sweep', label: '清除阶段', description: '回收未标记的对象' }
]
const steps = [
{ phase: 'mark', action: 'mark-root', description: '从根对象开始标记' },
{ phase: 'mark', action: 'mark-1', description: '标记 obj1 (根对象引用)' },
{ phase: 'mark', action: 'mark-2', description: '标记 obj2 (obj1 引用)' },
{ phase: 'mark', action: 'mark-3', description: '标记 obj3 (obj1 引用)' },
{ phase: 'mark', action: 'mark-4', description: '标记 obj4 (obj3 引用)' },
{ phase: 'sweep', action: 'collect-5', description: '回收 obj5 (未标记)' },
{ phase: 'sweep', action: 'collect-6', description: '回收 obj6 (未标记)' },
{ phase: 'done', action: 'finish', description: '垃圾回收完成' }
]
const reset = () => {
currentStep.value = 0
phase.value = 'mark'
isAnimating.value = false
objects.value.forEach(obj => {
obj.marked = false
obj.collected = false
})
references.value.forEach(ref => {
ref.active = false
})
}
const nextStep = () => {
if (currentStep.value >= steps.length) return
const step = steps[currentStep.value]
switch (step.action) {
case 'mark-root':
references.value[0].active = true
break
case 'mark-1':
objects.value[0].marked = true
references.value[1].active = true
references.value[2].active = true
break
case 'mark-2':
objects.value[1].marked = true
break
case 'mark-3':
objects.value[2].marked = true
references.value[3].active = true
break
case 'mark-4':
objects.value[3].marked = true
phase.value = 'sweep'
break
case 'collect-5':
objects.value[4].collected = true
break
case 'collect-6':
objects.value[5].collected = true
phase.value = 'done'
break
case 'finish':
phase.value = 'done'
break
}
currentStep.value++
}
const play = async () => {
if (isAnimating.value) return
isAnimating.value = true
reset()
while (currentStep.value < steps.length && isAnimating.value) {
nextStep()
await new Promise(resolve => setTimeout(resolve, 1200))
}
isAnimating.value = false
}
const stop = () => {
isAnimating.value = false
}
</script>
<template>
<div class="garbage-collection-demo">
<h3>垃圾回收机制</h3>
<!-- 阶段指示器 -->
<div class="phase-indicator">
<div class="phase-tabs">
<div
v-for="p in phases"
:key="p.name"
:class="{ 'active': phase === p.name }"
class="phase-tab"
>
<span class="phase-label">{{ p.label }}</span>
<span class="phase-description">{{ p.description }}</span>
</div>
</div>
</div>
<!-- 对象关系图 -->
<div class="graph-container">
<div class="graph-header">
<h4>对象引用关系</h4>
<div class="legend">
<div class="legend-item">
<span class="legend-color unmarked"></span>
<span>未标记</span>
</div>
<div class="legend-item">
<span class="legend-color marked"></span>
<span>已标记(可达)</span>
</div>
<div class="legend-item">
<span class="legend-color collected"></span>
<span>已回收</span>
</div>
</div>
</div>
<div class="object-graph">
<!-- 根对象 -->
<div class="root-object">
<div class="object-box root">
<div class="object-icon">🌳</div>
<div class="object-name">Root</div>
</div>
</div>
<!-- 对象节点 -->
<div class="objects-grid">
<div
v-for="obj in objects"
:key="obj.id"
class="object-node"
:class="{
'marked': obj.marked,
'collected': obj.collected
}"
>
<div class="object-box" :style="{ borderColor: obj.color }">
<div class="object-icon" :style="{ background: obj.color }">
{{ obj.collected ? '💀' : '📦' }}
</div>
<div class="object-name">{{ obj.name }}</div>
<div v-if="obj.marked" class="object-status"> 可达</div>
<div v-if="obj.collected" class="object-status collected"> 回收</div>
</div>
</div>
</div>
<!-- 引用连线 (用SVG绘制) -->
<svg class="connections" viewBox="0 0 600 400">
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#a0aec0" />
</marker>
</defs>
<!-- Root -> obj1 -->
<line
x1="80"
y1="200"
x2="180"
y2="100"
:class="{ 'active': references[0].active }"
marker-end="url(#arrowhead)"
/>
<!-- obj1 -> obj2 -->
<line
x1="220"
y1="120"
x2="220"
y2="180"
:class="{ 'active': references[1].active }"
marker-end="url(#arrowhead)"
/>
<!-- obj1 -> obj3 -->
<line
x1="260"
y1="120"
x2="380"
y2="120"
:class="{ 'active': references[2].active }"
marker-end="url(#arrowhead)"
/>
<!-- obj3 -> obj4 -->
<line
x1="400"
y1="140"
x2="400"
y2="200"
:class="{ 'active': references[3].active }"
marker-end="url(#arrowhead)"
/>
</svg>
</div>
</div>
<!-- 当前步骤说明 -->
<div class="step-description">
<div class="step-content">
<strong>当前操作:</strong>
<span v-if="currentStep < steps.length">
{{ steps[currentStep].description }}
</span>
<span v-else>
垃圾回收完成
</span>
</div>
</div>
<!-- 控制按钮 -->
<div class="controls">
<button @click="play" :disabled="isAnimating" class="btn-play">
{{ isAnimating ? '执行中...' : '▶ 自动演示' }}
</button>
<button @click="nextStep" :disabled="isAnimating || currentStep >= steps.length" class="btn-step">
单步执行
</button>
<button @click="stop" :disabled="!isAnimating" class="btn-stop">
停止
</button>
<button @click="reset" :disabled="isAnimating" class="btn-reset">
🔄 重置
</button>
</div>
<!-- 算法说明 -->
<div class="algorithm-box">
<h4>标记-清除算法 (Mark-and-Sweep)</h4>
<div class="algorithm-steps">
<div class="algorithm-step">
<span class="step-number">1</span>
<div class="step-content">
<strong>标记阶段</strong>
<p>从根对象(Root)开始,遍历所有可达对象,标记为"活动对象"</p>
</div>
</div>
<div class="algorithm-step">
<span class="step-number">2</span>
<div class="step-content">
<strong>清除阶段</strong>
<p>遍历整个堆内存,回收所有未被标记的对象</p>
</div>
</div>
<div class="algorithm-step">
<span class="step-number">3</span>
<div class="step-content">
<strong>重置标记</strong>
<p>清除所有标记位,为下一次垃圾回收做准备</p>
</div>
</div>
</div>
<div class="key-points">
<h5>核心要点</h5>
<ul>
<li><strong>根对象(Root):</strong> 全局变量栈上的变量等,总是被认为是可达的</li>
<li><strong>可达对象:</strong> 从根对象出发,通过引用链能访问到的对象</li>
<li><strong>垃圾对象:</strong> 无法从根对象访问到的对象,会被回收</li>
<li><strong>循环引用:</strong> 如果两个对象互相引用但都不可达,仍会被回收</li>
</ul>
</div>
</div>
<!-- 实际应用 -->
<div class="practical-tips">
<h4>实际应用技巧</h4>
<div class="tips-grid">
<div class="tip-card">
<div class="tip-icon">💡</div>
<div class="tip-content">
<strong>及时解除引用</strong>
<p>对象不再使用时,将其设为 null</p>
</div>
</div>
<div class="tip-card">
<div class="tip-icon">🔒</div>
<div class="tip-content">
<strong>避免意外的全局变量</strong>
<p>使用 const/let 代替 var</p>
</div>
</div>
<div class="tip-card">
<div class="tip-icon">🧹</div>
<div class="tip-content">
<strong>清理事件监听</strong>
<p>组件销毁时移除所有监听器</p>
</div>
</div>
<div class="tip-card">
<div class="tip-icon">📊</div>
<div class="tip-content">
<strong>定期检查内存</strong>
<p> DevTools Memory 面板监控</p>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.garbage-collection-demo {
border: 1px solid var(--vp-c-border);
border-radius: 12px;
padding: 24px;
margin: 24px 0;
background: var(--vp-c-bg);
}
h3 {
margin: 0 0 20px 0;
font-size: 18px;
font-weight: 600;
color: var(--vp-c-text-1);
}
h4 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-1);
}
h5 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
}
.phase-indicator {
margin-bottom: 20px;
}
.phase-tabs {
display: flex;
gap: 12px;
}
.phase-tab {
flex: 1;
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border: 2px solid transparent;
transition: all 0.3s ease;
}
.phase-tab.active {
border-color: var(--vp-c-brand-1);
background: rgba(62, 175, 124, 0.1);
}
.phase-label {
display: block;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.phase-description {
display: block;
font-size: 13px;
color: var(--vp-c-text-2);
}
.graph-container {
margin-bottom: 20px;
}
.graph-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.legend {
display: flex;
gap: 16px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--vp-c-text-2);
}
.legend-color {
width: 20px;
height: 20px;
border-radius: 4px;
border: 2px solid;
}
.legend-color.unmarked {
background: var(--vp-c-bg);
border-color: var(--vp-c-border);
}
.legend-color.marked {
background: rgba(104, 217, 145, 0.2);
border-color: #68d391;
}
.legend-color.collected {
background: rgba(245, 101, 101, 0.2);
border-color: #f56565;
}
.object-graph {
position: relative;
height: 400px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
}
.root-object {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
}
.objects-grid {
position: absolute;
left: 150px;
top: 20px;
right: 20px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.object-node {
transition: all 0.3s ease;
}
.object-box {
padding: 16px;
background: var(--vp-c-bg);
border: 3px solid var(--vp-c-border);
border-radius: 8px;
text-align: center;
transition: all 0.3s ease;
}
.object-box.root {
border-color: var(--vp-c-brand-1);
background: rgba(62, 175, 124, 0.1);
}
.object-node.marked .object-box {
border-color: #68d391;
background: rgba(104, 217, 145, 0.1);
}
.object-node.collected .object-box {
border-color: #f56565;
background: rgba(245, 101, 101, 0.1);
opacity: 0.5;
}
.object-icon {
width: 48px;
height: 48px;
margin: 0 auto 12px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
background: var(--vp-c-bg-soft);
}
.object-name {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 8px;
}
.object-status {
font-size: 12px;
font-weight: 600;
}
.object-status:not(.collected) {
color: #68d391;
}
.object-status.collected {
color: #f56565;
}
.connections {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.connections line {
stroke: #a0aec0;
stroke-width: 2;
transition: all 0.3s ease;
}
.connections line.active {
stroke: var(--vp-c-brand-1);
stroke-width: 3;
animation: pulse 1s ease infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.step-description {
padding: 16px;
background: var(--vp-c-bg-soft);
border-left: 4px solid var(--vp-c-brand-1);
border-radius: 8px;
margin-bottom: 20px;
}
.step-content {
font-size: 14px;
color: var(--vp-c-text-1);
}
.step-content strong {
color: var(--vp-c-brand-1);
}
.controls {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
button:active {
transform: scale(0.95);
}
.btn-play {
background: var(--vp-c-brand-1);
color: white;
}
.btn-play:hover:not(:disabled) {
background: var(--vp-c-brand-2);
}
.btn-step {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.btn-step:hover:not(:disabled) {
background: var(--vp-c-bg-soft-hover);
}
.btn-stop {
background: #ed8936;
color: white;
}
.btn-stop:hover:not(:disabled) {
background: #dd6b20;
}
.btn-reset {
background: #f56565;
color: white;
}
.btn-reset:hover:not(:disabled) {
background: #e53e3e;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.algorithm-box {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.algorithm-steps {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 20px;
}
.algorithm-step {
display: flex;
gap: 16px;
}
.step-number {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--vp-c-brand-1);
color: white;
font-size: 16px;
font-weight: 700;
flex-shrink: 0;
}
.algorithm-step .step-content {
flex: 1;
}
.algorithm-step strong {
display: block;
margin-bottom: 4px;
font-size: 14px;
color: var(--vp-c-text-1);
}
.algorithm-step p {
margin: 0;
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.key-points {
padding: 16px;
background: var(--vp-c-bg);
border-radius: 6px;
}
.key-points ul {
margin: 0;
padding-left: 20px;
}
.key-points li {
margin-bottom: 8px;
font-size: 14px;
line-height: 1.6;
color: var(--vp-c-text-2);
}
.key-points strong {
color: var(--vp-c-text-1);
}
.practical-tips {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 20px;
}
.tips-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.tip-card {
display: flex;
gap: 12px;
padding: 16px;
background: var(--vp-c-bg);
border-radius: 6px;
}
.tip-icon {
font-size: 24px;
flex-shrink: 0;
}
.tip-content {
flex: 1;
}
.tip-content strong {
display: block;
margin-bottom: 4px;
font-size: 14px;
color: var(--vp-c-text-1);
}
.tip-content p {
margin: 0;
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.4;
}
</style>
@@ -0,0 +1,682 @@
<script setup>
import { ref } from 'vue'
const activeScenario = ref('global-vars')
const scenarios = [
{ value: 'global-vars', label: '全局变量', icon: '🌍' },
{ value: 'event-listeners', label: '事件监听', icon: '🎯' },
{ value: 'closures', label: '闭包引用', icon: '🔒' }
]
// 全局变量场景
const globalMemory = ref([])
// 事件监听场景
const eventListeners = ref([])
const eventCount = ref(0)
// 闭包场景
const closureItems = ref([])
const memoryUsage = ref(0)
const maxMemory = ref(100)
const addGlobalVariable = () => {
const largeData = new Array(10000).fill(`数据 ${globalMemory.value.length}`)
globalMemory.value.push({
id: Date.now(),
data: largeData,
timestamp: new Date().toLocaleTimeString()
})
updateMemory()
}
const clearGlobalVariables = () => {
globalMemory.value = []
updateMemory()
}
// 事件监听场景
const addEventListener = () => {
const handler = () => console.log('事件监听器')
eventListeners.value.push({
id: Date.now(),
handler: handler,
active: true
})
eventCount.value++
updateMemory()
}
const removeAllListeners = () => {
eventListeners.value = []
eventCount.value = 0
updateMemory()
}
// 闭包场景
const createClosure = () => {
const largeData = new Array(10000).fill('闭包数据')
const closure = () => {
return largeData.length
}
closureItems.value.push({
id: Date.now(),
closure: closure,
data: largeData,
timestamp: new Date().toLocaleTimeString()
})
updateMemory()
}
const clearClosures = () => {
closureItems.value = []
updateMemory()
}
const updateMemory = () => {
const total = globalMemory.value.length + eventListeners.value.length + closureItems.value.length
memoryUsage.value = Math.min(total, maxMemory.value)
}
const resetAll = () => {
globalMemory.value = []
eventListeners.value = []
eventCount.value = 0
closureItems.value = []
memoryUsage.value = 0
}
</script>
<template>
<div class="memory-leak-demo">
<h3>内存泄漏演示</h3>
<!-- 场景选择 -->
<div class="scenario-tabs">
<button
v-for="scenario in scenarios"
:key="scenario.value"
@click="activeScenario = scenario.value"
:class="{ 'active': activeScenario === scenario.value }"
class="scenario-tab"
>
<span class="tab-icon">{{ scenario.icon }}</span>
<span class="tab-label">{{ scenario.label }}</span>
</button>
</div>
<!-- 内存使用情况 -->
<div class="memory-monitor">
<div class="monitor-header">
<span class="monitor-title">内存使用情况</span>
<span class="monitor-value">{{ memoryUsage }}%</span>
</div>
<div class="memory-bar">
<div
class="memory-fill"
:class="{ 'warning': memoryUsage > 70, 'danger': memoryUsage > 90 }"
:style="{ width: `${memoryUsage}%` }"
>
<span v-if="memoryUsage > 10" class="memory-text">{{ memoryUsage }}%</span>
</div>
</div>
<div v-if="memoryUsage > 90" class="memory-alert">
内存占用过高!可能导致页面卡顿或崩溃
</div>
</div>
<!-- 场景内容 -->
<div class="scenario-content">
<!-- 全局变量场景 -->
<div v-if="activeScenario === 'global-vars'" class="scenario-panel">
<h4>全局变量泄漏</h4>
<div class="scenario-description">
<p><strong>问题:</strong>全局变量不会被垃圾回收,会一直占用内存</p>
<p><strong>示例:</strong>不断往全局数组添加数据,从不清理</p>
</div>
<div class="action-buttons">
<button @click="addGlobalVariable" class="btn-add">
添加全局变量
</button>
<button @click="clearGlobalVariables" class="btn-clear">
🗑 清空全局变量
</button>
</div>
<div class="data-preview">
<div class="preview-header">
<span>全局变量 ({{ globalMemory.length }} )</span>
</div>
<div class="preview-list">
<div
v-for="item in globalMemory.slice(-5)"
:key="item.id"
class="preview-item"
>
<span class="item-id">ID: {{ item.id }}</span>
<span class="item-time">{{ item.timestamp }}</span>
<span class="item-size">{{ item.data.length }} 项数据</span>
</div>
<div v-if="globalMemory.length === 0" class="empty-state">
暂无全局变量
</div>
<div v-if="globalMemory.length > 5" class="more-items">
... 还有 {{ globalMemory.length - 5 }}
</div>
</div>
</div>
<div class="code-example">
<h5> 错误做法</h5>
<pre><code>// 全局变量不会被回收
globalCache = []
function addItem() {
globalCache.push(largeData)
}</code></pre>
</div>
</div>
<!-- 事件监听场景 -->
<div v-if="activeScenario === 'event-listeners'" class="scenario-panel">
<h4>事件监听器泄漏</h4>
<div class="scenario-description">
<p><strong>问题:</strong>事件监听器没有被移除,持续占用内存</p>
<p><strong>示例:</strong>动态创建元素并添加监听,但从不移除</p>
</div>
<div class="action-buttons">
<button @click="addEventListener" class="btn-add">
添加事件监听
</button>
<button @click="removeAllListeners" class="btn-clear">
🗑 移除所有监听
</button>
</div>
<div class="data-preview">
<div class="preview-header">
<span>活跃监听器: {{ eventCount }} </span>
</div>
<div class="listener-list">
<div
v-for="listener in eventListeners.slice(-5)"
:key="listener.id"
class="listener-item"
>
<div class="listener-icon">🎯</div>
<div class="listener-info">
<span class="listener-id">监听器 #{{ listener.id }}</span>
<span class="listener-status">活跃中</span>
</div>
</div>
<div v-if="eventListeners.length === 0" class="empty-state">
暂无事件监听器
</div>
<div v-if="eventListeners.length > 5" class="more-items">
... 还有 {{ eventListeners.length - 5 }} 个监听器
</div>
</div>
</div>
<div class="code-example">
<h5> 错误做法</h5>
<pre><code>// 监听器没有被移除
button.addEventListener('click', handler)
// 元素删除时监听器还在!</code></pre>
<h5> 正确做法</h5>
<pre><code>// 保存监听器引用
const handler = () => { ... }
button.addEventListener('click', handler)
// 不需要时移除
button.removeEventListener('click', handler)</code></pre>
</div>
</div>
<!-- 闭包场景 -->
<div v-if="activeScenario === 'closures'" class="scenario-panel">
<h4>闭包引用泄漏</h4>
<div class="scenario-description">
<p><strong>问题:</strong>闭包持有大对象引用,导致对象无法被回收</p>
<p><strong>示例:</strong>闭包函数一直引用大数组</p>
</div>
<div class="action-buttons">
<button @click="createClosure" class="btn-add">
创建闭包
</button>
<button @click="clearClosures" class="btn-clear">
🗑 清空闭包
</button>
</div>
<div class="data-preview">
<div class="preview-header">
<span>活跃闭包: {{ closureItems.length }} </span>
</div>
<div class="closure-list">
<div
v-for="item in closureItems.slice(-5)"
:key="item.id"
class="closure-item"
>
<div class="closure-icon">🔒</div>
<div class="closure-info">
<span class="closure-id">闭包 #{{ item.id }}</span>
<span class="closure-time">{{ item.timestamp }}</span>
<span class="closure-size">持有 {{ item.data.length }} 项数据</span>
</div>
</div>
<div v-if="closureItems.length === 0" class="empty-state">
暂无闭包
</div>
<div v-if="closureItems.length > 5" class="more-items">
... 还有 {{ closureItems.length - 5 }} 个闭包
</div>
</div>
</div>
<div class="code-example">
<h5> 错误做法</h5>
<pre><code>// 闭包持有大对象引用
function createHandler() {
const largeData = new Array(1000000)
return function() {
// largeData 一直被引用,不会被回收
console.log('处理中')
}
}
const handler = createHandler()</code></pre>
<h5> 正确做法</h5>
<pre><code>// 使用后释放引用
let handler = createHandler()
handler() // 使用
handler = null // 释放引用</code></pre>
</div>
</div>
</div>
<!-- 重置按钮 -->
<div class="global-actions">
<button @click="resetAll" class="btn-reset">
🔄 重置所有场景
</button>
</div>
<!-- 总结 -->
<div class="summary-box">
<h4>如何避免内存泄漏</h4>
<ul>
<li><strong>避免全局变量:</strong> 使用 const/let 代替 var,尽量使用局部变量</li>
<li><strong>及时清理监听器:</strong> 组件销毁时移除所有事件监听</li>
<li><strong>释放闭包引用:</strong> 不需要时将闭包变量设为 null</li>
<li><strong>使用 WeakMap/WeakSet:</strong> 自动清理不再被引用的对象</li>
<li><strong>定期检查:</strong> DevTools Memory 面板检查内存泄漏</li>
</ul>
</div>
</div>
</template>
<style scoped>
.memory-leak-demo {
border: 1px solid var(--vp-c-border);
border-radius: 12px;
padding: 24px;
margin: 24px 0;
background: var(--vp-c-bg);
}
h3 {
margin: 0 0 20px 0;
font-size: 18px;
font-weight: 600;
color: var(--vp-c-text-1);
}
h4 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-1);
}
h5 {
margin: 12px 0 8px 0;
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-2);
}
.scenario-tabs {
display: flex;
gap: 12px;
margin-bottom: 20px;
border-bottom: 2px solid var(--vp-c-border);
}
.scenario-tab {
padding: 12px 24px;
border: none;
background: transparent;
color: var(--vp-c-text-2);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
}
.scenario-tab:hover {
color: var(--vp-c-brand-1);
}
.scenario-tab.active {
color: var(--vp-c-brand-1);
border-bottom-color: var(--vp-c-brand-1);
}
.tab-icon {
font-size: 18px;
margin-right: 8px;
}
.tab-label {
font-size: 14px;
}
.memory-monitor {
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
margin-bottom: 20px;
}
.monitor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.monitor-title {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.monitor-value {
font-size: 18px;
font-weight: 700;
color: var(--vp-c-brand-1);
}
.memory-bar {
height: 32px;
background: var(--vp-c-bg);
border-radius: 6px;
overflow: hidden;
position: relative;
}
.memory-fill {
height: 100%;
background: var(--vp-c-brand-1);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.memory-fill.warning {
background: #ed8936;
}
.memory-fill.danger {
background: #f56565;
}
.memory-text {
color: white;
font-size: 12px;
font-weight: 600;
}
.memory-alert {
margin-top: 12px;
padding: 12px;
background: rgba(245, 101, 101, 0.1);
border-left: 4px solid #f56565;
border-radius: 6px;
font-size: 13px;
color: #f56565;
font-weight: 500;
}
.scenario-content {
margin-bottom: 20px;
}
.scenario-panel {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.scenario-description {
margin-bottom: 16px;
padding: 12px;
background: var(--vp-c-bg);
border-radius: 6px;
}
.scenario-description p {
margin: 0 0 8px 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.scenario-description p:last-child {
margin-bottom: 0;
}
.scenario-description strong {
color: var(--vp-c-text-1);
}
.action-buttons {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
button {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
button:active {
transform: scale(0.95);
}
.btn-add {
background: var(--vp-c-brand-1);
color: white;
}
.btn-add:hover {
background: var(--vp-c-brand-2);
}
.btn-clear {
background: #ed8936;
color: white;
}
.btn-clear:hover {
background: #dd6b20;
}
.btn-reset {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
border: 2px solid var(--vp-c-border);
}
.btn-reset:hover {
background: var(--vp-c-bg-soft-hover);
border-color: var(--vp-c-brand-1);
}
.data-preview {
margin-bottom: 20px;
}
.preview-header {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.preview-list,
.listener-list,
.closure-list {
background: var(--vp-c-bg);
border-radius: 6px;
padding: 12px;
min-height: 150px;
}
.preview-item,
.listener-item,
.closure-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px;
margin-bottom: 8px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 13px;
}
.preview-item {
justify-content: space-between;
}
.listener-icon,
.closure-icon {
font-size: 20px;
}
.listener-info,
.closure-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.listener-id,
.closure-id {
font-weight: 600;
color: var(--vp-c-text-1);
}
.listener-status {
font-size: 12px;
color: #68d391;
}
.item-id,
.item-time,
.item-size,
.closure-time,
.closure-size {
font-size: 12px;
color: var(--vp-c-text-2);
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--vp-c-text-3);
font-size: 14px;
}
.more-items {
text-align: center;
padding: 8px;
color: var(--vp-c-text-3);
font-size: 12px;
font-style: italic;
}
.code-example {
background: #1e1e1e;
border-radius: 6px;
padding: 16px;
}
.code-example pre {
margin: 0;
}
.code-example code {
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #d4d4d4;
}
.global-actions {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.summary-box {
background: var(--vp-c-bg-soft);
border-left: 4px solid var(--vp-c-brand-1);
border-radius: 8px;
padding: 16px;
}
.summary-box h4 {
margin: 0 0 12px 0;
font-size: 14px;
color: var(--vp-c-brand-1);
}
.summary-box ul {
margin: 0;
padding-left: 20px;
}
.summary-box li {
margin-bottom: 8px;
font-size: 14px;
line-height: 1.6;
color: var(--vp-c-text-2);
}
.summary-box strong {
color: var(--vp-c-text-1);
}
</style>
@@ -0,0 +1,487 @@
<script setup>
import { ref } from 'vue'
const activeTab = ref('browser')
const tabs = [
{ value: 'browser', label: '浏览器环境', icon: '🌐' },
{ value: 'nodejs', label: 'Node.js 环境', icon: '🟢' }
]
const browserApis = [
{ name: 'window', description: '浏览器全局对象', example: 'window.location.href' },
{ name: 'document', description: 'DOM 操作', example: 'document.querySelector("h1")' },
{ name: 'localStorage', description: '本地存储', example: 'localStorage.setItem("key", "value")' },
{ name: 'fetch', description: '网络请求', example: 'fetch("/api/data")' },
{ name: 'setTimeout', description: '定时器', example: 'setTimeout(() => {}, 1000)' }
]
const nodeApis = [
{ name: 'global', description: 'Node.js 全局对象', example: 'global.process' },
{ name: 'process', description: '进程信息', example: 'process.env.NODE_ENV' },
{ name: 'fs', description: '文件系统', example: 'fs.readFile("./data.txt")' },
{ name: 'http', description: 'HTTP 服务器', example: 'http.createServer((req, res) => {})' },
{ name: 'path', description: '路径处理', example: 'path.join("/a", "b")' }
]
const tryCode = ref('console.log(typeof window)')
const browserResult = ref('')
const nodeResult = ref('')
const runInBrowser = () => {
try {
browserResult.value = eval(tryCode.value)
} catch (e) {
browserResult.value = e.message
}
nodeResult.value = '在 Node.js 中运行...'
}
const runInNode = () => {
nodeResult.value = '在浏览器中无法直接运行 Node.js 代码'
try {
browserResult.value = eval(tryCode.value)
} catch (e) {
browserResult.value = e.message
}
}
const reset = () => {
browserResult.value = ''
nodeResult.value = ''
tryCode.value = 'console.log(typeof window)'
}
</script>
<template>
<div class="runtime-environment-demo">
<h3>运行时环境对比</h3>
<div class="tab-container">
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.value"
@click="activeTab = tab.value"
:class="{ 'active': activeTab === tab.value }"
class="tab-btn"
>
<span class="tab-icon">{{ tab.icon }}</span>
<span class="tab-label">{{ tab.label }}</span>
</button>
</div>
<div class="tab-content">
<!-- 浏览器环境 -->
<div v-if="activeTab === 'browser'" class="environment-content">
<h4>浏览器环境</h4>
<div class="api-grid">
<div
v-for="api in browserApis"
:key="api.name"
class="api-card"
>
<div class="api-name">{{ api.name }}</div>
<div class="api-description">{{ api.description }}</div>
<div class="api-example">{{ api.example }}</div>
</div>
</div>
<div class="environment-note">
<strong>特点:</strong>
<ul>
<li> DOM BOM API,可以操作网页</li>
<li> Web Storage (localStorage, sessionStorage)</li>
<li> fetch XMLHttpRequest 进行网络请求</li>
<li> 没有文件系统访问权限</li>
<li> 不能直接创建 HTTP 服务器</li>
</ul>
</div>
</div>
<!-- Node.js 环境 -->
<div v-if="activeTab === 'nodejs'" class="environment-content">
<h4>Node.js 环境</h4>
<div class="api-grid">
<div
v-for="api in nodeApis"
:key="api.name"
class="api-card"
>
<div class="api-name">{{ api.name }}</div>
<div class="api-description">{{ api.description }}</div>
<div class="api-example">{{ api.example }}</div>
</div>
</div>
<div class="environment-note">
<strong>特点:</strong>
<ul>
<li> 有文件系统访问权限</li>
<li> 可以创建 HTTP 服务器</li>
<li> 可以操作进程和系统资源</li>
<li> 没有 DOM BOM</li>
<li> 不能直接操作网页元素</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 代码对比演示 -->
<div class="code-comparison-section">
<h4>代码演示:不同环境的差异</h4>
<div class="code-input">
<label>试试运行这段代码:</label>
<input
v-model="tryCode"
type="text"
placeholder="输入 JavaScript 代码"
class="code-input-field"
>
</div>
<div class="result-grid">
<div class="result-card">
<div class="result-header">
<span class="result-icon">🌐</span>
<span class="result-title">浏览器结果</span>
</div>
<div class="result-content">
{{ browserResult || '点击"在浏览器运行"查看结果' }}
</div>
<button @click="runInBrowser" class="run-btn">
在浏览器运行
</button>
</div>
<div class="result-card">
<div class="result-header">
<span class="result-icon">🟢</span>
<span class="result-title">Node.js 结果</span>
</div>
<div class="result-content">
{{ nodeResult || '需要在 Node.js 环境中运行' }}
</div>
<button @click="runInNode" class="run-btn" disabled>
需要终端运行
</button>
</div>
</div>
<button @click="reset" class="reset-btn">
重置
</button>
</div>
<!-- 总结 -->
<div class="summary-box">
<p><strong>核心区别:</strong></p>
<p>浏览器运行时专注于用户界面和网页交互,提供 DOMBOMfetch 等前端专用 API</p>
<p>Node.js 运行时专注于服务器端开发,提供文件系统HTTP 服务器进程管理等后端专用 API</p>
<p class="highlight">同样的 JavaScript 语法,但能用的 API 完全不同这就是"环境判断"的重要性</p>
</div>
</div>
</template>
<style scoped>
.runtime-environment-demo {
border: 1px solid var(--vp-c-border);
border-radius: 12px;
padding: 24px;
margin: 24px 0;
background: var(--vp-c-bg);
}
h3 {
margin: 0 0 20px 0;
font-size: 18px;
font-weight: 600;
color: var(--vp-c-text-1);
}
h4 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.tab-container {
margin-bottom: 24px;
}
.tabs {
display: flex;
gap: 12px;
margin-bottom: 20px;
border-bottom: 2px solid var(--vp-c-border);
}
.tab-btn {
padding: 12px 24px;
border: none;
background: transparent;
color: var(--vp-c-text-2);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
}
.tab-btn:hover {
color: var(--vp-c-brand-1);
}
.tab-btn.active {
color: var(--vp-c-brand-1);
border-bottom-color: var(--vp-c-brand-1);
}
.tab-icon {
font-size: 18px;
margin-right: 8px;
}
.tab-label {
font-size: 14px;
}
.tab-content {
padding: 20px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.api-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.api-card {
padding: 16px;
border: 2px solid var(--vp-c-border);
border-radius: 8px;
background: var(--vp-c-bg);
transition: all 0.2s ease;
}
.api-card:hover {
border-color: var(--vp-c-brand-1);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.api-name {
font-size: 16px;
font-weight: 600;
color: var(--vp-c-brand-1);
margin-bottom: 8px;
font-family: 'Courier New', monospace;
}
.api-description {
font-size: 13px;
color: var(--vp-c-text-2);
margin-bottom: 8px;
}
.api-example {
font-size: 12px;
color: var(--vp-c-text-3);
font-family: 'Courier New', monospace;
padding: 8px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
}
.environment-note {
padding: 16px;
background: rgba(62, 175, 124, 0.1);
border-left: 4px solid var(--vp-c-brand-1);
border-radius: 8px;
}
.environment-note strong {
display: block;
margin-bottom: 8px;
color: var(--vp-c-text-1);
}
.environment-note ul {
list-style: none;
padding: 0;
margin: 0;
}
.environment-note li {
padding: 4px 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.code-comparison-section {
border-top: 2px solid var(--vp-c-border);
padding-top: 24px;
}
.code-input {
margin-bottom: 20px;
}
.code-input label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.code-input-field {
width: 100%;
padding: 12px;
border: 2px solid var(--vp-c-border);
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 14px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
transition: border-color 0.2s ease;
}
.code-input-field:focus {
outline: none;
border-color: var(--vp-c-brand-1);
}
.result-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.result-grid {
grid-template-columns: 1fr;
}
}
.result-card {
border: 2px solid var(--vp-c-border);
border-radius: 8px;
padding: 16px;
background: var(--vp-c-bg);
}
.result-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--vp-c-border);
}
.result-icon {
font-size: 20px;
}
.result-title {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.result-content {
min-height: 60px;
padding: 12px;
margin-bottom: 12px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 13px;
color: var(--vp-c-text-1);
word-break: break-all;
}
.run-btn {
width: 100%;
padding: 10px;
border: none;
border-radius: 6px;
background: var(--vp-c-brand-1);
color: white;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.run-btn:hover:not(:disabled) {
background: var(--vp-c-brand-2);
transform: translateY(-1px);
}
.run-btn:disabled {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-3);
cursor: not-allowed;
}
.reset-btn {
padding: 10px 24px;
border: 2px solid var(--vp-c-border);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.reset-btn:hover {
background: var(--vp-c-bg-soft);
border-color: var(--vp-c-brand-1);
}
.summary-box {
background: var(--vp-c-bg-soft);
border-left: 4px solid var(--vp-c-brand-1);
border-radius: 8px;
padding: 16px;
margin-top: 24px;
}
.summary-box p {
margin: 0 0 12px 0;
font-size: 14px;
line-height: 1.6;
color: var(--vp-c-text-1);
}
.summary-box p:last-child {
margin-bottom: 0;
}
.summary-box strong {
color: var(--vp-c-brand-1);
}
.summary-box .highlight {
padding: 12px;
background: rgba(62, 175, 124, 0.1);
border-radius: 6px;
font-weight: 500;
color: var(--vp-c-brand-1);
}
</style>
@@ -0,0 +1,652 @@
<script setup>
import { ref } from 'vue'
const isAnimating = ref(false)
const currentStep = ref(0)
const syncCode = ref([
{ id: 1, code: 'console.log("1")', type: 'sync', output: '1' },
{ id: 2, code: 'setTimeout(() => console.log("2"), 0)', type: 'macro', output: '2' },
{ id: 3, code: 'Promise.resolve().then(() => console.log("3"))', type: 'micro', output: '3' },
{ id: 4, code: 'console.log("4")', type: 'sync', output: '4' },
{ id: 5, code: 'setTimeout(() => console.log("5"), 0)', type: 'macro', output: '5' }
])
const microTaskQueue = ref([])
const macroTaskQueue = ref([])
const outputLog = ref([])
const executionSteps = [
{ description: '执行 console.log("1")', action: 'execute', output: '1', source: '同步' },
{ description: '遇到 setTimeout,将回调加入宏任务队列', action: 'add-macro', task: 'console.log("2")' },
{ description: '遇到 Promise.then,将回调加入微任务队列', action: 'add-micro', task: 'console.log("3")' },
{ description: '执行 console.log("4")', action: 'execute', output: '4', source: '同步' },
{ description: '遇到 setTimeout,将回调加入宏任务队列', action: 'add-macro', task: 'console.log("5")' },
{ description: '同步代码执行完毕,检查微任务队列', action: 'check-micro' },
{ description: '执行微任务: console.log("3")', action: 'execute-micro', output: '3', source: '微任务' },
{ description: '微任务队列为空,检查宏任务队列', action: 'check-macro' },
{ description: '执行宏任务: console.log("2")', action: 'execute-macro', output: '2', source: '宏任务' },
{ description: '检查微任务队列(空)', action: 'check-micro' },
{ description: '执行宏任务: console.log("5")', action: 'execute-macro', output: '5', source: '宏任务' },
{ description: '所有任务执行完毕', action: 'done' }
]
const reset = () => {
currentStep.value = 0
microTaskQueue.value = []
macroTaskQueue.value = []
outputLog.value = []
isAnimating.value = false
}
const nextStep = () => {
if (currentStep.value >= executionSteps.length) return
const step = executionSteps[currentStep.value]
switch (step.action) {
case 'execute':
outputLog.value.push({ output: step.output, source: step.source })
break
case 'add-macro':
macroTaskQueue.value.push({ code: step.task, status: 'pending' })
break
case 'add-micro':
microTaskQueue.value.push({ code: step.task, status: 'pending' })
break
case 'check-micro':
if (microTaskQueue.value.length > 0) {
microTaskQueue.value[0].status = 'ready'
}
break
case 'execute-micro':
if (microTaskQueue.value.length > 0) {
outputLog.value.push({ output: step.output, source: step.source })
microTaskQueue.value.shift()
}
break
case 'check-macro':
if (macroTaskQueue.value.length > 0) {
macroTaskQueue.value[0].status = 'ready'
}
break
case 'execute-macro':
if (macroTaskQueue.value.length > 0) {
outputLog.value.push({ output: step.output, source: step.source })
macroTaskQueue.value.shift()
}
break
}
currentStep.value++
}
const play = async () => {
if (isAnimating.value) return
isAnimating.value = true
reset()
while (currentStep.value < executionSteps.length && isAnimating.value) {
nextStep()
await new Promise(resolve => setTimeout(resolve, 1000))
}
isAnimating.value = false
}
const stop = () => {
isAnimating.value = false
}
</script>
<template>
<div class="task-queue-demo">
<h3>任务队列:宏任务 vs 微任务</h3>
<!-- 代码展示 -->
<div class="code-section">
<h4>代码示例</h4>
<div class="code-display">
<div
v-for="(item, index) in syncCode"
:key="item.id"
class="code-item"
:class="{
'current': currentStep === index,
'executed': currentStep > index && index < 4
}"
>
<span class="item-number">{{ item.id }}</span>
<span class="item-code" :class="`type-${item.type}`">{{ item.code }}</span>
<span v-if="item.type === 'sync'" class="item-tag">同步</span>
<span v-else-if="item.type === 'micro'" class="item-tag micro">微任务</span>
<span v-else-if="item.type === 'macro'" class="item-tag macro">宏任务</span>
</div>
</div>
</div>
<!-- 执行过程可视化 -->
<div class="visualization">
<!-- 调用栈 -->
<div class="stack-panel">
<h4>调用栈 (正在执行)</h4>
<div class="stack-content">
<div v-if="currentStep < executionSteps.length" class="current-action">
{{ executionSteps[currentStep]?.description }}
</div>
<div v-else class="current-action done">
执行完成
</div>
</div>
</div>
<!-- 微任务队列 -->
<div class="queue-panel micro">
<h4>
微任务队列
<span class="badge">Microtask</span>
</h4>
<div class="queue-content">
<transition-group name="task-item">
<div
v-for="(task, index) in microTaskQueue"
:key="`micro-${index}`"
class="task-item micro"
:class="{ 'ready': task.status === 'ready' }"
>
<div class="task-code">{{ task.code }}</div>
<div v-if="task.status === 'ready'" class="task-status"> 就绪</div>
<div v-else class="task-status"> 等待</div>
</div>
</transition-group>
<div v-if="microTaskQueue.length === 0" class="empty-queue">
队列为空
</div>
</div>
</div>
<!-- 宏任务队列 -->
<div class="queue-panel macro">
<h4>
宏任务队列
<span class="badge">Macrotask</span>
</h4>
<div class="queue-content">
<transition-group name="task-item">
<div
v-for="(task, index) in macroTaskQueue"
:key="`macro-${index}`"
class="task-item macro"
:class="{ 'ready': task.status === 'ready' }"
>
<div class="task-code">{{ task.code }}</div>
<div v-if="task.status === 'ready'" class="task-status"> 就绪</div>
<div v-else class="task-status"> 等待</div>
</div>
</transition-group>
<div v-if="macroTaskQueue.length === 0" class="empty-queue">
队列为空
</div>
</div>
</div>
</div>
<!-- 输出日志 -->
<div class="output-section">
<h4>输出日志 (执行顺序)</h4>
<div class="output-log">
<div v-if="outputLog.length === 0" class="empty-log">
等待输出...
</div>
<transition-group name="output">
<div
v-for="(log, index) in outputLog"
:key="`log-${index}`"
class="log-entry"
>
<span class="log-output">{{ log.output }}</span>
<span class="log-source">({{ log.source }})</span>
</div>
</transition-group>
</div>
</div>
<!-- 控制按钮 -->
<div class="controls">
<button @click="play" :disabled="isAnimating" class="btn-play">
{{ isAnimating ? '执行中...' : '▶ 自动演示' }}
</button>
<button @click="nextStep" :disabled="isAnimating || currentStep >= executionSteps.length" class="btn-step">
单步执行
</button>
<button @click="stop" :disabled="!isAnimating" class="btn-stop">
停止
</button>
<button @click="reset" :disabled="isAnimating" class="btn-reset">
🔄 重置
</button>
</div>
<!-- 执行规则 -->
<div class="rules-box">
<h4>执行顺序规则</h4>
<div class="rule-list">
<div class="rule-item">
<span class="rule-number">1</span>
<span class="rule-text">执行所有同步代码</span>
</div>
<div class="rule-item">
<span class="rule-number">2</span>
<span class="rule-text">执行微任务队列中的所有任务</span>
</div>
<div class="rule-item">
<span class="rule-number">3</span>
<span class="rule-text">执行一个宏任务</span>
</div>
<div class="rule-item">
<span class="rule-number">4</span>
<span class="rule-text">重复步骤 2-3</span>
</div>
</div>
<p class="highlight">
<strong>核心要点:</strong> 微任务优先级高于宏任务每次执行完一个宏任务后,都会检查并执行所有微任务,然后再执行下一个宏任务
</p>
</div>
</div>
</template>
<style scoped>
.task-queue-demo {
border: 1px solid var(--vp-c-border);
border-radius: 12px;
padding: 24px;
margin: 24px 0;
background: var(--vp-c-bg);
}
h3 {
margin: 0 0 20px 0;
font-size: 18px;
font-weight: 600;
color: var(--vp-c-text-1);
}
h4 {
margin: 0 0 12px 0;
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
}
.code-section {
margin-bottom: 20px;
}
.code-display {
background: #1e1e1e;
border-radius: 8px;
padding: 16px;
font-family: 'Courier New', monospace;
}
.code-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px;
border-radius: 4px;
transition: all 0.3s ease;
}
.code-item.current {
background: rgba(62, 175, 124, 0.2);
border-left: 3px solid var(--vp-c-brand-1);
}
.code-item.executed {
opacity: 0.5;
}
.item-number {
color: #858585;
font-size: 12px;
min-width: 20px;
}
.item-code {
flex: 1;
color: #d4d4d4;
font-size: 13px;
}
.item-code.type-micro {
color: #68d391;
}
.item-code.type-macro {
color: #f687b3;
}
.item-tag {
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
}
.item-tag.micro {
background: rgba(104, 217, 145, 0.2);
color: #68d391;
}
.item-tag.macro {
background: rgba(246, 135, 179, 0.2);
color: #f687b3;
}
.visualization {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
margin-bottom: 20px;
}
@media (max-width: 1024px) {
.visualization {
grid-template-columns: 1fr;
}
}
.stack-panel,
.queue-panel {
border: 2px solid var(--vp-c-border);
border-radius: 8px;
padding: 16px;
background: var(--vp-c-bg-soft);
min-height: 250px;
}
.queue-panel.micro {
border-color: #68d391;
}
.queue-panel.macro {
border-color: #f687b3;
}
.badge {
margin-left: 8px;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
}
.stack-content,
.queue-content {
min-height: 200px;
}
.current-action {
padding: 16px;
background: var(--vp-c-bg);
border-radius: 8px;
border-left: 4px solid var(--vp-c-brand-1);
font-size: 14px;
color: var(--vp-c-text-1);
line-height: 1.6;
}
.current-action.done {
border-color: #48bb78;
text-align: center;
font-weight: 600;
}
.task-item {
padding: 12px;
margin-bottom: 8px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-border);
border-radius: 6px;
transition: all 0.3s ease;
}
.task-item.micro {
border-color: #68d391;
}
.task-item.macro {
border-color: #f687b3;
}
.task-item.ready {
animation: pulse 1s ease infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(104, 217, 145, 0.4); }
50% { box-shadow: 0 0 0 6px rgba(104, 217, 145, 0); }
}
.task-code {
font-family: 'Courier New', monospace;
font-size: 12px;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.task-status {
font-size: 11px;
font-weight: 600;
color: var(--vp-c-text-2);
}
.task-item-enter-active,
.task-item-leave-active {
transition: all 0.3s ease;
}
.task-item-enter-from {
opacity: 0;
transform: translateX(-20px);
}
.task-item-leave-to {
opacity: 0;
transform: translateX(20px);
}
.empty-queue {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--vp-c-text-3);
font-size: 13px;
}
.output-section {
margin-bottom: 20px;
}
.output-log {
min-height: 60px;
padding: 12px;
border: 2px solid var(--vp-c-border);
border-radius: 8px;
background: var(--vp-c-bg);
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.empty-log {
color: var(--vp-c-text-3);
font-size: 14px;
}
.log-entry {
padding: 8px 12px;
background: var(--vp-c-brand-1);
color: white;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.log-output {
font-family: 'Courier New', monospace;
}
.log-source {
margin-left: 8px;
font-size: 12px;
opacity: 0.8;
}
.output-enter-active,
.output-leave-active {
transition: all 0.3s ease;
}
.output-enter-from {
opacity: 0;
transform: translateY(-10px);
}
.output-leave-to {
opacity: 0;
transform: translateY(10px);
}
.controls {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
button:active {
transform: scale(0.95);
}
.btn-play {
background: var(--vp-c-brand-1);
color: white;
}
.btn-play:hover:not(:disabled) {
background: var(--vp-c-brand-2);
}
.btn-step {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.btn-step:hover:not(:disabled) {
background: var(--vp-c-bg-soft-hover);
}
.btn-stop {
background: #ed8936;
color: white;
}
.btn-stop:hover:not(:disabled) {
background: #dd6b20;
}
.btn-reset {
background: #f56565;
color: white;
}
.btn-reset:hover:not(:disabled) {
background: #e53e3e;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.rules-box {
background: var(--vp-c-bg-soft);
border-left: 4px solid var(--vp-c-brand-1);
border-radius: 8px;
padding: 16px;
}
.rule-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
}
.rule-item {
display: flex;
align-items: center;
gap: 12px;
}
.rule-number {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--vp-c-brand-1);
color: white;
font-size: 14px;
font-weight: 600;
flex-shrink: 0;
}
.rule-text {
font-size: 14px;
color: var(--vp-c-text-1);
}
.highlight {
padding: 12px;
background: rgba(62, 175, 124, 0.1);
border-radius: 6px;
font-size: 14px;
line-height: 1.6;
color: var(--vp-c-text-1);
margin: 0;
}
.highlight strong {
color: var(--vp-c-brand-1);
}
</style>
@@ -0,0 +1,556 @@
<script setup>
import { ref } from 'vue'
// 泛型函数演示
const inputValue = ref('')
const selectedType = ref('number')
const result = ref(null)
const showResult = ref(false)
// 泛型数组反转(不使用 TypeScript 泛型语法)
function reverseArray(arr) {
return [...arr].reverse()
}
// 执行反转操作
const executeReverse = () => {
if (!inputValue.value) {
result.value = '请输入内容'
showResult.value = true
return
}
try {
switch (selectedType.value) {
case 'number':
const numArray = inputValue.value.split(',').map(n => parseFloat(n.trim())).filter(n => !isNaN(n))
result.value = {
input: numArray,
output: reverseArray(numArray),
type: 'number[]'
}
break
case 'string':
const strArray = inputValue.value.split(',').map(s => s.trim())
result.value = {
input: strArray,
output: reverseArray(strArray),
type: 'string[]'
}
break
default:
result.value = { error: '未知类型' }
}
showResult.value = true
} catch (error) {
result.value = { error: '输入格式错误' }
showResult.value = true
}
}
// 重置
const reset = () => {
inputValue.value = ''
result.value = null
showResult.value = false
}
// 示例数据
const loadExample = (type) => {
selectedType.value = type
if (type === 'number') {
inputValue.value = '1, 2, 3, 4, 5'
} else {
inputValue.value = '苹果, 香蕉, 橙子, 葡萄'
}
result.value = null
showResult.value = false
}
</script>
<template>
<div class="generic-demo">
<h3>🔄 泛型 (Generics) 演示</h3>
<div class="demo-container">
<!-- 泛型概念说明 -->
<div class="concept-box">
<div class="concept-icon">💡</div>
<div class="concept-text">
<strong>泛型就像"通用模板"</strong> - 可以处理不同类型的数据同时保持类型安全
</div>
</div>
<!-- 泛型函数定义 -->
<div class="function-definition">
<div class="code-header">
<span class="typescript-logo">TS</span>
<span>泛型函数定义</span>
</div>
<pre><code class="typescript">// T 是类型变量,使用时才会确定具体类型
function identity&lt;T&gt;(arg: T): T {
return arg
}
// 泛型数组反转
function reverseArray&lt;T&gt;(arr: T[]): T[] {
return [...arr].reverse()
}</code></pre>
</div>
<!-- 交互演示 -->
<div class="interactive-demo">
<div class="demo-controls">
<div class="input-group">
<label>选择数据类型</label>
<div class="type-selector">
<button
:class="['type-btn', { active: selectedType === 'number' }]"
@click="selectedType = 'number'"
>
数字数组
</button>
<button
:class="['type-btn', { active: selectedType === 'string' }]"
@click="selectedType = 'string'"
>
字符串数组
</button>
</div>
</div>
<div class="input-group">
<label>输入数组逗号分隔</label>
<input
v-model="inputValue"
type="text"
:placeholder="selectedType === 'number' ? '1, 2, 3, 4, 5' : '苹果, 香蕉, 橙子'"
class="text-input"
/>
</div>
<div class="example-buttons">
<button @click="loadExample('number')" class="btn-example">
加载数字示例
</button>
<button @click="loadExample('string')" class="btn-example">
加载字符串示例
</button>
</div>
<div class="action-buttons">
<button @click="executeReverse" class="btn-primary">
执行反转
</button>
<button @click="reset" class="btn-secondary">
重置
</button>
</div>
</div>
<!-- 结果展示 -->
<div v-if="showResult" class="result-display">
<div class="result-header">
<span class="result-icon">📊</span>
<span>执行结果</span>
</div>
<div v-if="result && !result.error" class="result-content">
<div class="result-item">
<div class="result-label">输入类型</div>
<div class="result-value type-badge">{{ result.type }}</div>
</div>
<div class="result-item">
<div class="result-label">输入数组</div>
<div class="result-value array-display">
[{{ result.input.join(', ') }}]
</div>
</div>
<div class="result-item">
<div class="result-label">输出数组</div>
<div class="result-value array-display output">
[{{ result.output.join(', ') }}]
</div>
</div>
<div class="type-info">
<div class="info-icon"></div>
<div>类型安全输入 {{ result.type }}输出 {{ result.type }}</div>
</div>
</div>
<div v-else class="error-display">
{{ result?.error || result }}
</div>
</div>
</div>
<!-- 使用示例 -->
<div class="usage-examples">
<h4>📝 泛型使用示例</h4>
<div class="example-grid">
<div class="example-card">
<div class="example-title">数字数组</div>
<pre><code class="typescript">const nums = [1, 2, 3, 4, 5]
const reversed = reverseArray&lt;number&gt;(nums)
// 结果: [5, 4, 3, 2, 1]
// 类型: number[]</code></pre>
</div>
<div class="example-card">
<div class="example-title">字符串数组</div>
<pre><code class="typescript">const strs = ["a", "b", "c"]
const reversed = reverseArray&lt;string&gt;(strs)
// 结果: ["c", "b", "a"]
// 类型: string[]</code></pre>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.generic-demo {
border: 1px solid var(--vp-c-border);
border-radius: 16px;
padding: 24px;
margin: 24px 0;
background: var(--vp-c-bg);
}
h3 {
margin: 0 0 20px 0;
font-size: 18px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.demo-container {
max-width: 900px;
margin: 0 auto;
}
.concept-box {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
margin-bottom: 20px;
color: white;
}
.concept-icon {
font-size: 24px;
}
.concept-text {
flex: 1;
font-size: 14px;
}
.function-definition {
background: #1e1e1e;
border-radius: 8px;
padding: 16px;
margin-bottom: 20px;
border-left: 4px solid #3178c6;
}
.code-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
color: white;
font-size: 14px;
font-weight: 600;
}
.typescript-logo {
background: #3178c6;
color: white;
width: 28px;
height: 28px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
}
.function-definition pre {
margin: 0;
}
.function-definition code {
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
color: #d4d4d4;
}
.interactive-demo {
border: 2px solid var(--vp-c-border);
border-radius: 12px;
padding: 20px;
background: var(--vp-c-bg-soft);
margin-bottom: 20px;
}
.demo-controls {
display: flex;
flex-direction: column;
gap: 16px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.input-group label {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
}
.type-selector {
display: flex;
gap: 8px;
}
.type-btn {
flex: 1;
padding: 10px 16px;
border: 2px solid var(--vp-c-border);
border-radius: 8px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.type-btn:hover {
border-color: var(--vp-c-brand-1);
}
.type-btn.active {
border-color: var(--vp-c-brand-1);
background: var(--vp-c-brand-1);
color: white;
}
.text-input {
padding: 12px 16px;
border: 2px solid var(--vp-c-border);
border-radius: 8px;
font-size: 14px;
font-family: 'Courier New', monospace;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
transition: border-color 0.2s ease;
}
.text-input:focus {
outline: none;
border-color: var(--vp-c-brand-1);
}
.example-buttons {
display: flex;
gap: 8px;
}
.action-buttons {
display: flex;
gap: 12px;
}
button {
padding: 10px 18px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
button:active {
transform: scale(0.95);
}
.btn-primary {
background: var(--vp-c-brand-1);
color: white;
flex: 1;
}
.btn-primary:hover {
background: var(--vp-c-brand-2);
}
.btn-secondary {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-border);
}
.btn-secondary:hover {
background: var(--vp-c-bg-soft-hover);
}
.btn-example {
background: #dbeafe;
color: #1e40af;
flex: 1;
}
.btn-example:hover {
background: #bfdbfe;
}
.result-display {
margin-top: 20px;
padding: 20px;
background: var(--vp-c-bg);
border-radius: 8px;
border: 2px solid var(--vp-c-brand-1);
}
.result-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.result-icon {
font-size: 20px;
}
.result-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.result-item {
display: flex;
align-items: center;
gap: 12px;
font-size: 14px;
}
.result-label {
font-weight: 600;
color: var(--vp-c-text-2);
min-width: 100px;
}
.result-value {
flex: 1;
font-family: 'Courier New', monospace;
}
.type-badge {
padding: 4px 10px;
background: #dbeafe;
color: #1e40af;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.array-display {
padding: 8px 12px;
background: #f3f4f6;
border-radius: 6px;
}
.array-display.output {
background: #d1fae5;
font-weight: 600;
}
.type-info {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background: #f0fdf4;
border-radius: 6px;
color: #166534;
font-size: 13px;
margin-top: 8px;
}
.info-icon {
font-size: 16px;
}
.error-display {
padding: 12px;
background: #fef2f2;
color: #991b1b;
border-radius: 6px;
text-align: center;
}
.usage-examples {
margin-top: 20px;
}
.usage-examples h4 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.example-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}
.example-card {
border: 2px solid var(--vp-c-border);
border-radius: 8px;
overflow: hidden;
}
.example-title {
padding: 12px 16px;
background: var(--vp-c-bg-soft);
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.example-card pre {
margin: 0;
padding: 16px;
background: #1e1e1e;
overflow-x: auto;
}
.example-card code {
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #d4d4d4;
}
</style>
@@ -0,0 +1,445 @@
<script setup>
import { ref } from 'vue'
// 用户数据
const user = ref({
id: 1,
name: '张三',
email: 'zhangsan@example.com',
age: 25
})
// 显示错误信息
const showError = ref(false)
const errorMessage = ref('')
const setMessage = (msg, isError = false) => {
errorMessage.value = msg
showError.value = isError
setTimeout(() => {
errorMessage.value = ''
showError.value = false
}, 3000)
}
// 尝试添加错误类型的属性
const addErrorProperty = () => {
showError.value = true
errorMessage.value = '❌ TypeScript 错误:类型 "string" 不可分配给类型 "number"'
setTimeout(() => {
showError.value = false
errorMessage.value = ''
}, 3000)
}
// 添加新用户
const addNewUser = () => {
user.value = {
id: 2,
name: '李四',
email: 'lisi@example.com',
age: 30
}
setMessage('✅ 创建新用户成功!类型检查通过', false)
}
// 修改用户年龄
const modifyAge = () => {
user.value.age = user.value.age + 1
setMessage(`✅ 年龄更新为 ${user.value.age}`, false)
}
// 重置
const reset = () => {
user.value = {
id: 1,
name: '张三',
email: 'zhangsan@example.com',
age: 25
}
errorMessage.value = ''
showError.value = false
}
</script>
<template>
<div class="interface-demo">
<h3>🎯 Interface 接口演示</h3>
<div class="demo-container">
<!-- 接口定义 -->
<div class="interface-definition">
<div class="code-header">
<span class="typescript-logo">TS</span>
<span>User Interface 定义</span>
</div>
<pre><code class="typescript">interface User {
id: number
name: string
email: string
age: number
}</code></pre>
</div>
<!-- 用户对象展示 -->
<div class="user-display">
<div class="user-card">
<div class="card-header">
<div class="avatar">👤</div>
<div class="user-info">
<div class="user-name">{{ user.name }}</div>
<div class="user-email">{{ user.email }}</div>
</div>
</div>
<div class="user-details">
<div class="detail-item">
<span class="label">ID:</span>
<span class="value">{{ user.id }}</span>
<span class="type-badge">number</span>
</div>
<div class="detail-item">
<span class="label">年龄:</span>
<span class="value">{{ user.age }}</span>
<span class="type-badge">number</span>
</div>
</div>
</div>
</div>
<!-- 错误消息显示 -->
<div
v-if="errorMessage"
:class="['message-box', showError ? 'error' : 'success']"
>
{{ errorMessage }}
</div>
<!-- 操作按钮 -->
<div class="controls">
<button @click="modifyAge" class="btn-primary">
增加年龄
</button>
<button @click="addErrorProperty" class="btn-danger">
尝试赋值错误类型
</button>
<button @click="addNewUser" class="btn-secondary">
创建新用户
</button>
<button @click="reset" class="btn-ghost">重置</button>
</div>
<!-- 代码示例 -->
<div class="code-examples">
<div class="example-item">
<div class="example-header"> 正确使用</div>
<pre><code class="typescript">const user: User = {
id: 1,
name: "张三",
email: "zhangsan@example.com",
age: 25
} // ✅ 类型完全匹配</code></pre>
</div>
<div class="example-item error">
<div class="example-header"> 错误使用</div>
<pre><code class="typescript">const user: User = {
id: 1,
name: "张三",
email: "zhangsan@example.com",
age: "25" // ❌ 错误:age 应该是 number,不是 string
}</code></pre>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.interface-demo {
border: 1px solid var(--vp-c-border);
border-radius: 16px;
padding: 24px;
margin: 24px 0;
background: var(--vp-c-bg);
}
h3 {
margin: 0 0 20px 0;
font-size: 18px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.demo-container {
max-width: 900px;
margin: 0 auto;
}
.interface-definition {
background: #1e1e1e;
border-radius: 8px;
padding: 16px;
margin-bottom: 20px;
border-left: 4px solid #3178c6;
}
.code-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
color: white;
font-size: 14px;
font-weight: 600;
}
.typescript-logo {
background: #3178c6;
color: white;
width: 28px;
height: 28px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
}
.interface-definition pre {
margin: 0;
}
.interface-definition code {
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
color: #d4d4d4;
}
.user-display {
margin-bottom: 20px;
}
.user-card {
border: 2px solid var(--vp-c-border);
border-radius: 12px;
padding: 20px;
background: var(--vp-c-bg-soft);
max-width: 400px;
margin: 0 auto;
}
.card-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid var(--vp-c-border);
}
.avatar {
width: 60px;
height: 60px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
}
.user-info {
flex: 1;
}
.user-name {
font-size: 18px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.user-email {
font-size: 13px;
color: var(--vp-c-text-2);
}
.user-details {
display: flex;
flex-direction: column;
gap: 12px;
}
.detail-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.label {
font-weight: 600;
color: var(--vp-c-text-2);
min-width: 60px;
}
.value {
font-family: 'Courier New', monospace;
color: var(--vp-c-brand-1);
font-weight: 600;
}
.type-badge {
margin-left: auto;
padding: 3px 8px;
background: #dbeafe;
color: #1e40af;
border-radius: 4px;
font-size: 11px;
font-family: 'Courier New', monospace;
font-weight: 600;
}
.message-box {
padding: 12px 20px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
font-weight: 500;
text-align: center;
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message-box.error {
background: #fef2f2;
color: #991b1b;
border: 1px solid #fecaca;
}
.message-box.success {
background: #f0fdf4;
color: #166534;
border: 1px solid #bbf7d0;
}
.controls {
display: flex;
gap: 12px;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 24px;
}
button {
padding: 10px 18px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
button:active {
transform: scale(0.95);
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover {
background: #2563eb;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-secondary {
background: #6b7280;
color: white;
}
.btn-secondary:hover {
background: #4b5563;
}
.btn-ghost {
background: transparent;
color: var(--vp-c-text-2);
border: 1px solid var(--vp-c-border);
}
.btn-ghost:hover {
background: var(--vp-c-bg-soft-hover);
}
.code-examples {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 768px) {
.code-examples {
grid-template-columns: 1fr;
}
}
.example-item {
border: 2px solid var(--vp-c-border);
border-radius: 8px;
overflow: hidden;
}
.example-item.error {
border-color: #ef4444;
}
.example-header {
padding: 10px 16px;
font-size: 13px;
font-weight: 600;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.example-item.error .example-header {
background: #fef2f2;
color: #991b1b;
}
.example-item pre {
margin: 0;
padding: 16px;
background: #1e1e1e;
overflow-x: auto;
}
.example-item code {
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #d4d4d4;
}
</style>
@@ -0,0 +1,402 @@
<script setup>
import { ref, computed } from 'vue'
const name = ref('张三')
const age = ref(25)
const isActive = ref(true)
const showError = ref(false)
const errorMessage = ref('')
const setMessage = (msg, isError = false) => {
errorMessage.value = msg
showError.value = isError
setTimeout(() => {
errorMessage.value = ''
showError.value = false
}, 3000)
}
const modifyName = () => {
// TypeScript 会在编译时检查类型错误
// name.value = 123 // 这行会在 TypeScript 中报错
name.value = '李四'
setMessage('✅ 修改成功!类型检查通过', false)
}
const modifyAgeError = () => {
// 演示类型错误
showError.value = true
errorMessage.value = '❌ TypeScript 错误:不能将类型 "string" 分配给类型 "number"'
setTimeout(() => {
showError.value = false
errorMessage.value = ''
}, 3000)
}
const toggleActive = () => {
isActive.value = !isActive.value
setMessage(`✅ 状态切换为 ${isActive.value}`, false)
}
const reset = () => {
name.value = '张三'
age.value = 25
isActive.value = true
errorMessage.value = ''
showError.value = false
}
</script>
<template>
<div class="type-annotation-demo">
<h3>📝 TypeScript 类型注解演示</h3>
<div class="demo-container">
<div class="variables-grid">
<!-- String 类型 -->
<div class="variable-card string-card">
<div class="card-header">
<span class="type-badge string">string</span>
<span class="var-name">name</span>
</div>
<div class="card-value">{{ name }}</div>
<div class="card-code">
<code>const name: string = "{{ name }}"</code>
</div>
</div>
<!-- Number 类型 -->
<div class="variable-card number-card">
<div class="card-header">
<span class="type-badge number">number</span>
<span class="var-name">age</span>
</div>
<div class="card-value">{{ age }}</div>
<div class="card-code">
<code>const age: number = {{ age }}</code>
</div>
</div>
<!-- Boolean 类型 -->
<div class="variable-card boolean-card">
<div class="card-header">
<span class="type-badge boolean">boolean</span>
<span class="var-name">isActive</span>
</div>
<div class="card-value">
<span :class="['status-dot', isActive ? 'active' : 'inactive']"></span>
{{ isActive ? 'true' : 'false' }}
</div>
<div class="card-code">
<code>const isActive: boolean = {{ isActive }}</code>
</div>
</div>
</div>
<!-- 错误消息显示 -->
<div
v-if="errorMessage"
:class="['message-box', showError ? 'error' : 'success']"
>
{{ errorMessage }}
</div>
<!-- 操作按钮 -->
<div class="controls">
<button @click="modifyName" class="btn-primary">
修改 name (正确)
</button>
<button @click="modifyAgeError" class="btn-danger">
赋值错误类型
</button>
<button @click="toggleActive" class="btn-secondary">
切换 isActive
</button>
<button @click="reset" class="btn-ghost">重置</button>
</div>
<!-- 代码对比 -->
<div class="code-comparison">
<div class="code-panel javascript">
<div class="panel-header">JavaScript (无类型检查)</div>
<pre><code>let name = "张三"
name = 123 // ✅ 运行时才会报错(可能很晚才发现)</code></pre>
</div>
<div class="code-panel typescript">
<div class="panel-header">TypeScript (编译时检查)</div>
<pre><code>let name: string = "张三"
name = 123 // ❌ 编译时立即报错(写代码时就发现)</code></pre>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.type-annotation-demo {
border: 1px solid var(--vp-c-border);
border-radius: 16px;
padding: 24px;
margin: 24px 0;
background: var(--vp-c-bg);
}
h3 {
margin: 0 0 20px 0;
font-size: 18px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.demo-container {
max-width: 900px;
margin: 0 auto;
}
.variables-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.variable-card {
border: 2px solid var(--vp-c-border);
border-radius: 12px;
padding: 16px;
background: var(--vp-c-bg-soft);
transition: all 0.3s ease;
}
.variable-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.type-badge {
padding: 4px 10px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
font-family: 'Courier New', monospace;
text-transform: uppercase;
}
.type-badge.string {
background: #dbeafe;
color: #1e40af;
}
.type-badge.number {
background: #d1fae5;
color: #065f46;
}
.type-badge.boolean {
background: #fef3c7;
color: #92400e;
}
.var-name {
font-size: 14px;
font-weight: 600;
font-family: 'Courier New', monospace;
color: var(--vp-c-text-2);
}
.card-value {
font-size: 24px;
font-weight: 700;
font-family: 'Courier New', monospace;
color: var(--vp-c-text-1);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
}
.status-dot.active {
background: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.5);
}
.status-dot.inactive {
background: #ef4444;
}
.card-code {
background: #1e1e1e;
border-radius: 6px;
padding: 8px 12px;
font-size: 12px;
overflow-x: auto;
}
.card-code code {
font-family: 'Courier New', monospace;
color: #d4d4d4;
line-height: 1.5;
}
.message-box {
padding: 12px 20px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
font-weight: 500;
text-align: center;
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message-box.error {
background: #fef2f2;
color: #991b1b;
border: 1px solid #fecaca;
}
.message-box.success {
background: #f0fdf4;
color: #166534;
border: 1px solid #bbf7d0;
}
.controls {
display: flex;
gap: 12px;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 24px;
}
button {
padding: 10px 18px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
button:active {
transform: scale(0.95);
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover {
background: #2563eb;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-secondary {
background: #6b7280;
color: white;
}
.btn-secondary:hover {
background: #4b5563;
}
.btn-ghost {
background: transparent;
color: var(--vp-c-text-2);
border: 1px solid var(--vp-c-border);
}
.btn-ghost:hover {
background: var(--vp-c-bg-soft-hover);
}
.code-comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-top: 20px;
}
@media (max-width: 768px) {
.code-comparison {
grid-template-columns: 1fr;
}
}
.code-panel {
border: 2px solid var(--vp-c-border);
border-radius: 8px;
overflow: hidden;
}
.code-panel.javascript {
border-color: #f59e0b;
}
.code-panel.typescript {
border-color: #3178c6;
}
.panel-header {
padding: 10px 16px;
font-size: 13px;
font-weight: 600;
color: white;
}
.code-panel.javascript .panel-header {
background: #f59e0b;
}
.code-panel.typescript .panel-header {
background: #3178c6;
}
.code-panel pre {
margin: 0;
padding: 16px;
background: #1e1e1e;
overflow-x: auto;
}
.code-panel code {
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #d4d4d4;
}
</style>
@@ -0,0 +1,618 @@
<script setup>
import { ref, computed } from 'vue'
// 类型推断演示
const codeExamples = ref([
{
id: 1,
code: 'let name = "张三"',
inferredType: 'string',
explanation: 'TypeScript 根据赋值的字符串推断出 name 的类型是 string'
},
{
id: 2,
code: 'let age = 25',
inferredType: 'number',
explanation: 'TypeScript 根据数字字面量推断出 age 的类型是 number'
},
{
id: 3,
code: 'let isActive = true',
inferredType: 'boolean',
explanation: 'TypeScript 根据布尔值推断出 isActive 的类型是 boolean'
},
{
id: 4,
code: 'let numbers = [1, 2, 3]',
inferredType: 'number[]',
explanation: 'TypeScript 推断出这是一个数字数组'
}
])
const currentExample = ref(codeExamples.value[0])
// 显示类型错误
const showError = ref(false)
const errorMessage = ref('')
const setMessage = (msg, isError = false) => {
errorMessage.value = msg
showError.value = isError
setTimeout(() => {
errorMessage.value = ''
showError.value = false
}, 3000)
}
// 切换示例
const selectExample = (example) => {
currentExample.value = example
errorMessage.value = ''
showError.value = false
}
// 尝试类型错误
const tryTypeError = () => {
showError.value = true
errorMessage.value = `❌ TypeScript 错误:不能将类型 "number" 分配给类型 "${currentExample.value.inferredType}"`
setTimeout(() => {
showError.value = false
errorMessage.value = ''
}, 3000)
}
// 最佳实践示例
const bestPractices = ref([
{
title: '何时使用类型推断',
items: [
'变量初始化时有明确的值',
'函数返回值可以明显推断',
'简单的字面量赋值'
]
},
{
title: '何时需要显式注解',
items: [
'函数参数(必须)',
'对象或数组的复杂结构',
'无法从初始值推断类型',
'需要明确的类型约束'
]
}
])
// 代码对比
const codeComparisons = ref([
{
scenario: '函数返回值',
withInference: 'function add(a: number, b: number) {\n return a + b // 推断为 number\n}',
withAnnotation: 'function add(a: number, b: number): number {\n return a + b\n}',
recommendation: '推荐使用推断'
},
{
scenario: '复杂对象',
withInference: 'const user = {\n name: "张三",\n age: 25,\n email: "test@example.com"\n} // 类型自动推断',
withAnnotation: 'interface User {\n name: string\n age: number\n email: string\n}\n\nconst user: User = { ... }',
recommendation: '复杂结构建议用接口'
}
])
</script>
<template>
<div class="type-inference-demo">
<h3>🔮 类型推断演示</h3>
<div class="demo-container">
<!-- 概念说明 -->
<div class="concept-section">
<div class="concept-card">
<div class="concept-icon">🧠</div>
<div class="concept-content">
<h4>什么是类型推断</h4>
<p>TypeScript 很聪明它能根据你写的代码自动推断出变量的类型不需要每次都手动标注</p>
</div>
</div>
</div>
<!-- 示例选择器 -->
<div class="example-selector">
<h4>选择一个示例看看类型推断是如何工作的</h4>
<div class="examples-grid">
<div
v-for="example in codeExamples"
:key="example.id"
:class="['example-card', { active: currentExample.id === example.id }]"
@click="selectExample(example)"
>
<div class="example-code">{{ example.code }}</div>
<div class="example-type"> {{ example.inferredType }}</div>
</div>
</div>
</div>
<!-- 当前示例详情 -->
<div class="current-example">
<div class="example-display">
<div class="code-panel">
<div class="panel-header">
<span class="code-icon">💻</span>
<span>代码</span>
</div>
<pre><code class="typescript">{{ currentExample.code }}</code></pre>
</div>
<div class="inference-arrow"></div>
<div class="type-panel">
<div class="panel-header">
<span class="type-icon">🏷</span>
<span>推断的类型</span>
</div>
<div class="inferred-type">{{ currentExample.inferredType }}</div>
</div>
</div>
<div class="explanation">
<div class="explanation-icon">💡</div>
<div class="explanation-text">{{ currentExample.explanation }}</div>
</div>
</div>
<!-- 错误消息 -->
<div
v-if="errorMessage"
:class="['message-box', showError ? 'error' : 'success']"
>
{{ errorMessage }}
</div>
<!-- 操作按钮 -->
<div class="controls">
<button @click="tryTypeError" class="btn-danger">
尝试类型错误
</button>
<button @click="showError = false; errorMessage = ''" class="btn-secondary">
清除消息
</button>
</div>
<!-- 最佳实践 -->
<div class="best-practices">
<h4>📚 最佳实践</h4>
<div class="practices-grid">
<div
v-for="(practice, index) in bestPractices"
:key="index"
class="practice-card"
>
<div class="practice-header">{{ practice.title }}</div>
<ul class="practice-list">
<li v-for="(item, i) in practice.items" :key="i">
{{ item }}
</li>
</ul>
</div>
</div>
</div>
<!-- 代码对比 -->
<div class="comparisons">
<h4>🔄 类型推断 vs 显式注解</h4>
<div
v-for="(comparison, index) in codeComparisons"
:key="index"
class="comparison-item"
>
<div class="comparison-scenario">{{ comparison.scenario }}</div>
<div class="comparison-codes">
<div class="comparison-code">
<div class="code-label">使用推断</div>
<pre><code class="typescript">{{ comparison.withInference }}</code></pre>
</div>
<div class="comparison-code">
<div class="code-label">显式注解</div>
<pre><code class="typescript">{{ comparison.withAnnotation }}</code></pre>
</div>
</div>
<div class="comparison-recommendation">
<span class="recommendation-icon"></span>
{{ comparison.recommendation }}
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.type-inference-demo {
border: 1px solid var(--vp-c-border);
border-radius: 16px;
padding: 24px;
margin: 24px 0;
background: var(--vp-c-bg);
}
h3, h4 {
margin: 0 0 16px 0;
font-weight: 600;
color: var(--vp-c-text-1);
}
h3 {
font-size: 18px;
}
h4 {
font-size: 16px;
}
.demo-container {
max-width: 1000px;
margin: 0 auto;
}
.concept-section {
margin-bottom: 24px;
}
.concept-card {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
color: white;
}
.concept-icon {
font-size: 32px;
flex-shrink: 0;
}
.concept-content h4 {
color: white;
margin-bottom: 8px;
}
.concept-content p {
margin: 0;
font-size: 14px;
line-height: 1.6;
opacity: 0.95;
}
.example-selector {
margin-bottom: 24px;
}
.examples-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.example-card {
padding: 16px;
border: 2px solid var(--vp-c-border);
border-radius: 8px;
background: var(--vp-c-bg-soft);
cursor: pointer;
transition: all 0.2s ease;
}
.example-card:hover {
border-color: var(--vp-c-brand-1);
transform: translateY(-2px);
}
.example-card.active {
border-color: var(--vp-c-brand-1);
background: #dbeafe;
}
.example-code {
font-family: 'Courier New', monospace;
font-size: 13px;
color: var(--vp-c-text-1);
margin-bottom: 8px;
font-weight: 600;
}
.example-type {
font-family: 'Courier New', monospace;
font-size: 12px;
color: var(--vp-c-brand-1);
font-weight: 600;
}
.current-example {
margin-bottom: 24px;
}
.example-display {
display: flex;
align-items: stretch;
gap: 16px;
margin-bottom: 16px;
}
@media (max-width: 768px) {
.example-display {
flex-direction: column;
}
.inference-arrow {
transform: rotate(90deg);
}
}
.code-panel, .type-panel {
flex: 1;
border: 2px solid var(--vp-c-border);
border-radius: 8px;
overflow: hidden;
}
.panel-header {
padding: 12px 16px;
background: var(--vp-c-bg-soft);
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-2);
display: flex;
align-items: center;
gap: 8px;
}
.code-icon, .type-icon {
font-size: 16px;
}
.code-panel pre {
margin: 0;
padding: 20px;
background: #1e1e1e;
overflow-x: auto;
}
.code-panel code {
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
color: #d4d4d4;
}
.inference-arrow {
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: var(--vp-c-brand-1);
font-weight: 700;
}
.type-panel {
display: flex;
flex-direction: column;
}
.inferred-type {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Courier New', monospace;
font-size: 20px;
font-weight: 700;
color: var(--vp-c-brand-1);
background: #dbeafe;
}
.explanation {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border-left: 4px solid var(--vp-c-brand-1);
}
.explanation-icon {
font-size: 20px;
flex-shrink: 0;
}
.explanation-text {
flex: 1;
font-size: 14px;
line-height: 1.6;
color: var(--vp-c-text-1);
}
.message-box {
padding: 12px 20px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
font-weight: 500;
text-align: center;
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message-box.error {
background: #fef2f2;
color: #991b1b;
border: 1px solid #fecaca;
}
.message-box.success {
background: #f0fdf4;
color: #166534;
border: 1px solid #bbf7d0;
}
.controls {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 24px;
}
button {
padding: 10px 18px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
button:active {
transform: scale(0.95);
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-secondary {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-border);
}
.btn-secondary:hover {
background: var(--vp-c-bg-soft-hover);
}
.best-practices {
margin-bottom: 24px;
}
.practices-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}
.practice-card {
border: 2px solid var(--vp-c-border);
border-radius: 8px;
padding: 16px;
background: var(--vp-c-bg-soft);
}
.practice-header {
font-size: 15px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 12px;
}
.practice-list {
margin: 0;
padding-left: 20px;
}
.practice-list li {
font-size: 14px;
line-height: 1.8;
color: var(--vp-c-text-2);
}
.comparisons {
margin-top: 24px;
}
.comparison-item {
margin-bottom: 20px;
padding: 20px;
border: 2px solid var(--vp-c-border);
border-radius: 12px;
background: var(--vp-c-bg-soft);
}
.comparison-scenario {
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 16px;
}
.comparison-codes {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
@media (max-width: 768px) {
.comparison-codes {
grid-template-columns: 1fr;
}
}
.comparison-code {
background: #1e1e1e;
border-radius: 8px;
overflow: hidden;
}
.code-label {
padding: 10px 16px;
background: #374151;
color: white;
font-size: 12px;
font-weight: 600;
}
.comparison-code pre {
margin: 0;
padding: 16px;
overflow-x: auto;
}
.comparison-code code {
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: #d4d4d4;
}
.comparison-recommendation {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background: #f0fdf4;
border-radius: 6px;
color: #166534;
font-size: 14px;
font-weight: 500;
}
.recommendation-icon {
font-size: 16px;
}
</style>
+27
View File
@@ -494,6 +494,19 @@ import ThisContextDemo from './components/appendix/javascript-intro/ThisContextD
import PrototypeDemo from './components/appendix/javascript-intro/PrototypeDemo.vue'
import AsyncDemo from './components/appendix/javascript-intro/AsyncDemo.vue'
// JavaScript Runtime Components
import RuntimeEnvironmentDemo from './components/appendix/js-runtime/RuntimeEnvironmentDemo.vue'
import CallStackDemo from './components/appendix/js-runtime/CallStackDemo.vue'
import TaskQueueDemo from './components/appendix/js-runtime/TaskQueueDemo.vue'
import MemoryLeakDemo from './components/appendix/js-runtime/MemoryLeakDemo.vue'
import GarbageCollectionDemo from './components/appendix/js-runtime/GarbageCollectionDemo.vue'
// TypeScript Intro Components
import TypeAnnotationDemo from './components/appendix/typescript-intro/TypeAnnotationDemo.vue'
import InterfaceDemo from './components/appendix/typescript-intro/InterfaceDemo.vue'
import GenericDemo from './components/appendix/typescript-intro/GenericDemo.vue'
import TypeInferenceDemo from './components/appendix/typescript-intro/TypeInferenceDemo.vue'
export default {
extends: DefaultTheme,
Layout,
@@ -935,6 +948,7 @@ export default {
app.component('MacroMicroTaskDemo', MacroMicroTaskDemo)
app.component('RenderingPerformanceDemo', RenderingPerformanceDemo)
app.component('RenderingPipelineDemo', RenderingPipelineDemo)
app.component('EventLoopDemo', JSEventLoopDemo) // Alias for browser rendering context
// Cache Design Extra Components Registration
app.component('CacheArchitectureOverview', CacheArchitectureOverview)
@@ -986,6 +1000,19 @@ export default {
app.component('DOMTreeDemo', DOMTreeDemo)
app.component('AsyncRestaurantDemo', AsyncRestaurantDemo)
app.component('JSEventLoopDemo', JSEventLoopDemo)
// JavaScript Runtime Components Registration
app.component('RuntimeEnvironmentDemo', RuntimeEnvironmentDemo)
app.component('CallStackDemo', CallStackDemo)
app.component('TaskQueueDemo', TaskQueueDemo)
app.component('MemoryLeakDemo', MemoryLeakDemo)
app.component('GarbageCollectionDemo', GarbageCollectionDemo)
// TypeScript Intro Components Registration
app.component('TypeAnnotationDemo', TypeAnnotationDemo)
app.component('InterfaceDemo', InterfaceDemo)
app.component('GenericDemo', GenericDemo)
app.component('TypeInferenceDemo', TypeInferenceDemo)
},
setup() {
const route = useRoute()
@@ -3,6 +3,22 @@
**为什么有些网页流畅如丝,有些却卡成PPT?** 浏览器是怎么把一堆HTML、CSS、JavaScript代码变成你眼前看到的网页的?本章将带你深入浏览器的"车间",理解它的工作流程,从而写出性能更好的网页。
:::
**这篇文章会带你学什么?**
| 章节 | 内容 | 学完能干嘛 |
|-----|------|-----------|
| **第 1 章** | 为什么要理解渲染管线 | 理解性能优化的必要性 |
| **第 2 章** | 渲染管线的五个阶段 | 掌握浏览器渲染的基本流程 |
| **第 3 章** | 构建DOM树和CSSOM树 | 理解HTML和CSS如何被解析 |
| **第 4 章** | 构建渲染树 | 知道哪些元素会被渲染 |
| **第 5 章** | 布局与重排 | 避免触发昂贵的布局计算 |
| **第 6 章** | 绘制与重绘 | 减少不必要的绘制操作 |
| **第 7 章** | 合成与GPU加速 | 利用GPU提升动画性能 |
| **第 8 章** | 事件循环 | 理解JavaScript的执行机制 |
| **第 9 章** | 性能优化实战 | 掌握常用的性能优化技巧 |
每一章都从"理解原理"开始,不需要你会手写优化代码。遇到性能问题时,随时回来查就行。
---
## 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. 引言:当你按下回车键的那一刻
想象你正在进行一次**网购**。你需要:
::: tip 🤔 核心问题
**当你在浏览器输入网址并按下回车,后台发生了什么?** 为什么有的网页打开很快,有的很慢?为什么有时候会出现"找不到服务器"的错误?
:::
1. **填写订单**(选好商品,确认收货地址)
2. **系统查找仓库**(根据店铺名找到具体的发货仓库)
3. **建立物流通道**(确保仓库正常营业且能发货)
4. **仓库发货**(快递员把包裹送上门)
5. **拆箱体验**(打开包裹,看到心仪的商品)
### 生活比喻:一次网购之旅
想象你正在进行一次**网购**。整个过程可以分为 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 />
::: info 💡 核心启示
理解浏览器工作原理的关键是:**把复杂的技术过程映射到熟悉的生活场景**。网购的 5 个步骤完美对应了浏览器访问网页的 5 个技术阶段。
:::
---
## 1. 第一步:填写"订单" —— URL 解析
::: tip 🤔 核心问题
**为什么网址要写成这样?** `https://www.example.com:8080/path/page.html?id=123#section` — 这串字符到底有什么含义?
:::
### 生活比喻:填写购物单
假设你只在订单上写"买鞋子",仓库肯定不知道发哪双。你需要写清楚:
@@ -49,12 +116,18 @@
<UrlParserDemo />
> **关键理解**:URL 的存在是为了让**人类**能记住和输入。计算机最终需要的是 **IP 地址**(就像快递员最终需要的是具体的仓库地址,而不是"Nike 官方店"这个名字)。
::: info 💡 关键理解
URL 的存在是为了让**人类**能记住和输入。计算机最终需要的是 **IP 地址**(就像快递员最终需要的是具体的仓库地址,而不是"Nike 官方店"这个名字)。
:::
---
## 2. 第二步:查"地址簿" —— DNS 查询
::: tip 🤔 核心问题
**为什么浏览器能找到网站?** 你输入的是人类可读的域名(如 `baidu.com`),但计算机真正需要的是数字地址(IP)。这中间发生了什么?
:::
### 生活比喻:查仓库地址
你下单写的是"Nike 官方店",但物流系统不知道仓库在哪。它需要查地址簿:
@@ -90,12 +163,20 @@
<DnsLookupDemo />
> **为什么需要这么多层?** 想象一下如果全世界只有一个地址簿,几十亿人同时查,早就崩溃了。分层设计让每个层级只管理自己的"辖区",既高效又可靠。
::: info 💡 为什么需要这么多层?
想象一下如果全世界只有一个地址簿,几十亿人同时查,早就崩溃了。分层设计让每个层级只管理自己的"辖区",既高效又可靠。
这就是互联网设计的核心思想:**分布式系统**。
:::
---
## 3. 第三步:打电话确认 —— TCP 三次握手
::: tip 🤔 核心问题
**为什么需要"三次握手"?** 找到服务器地址后,为什么不能直接发送数据?为什么要先进行三次通信?
:::
### 生活比喻:建立物流通道
假设物流车直接开到仓库,结果:
@@ -145,6 +226,10 @@
## 4. 第四步:"买家"和"商家"的对话 —— HTTP 请求与响应
::: tip 🤔 核心问题
**浏览器和服务器在说什么?** 建立连接后,浏览器如何"告诉"服务器它想要什么?服务器又如何"回应"?
:::
### 生活比喻:仓库发货
物流车到达仓库:"这是订单(HTTP请求),**我要取回商品(网页 HTML 源代码)!**"
@@ -225,6 +310,10 @@ Set-Cookie: user_id=xyz789 ← 设置 Cookie
## 5. 第五步:拆开"包裹" —— 浏览器渲染
::: tip 🤔 核心问题
**代码怎么变成画面?** 服务器发来的是枯燥的 HTML/CSS/JavaScript 代码,浏览器如何把它们变成丰富多彩的网页?
:::
### 生活比喻:拆箱与组装
你终于收到了快递包裹(HTTP 响应),但打开一看,里面不是现成的家具,而是一堆**零件**(HTML)和一本**组装说明书**CSS)。作为"买家"(浏览器),你需要亲自动手组装:
@@ -298,14 +387,18 @@ DOM 树 + CSSOM 树 = **渲染树 (Render Tree)**。
<BrowserRenderingDemo />
> **💡 你知道吗?**
>
> **布局和绘制**是浏览器最忙碌的时候。网页里的元素越多、结构越复杂,浏览器就需要花更多时间来计算位置和上色。这就是为什么有的复杂网页打开会卡顿的原因。
::: info 💡 你知道吗?
**布局和绘制**是浏览器最忙碌的时候。网页里的元素越多、结构越复杂,浏览器就需要花更多时间来计算位置和上色。这就是为什么有的复杂网页打开会卡顿的原因。
:::
---
## 5.5 网页是怎么"生成"的?静态网站 vs 动态网站
::: tip 🤔 核心问题
**网页内容从哪里来?** 前面我们讲了浏览器如何渲染页面,但服务器上的 HTML 文件是怎么来的?是提前做好还是现做?
:::
前面我们讲的都是浏览器如何"拆开包裹"——把服务器发来的 HTML/CSS/JS 渲染成页面。但你有没有想过一个问题:**服务器上那个 HTML 文件是怎么来的?**
答案是:**有两种方式**,这就是静态网站和动态网站的区别。
@@ -381,6 +474,14 @@ DOM 树 + CSSOM 树 = **渲染树 (Render Tree)**。
## 6. 总结:一次完整的"网购"之旅
::: tip 🎉 学完本章,你应该能
- 解释从输入网址到显示页面的完整流程
- 理解 URL、DNS、TCP、HTTP 的作用和关系
- 知道浏览器如何渲染页面
- 区分静态网站和动态网站
- 用生活化比喻向他人解释浏览器工作原理
:::
让我们回顾整个旅程:
| 阶段 | 技术术语 | 网购类比 | 核心任务 | 关键技术 |
@@ -403,6 +504,13 @@ DOM 树 + CSSOM 树 = **渲染树 (Render Tree)**。
这就是互联网的魅力:**复杂的技术,简单的体验**。
::: info 💡 进阶学习
如果你想深入了解某个环节,可以参考:
- **API 开发**[API 简介](./api-intro.md) - 学习如何设计和使用 API
- **前端性能**[前端性能优化](./frontend-performance.md) - 学习如何优化网页加载速度
- **浏览器渲染**[浏览器渲染管道](./browser-rendering-pipeline.md) - 深入了解渲染细节
:::
---
## 7. 名词速查表 (Glossary)
@@ -424,4 +532,13 @@ DOM 树 + CSSOM 树 = **渲染树 (Render Tree)**。
---
> **恭喜!** 现在当你再次在地址栏输入网址时,你已经能看到屏幕背后的那个忙碌而精彩的数字世界了。
::: 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. 为什么要关注前端演进史?
::: tip 🤔 核心问题
**为什么网页越来越复杂?前端技术为什么要不断演进?** 这个问题会带你理解从简单网页到现代 Web 应用的技术演变之路。
:::
### 1.1 从"电子海报"到"桌面应用"
想象一下你在街上看到的**海报**:
想象一下你在街上看到的**海报**
- ✅ 有内容(文字、图片)
- ✅ 有设计(颜色、排版)
- ❌ 但你跟它说话,它不会回应
- ❌ 你点击某个地方,不会发生什么
- ✅ 有内容文字、图片
- ✅ 有设计颜色、排版
- ❌ 但你跟它说话它不会回应
- ❌ 你点击某个地方不会发生什么
**最早的网页**就是这样的"电子海报":只能看、不能改、内容固定。
**最早的网页**就是这样的"电子海报"只能看、不能改、内容固定。
**现代网页**完全不同了。它们像**桌面应用**(VS Code、Figma):
**现代网页**完全不同了。它们像**桌面应用**VS Code、Figma):
- ✅ 可以编辑文档、画图、玩游戏
- ✅ 实时响应你的每个操作
- ✅ 甚至可以离线工作
**这种转变的核心原因: 网页的功能越来越复杂,需要更高效的技术和开发方式。**
**这种转变的核心原因网页的功能越来越复杂需要更高效的技术和开发方式。**
### 1.2 一个生活的比喻:盖房子
### 1.2 一个生活的比喻盖房子
前端技术的演进,就像盖房子方式的进化:
前端技术的演进就像盖房子方式的进化
| 时代 | 🏠 盖房比喻 | 实际特点 | 优缺点 |
| --------- | ------------------ | ---------------------------- | --------------------------- |
| **2000s** | **贴海报** | 静态网页,写好 HTML 就行 | ✅ 简单 ❌ 不能互动 |
| **2010s** | **请工人手动装修** | jQuery 时代,手动操作每个元素 | ✅ 能互动 ❌ 代码乱、难维护 |
| **2020s** | **用乐高搭房子** | Vue/React 时代,组件化开发 | ✅ 高效、可维护 ❌ 学习曲线 |
| 时代 | 🏠 盖房比喻 | 实际特点 | 优缺点 |
|------|-----------|---------|--------|
| **2000s** | **贴海报** | 静态网页写好 HTML 就行 | ✅ 简单 ❌ 不能互动 |
| **2010s** | **请工人手动装修** | jQuery 时代手动操作每个元素 | ✅ 能互动 ❌ 代码乱、难维护 |
| **2020s** | **用乐高搭房子** | Vue/React 时代组件化开发 | ✅ 高效、可维护 ❌ 学习曲线 |
::: tip 💡 从表格中你能看到什么?
::: tip 💡 从表格中你能看到什么
**阶段一 → 阶段二**: 从"不能动"到"能动"。这是质的飞跃——网页开始有交互,但代价是代码变得混乱。
**阶段一 → 阶段二**从"不能动"到"能动"。这是质的飞跃——网页开始有交互但代价是代码变得混乱。
**阶段二 → 阶段三**: 从"能用"到"好用"。组件化让代码像积木一样可复用,大幅提升开发效率。
**阶段二 → 阶段三**从"能用"到"好用"。组件化让代码像积木一样可复用大幅提升开发效率。
**核心思想**: 技术演进不是"为了新而新",而是为了解决上一个阶段的痛点。
**核心思想**技术演进不是"为了新而新"而是为了解决上一个阶段的痛点。
:::
---
## 2. 第一阶段:静态网页与"切图"(2000s)
---
## 2. 第一阶段:静态网页与"切图"2000s
::: tip 🤔 核心问题
**最早的网页是什么样的?为什么那时候不需要框架?** 理解这个阶段的局限性,才能明白后来技术演进的必要性。
:::
<FrontendEvolutionDemo />
### 2.1 这个时代是什么样的?
### 2.1 这个时代是什么样的
**开发方式**:
**开发方式**
- 写几个 HTML 文件
- 内嵌一些 CSS 和 JavaScript
- 直接把文件拖到浏览器就能看效果
- 上传文件夹到服务器就完成部署
**特点**:
**特点**
- ✅ **优点**: 简单直接,没有学习成本,写完就能跑
- ❌ **缺点**: 无法实现复杂交互,代码一多就乱
- ✅ **优点**简单直接没有学习成本写完就能跑
- ❌ **缺点**无法实现复杂交互代码一多就乱
::: details 查看当时的项目结构
@@ -80,63 +112,69 @@ project/
└── images/
```
**遇到的问题**:
**遇到的问题**
1. **全局变量污染**: 所有变量都在全局命名空间,容易互相覆盖
2. **依赖管理混乱**: 必须按正确顺序加载 JS 文件,否则会报错
3. **代码难以复用**: 想复用某个功能,只能复制粘贴
:::
1. **全局变量污染**所有变量都在全局命名空间容易互相覆盖
2. **依赖管理混乱**必须按正确顺序加载 JS 文件否则会报错
3. **代码难以复用**想复用某个功能只能复制粘贴
:::
### 2.2 "切图"是什么?
### 2.2 "切图"是什么
<SliceRequestDemo />
你可能听说过"切图"这个词。它是早期前端的主要工作:
你可能听说过"切图"这个词。它是早期前端的主要工作:
**什么是切图?**
**什么是切图?**
设计师用 Photoshop 设计好页面 → 前端把设计切成小图片 → 用 HTML 把图片拼成页面
**为什么这么慢?**
**为什么这么慢**
网页上的每张小图片,浏览器都要发一次**网络请求**。请求越多,加载越慢。
网页上的每张小图片浏览器都要发一次**网络请求**。请求越多加载越慢。
::: tip 💡 雪碧图(Sprite)
👇 **动手试试看**:观察图片请求对加载性能的影响
为了减少请求数,出现了"雪碧图"技术:把很多小图合成一张大图。
<SliceRequestDemo />
优点是请求数变少,缺点是制作和维护都很麻烦。
::: tip 💡 雪碧图(Sprite
这个阶段的教训:**请求太多是性能大敌**
为了减少请求数,出现了"雪碧图"技术:把很多小图合成一张大图
优点是请求数变少,缺点是制作和维护都很麻烦。
这个阶段的教训:**请求太多是性能大敌**。
:::
---
## 3. 第二阶段:jQuery 时代 - "手动搬砖"(2010s)
---
### 3.1 为什么需要 jQuery?
## 3. 第二阶段:jQuery 时代 - "手动搬砖"2010s
随着网页变复杂,原生 JavaScript 的问题暴露出来:
::: tip 🤔 核心问题
**为什么需要 jQuery?它解决了什么问题,又带来了什么新问题?** 理解 jQuery 的局限性,才能明白 Vue/React 的价值。
:::
- ❌ **API 繁琐**: 简单的操作也要写很多代码
- ❌ **浏览器兼容**: 不同浏览器的 API 不一样,要写很多兼容代码
- ❌ **选择器弱**: 找元素很麻烦
### 3.1 为什么需要 jQuery
**jQuery** 诞生了。它让 JavaScript 变得简单:
随着网页变复杂,原生 JavaScript 的问题暴露出来:
- ❌ **API 繁琐**:简单的操作也要写很多代码
- ❌ **浏览器兼容**:不同浏览器的 API 不一样,要写很多兼容代码
- ❌ **选择器弱**:找元素很麻烦
**jQuery** 诞生了。它让 JavaScript 变得简单:
```javascript
// 原生 JavaScript (繁琐)
// 原生 JavaScript繁琐
const element = document.getElementById('title')
// jQuery (简洁)
// jQuery简洁
const element = $('#title')
```
### 3.2 jQuery 的思路:亲手改页面
### 3.2 jQuery 的思路亲手改页面
<JQueryVsStateDemo />
jQuery 的核心思路是**命令式**: 你告诉浏览器"怎么做"。
jQuery 的核心思路是**命令式**:你告诉浏览器"怎么做"。
```javascript
// 找到标题元素
@@ -149,11 +187,15 @@ $('#submit-btn').attr('disabled', true)
$('ul').append('<li>新项目</li>')
```
**问题**: 你需要记住页面上有哪些元素,每次数据变化都要手动更新所有相关元素。
**问题**你需要记住页面上有哪些元素每次数据变化都要手动更新所有相关元素。
👇 **动手试试看**:对比 jQuery 和数据驱动的方式
<JQueryVsStateDemo />
::: warning ⚠️ jQuery 的痛点
想象你在做一个购物车:
想象你在做一个购物车
```javascript
// 用户点击"添加到购物车"
@@ -165,32 +207,30 @@ function addToCart() {
$('#cart-page-count').text(cartCount) // 购物车页面
$('#checkout-price').text(calculatePrice()) // 结算按钮
// 如果漏了一个地方,页面就不一致了!
// 如果漏了一个地方页面就不一致了
}
```
**这就是"手动搬砖"的代价**: 容易出错,难以维护。
**这就是"手动搬砖"的代价**容易出错难以维护。
:::
### 3.3 移动端普及:响应式设计的出现
### 3.3 移动端普及响应式设计的出现
这个阶段还有一个重要变化:**手机和平板开始流行**。
这个阶段还有一个重要变化**手机和平板开始流行**。
<ResponsiveGridDemo />
网页必须适配不同屏幕。这需要**响应式布局**:同一套 HTML/CSS,自动根据屏幕宽度变换布局。
网页必须适配不同屏幕。这需要**响应式布局**: 同一套 HTML/CSS,自动根据屏幕宽度变换布局。
**响应式布局的核心: 媒体查询 (Media Query)**
**响应式布局的核心:媒体查询(Media Query**
```css
/* 电脑屏幕(大于 640px) */
/* 电脑屏幕大于 640px */
@media (min-width: 640px) {
.container {
display: flex;
}
}
/* 手机屏幕(小于 640px) */
/* 手机屏幕小于 640px */
@media (max-width: 640px) {
.container {
display: block;
@@ -198,35 +238,43 @@ function addToCart() {
}
```
👇 **动手试试看**:调整浏览器宽度,观察响应式布局的效果
<ResponsiveGridDemo />
::: tip 💡 响应式就像"智能相框"
想象你在不同房间看同一张照片:
想象你在不同房间看同一张照片
- 在**大客厅**(电脑屏幕),照片可以摆大一些,旁边还能放其他装饰品
- 在**小卧室**(手机屏幕),照片需要缩小,其他装饰品要收起来
- 在**大客厅**电脑屏幕),照片可以摆大一些旁边还能放其他装饰品
- 在**小卧室**手机屏幕),照片需要缩小其他装饰品要收起来
**响应式布局**就是"智能相框",它会自动根据房间大小调整展示方式。
**响应式布局**就是"智能相框"它会自动根据房间大小调整展示方式。
:::
---
## 4. 第三阶段:从"手动搬砖"到"数据驱动"(Vue/React)
---
### 4.1 为什么需要新框架?
## 4. 第三阶段:从"手动搬砖"到"数据驱动"Vue/React
jQuery 时代的问题积累到一定程度:
::: tip 🤔 核心问题
**为什么需要 Vue/React?它们和 jQuery 的本质区别是什么?** 理解"声明式"和"数据驱动",是掌握现代前端框架的关键。
:::
- **代码一多就乱**: 到处都是 DOM 操作,难以维护
- **容易出 bug**: 漏更新一个地方,页面就不一致
- **协作困难**: 多人修改同一个文件,容易冲突
### 4.1 为什么需要新框架?
**Vue / React** 的核心思路:**只改数据,页面自动更新**。
jQuery 时代的问题积累到一定程度:
### 4.2 Vue/React 的思路:声明式 UI
- **代码一多就乱**:到处都是 DOM 操作,难以维护
- **容易出 bug**:漏更新一个地方,页面就不一致
- **协作困难**:多人修改同一个文件,容易冲突
<ImperativeVsDeclarativeDemo />
**Vue / React** 的核心思路:**只改数据,页面自动更新**。
**jQuery (命令式)**:
### 4.2 Vue/React 的思路:声明式 UI
**jQuery(命令式)**
```javascript
// 你要告诉浏览器每一步怎么做
@@ -235,7 +283,7 @@ $('#title').css('color', 'red')
$('#title').show()
```
**Vue (声明式)**:
**Vue声明式**
```javascript
// 你只需告诉浏览器"要显示什么"
@@ -248,213 +296,409 @@ data() {
}
```
👇 **动手试试看**:对比命令式和声明式的区别
<ImperativeVsDeclarativeDemo />
::: 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**。
**MPAMulti-Page Application**
- 点一个链接 → 整页刷新 → 显示新页面
- 就像**翻书**:每翻一页都要把旧书合上、去书架拿新书
**SPASingle-Page Application**
- 点一个链接 → 只刷新内容区域 → 页面不刷新
- 就像**同一本书里换章节**:只擦掉旧内容、写上新内容
👇 **动手试试看**:体验 MPA 和 SPA 的区别
<RoutingModeDemo />
**Vue / React** 时代还有一个重要变化:**从 MPA 到 SPA**
**SPA 的优点**
**MPA (Multi-Page Application)**:
- 点一个链接 → 整页刷新 → 显示新页面
- 就像**翻书**: 每翻一页都要把旧书合上、去书架拿新书
**SPA (Single-Page Application)**:
- 点一个链接 → 只刷新内容区域 → 页面不刷新
- 就像**同一本书里换章节**: 只擦掉旧内容、写上新内容
**SPA 的优点**:
- ✅ **体验丝滑**: 页面切换快
- ✅ **状态好管理**: 输入的内容、滚动位置都在
- ❌ **首屏可能慢**: 需要先下载 JavaScript
- ❌ **SEO 要额外处理**: 搜索引擎可能抓不到内容(需要 SSR/SSG)
- ✅ **体验丝滑**:页面切换快
- ✅ **状态好管理**:输入的内容、滚动位置都在
- **首屏可能慢**:需要先下载 JavaScript
- **SEO 要额外处理**:搜索引擎可能抓不到内容(需要 SSR/SSG)
---
## 5. 渲染策略:从 CSR 到 SSR/SSG
---
## 5. 渲染策略:从 CSR 到 SSR/SSG
::: tip 🤔 核心问题
**页面是在服务器生成,还是在浏览器生成?** 不同渲染策略各有优劣,选择合适的策略对性能和 SEO 至关重要。
:::
**CSRClient-Side Rendering)客户端渲染**
- 浏览器下载 JavaScript → 执行代码 → 生成页面
- 优点:交互流畅,服务器压力小
- 缺点:首屏慢,不利于 SEO
**SSRServer-Side Rendering)服务端渲染**
- 服务器生成 HTML → 发给浏览器 → 浏览器直接显示
- 优点:首屏快,利于 SEO
- 缺点:服务器压力大,实现复杂
**SSGStatic Site Generation)静态站点生成**
- 构建时生成所有页面的 HTML
- 优点:极快,完全静态,CDN 友好
- 缺点:不适合动态内容
👇 **动手试试看**:对比不同渲染策略的特点
<RenderingStrategyDemo />
## 6. 第四阶段:工程化与构建工具(2015s-2020s)
### 6.1 为什么需要"工程化"?
前端项目越来越大,不能再靠"手动引入脚本"。
**工程化**就是用工具和规范,让开发更高效、代码更可靠、协作更顺畅。
::: tip 💡 工程化 = 从"手工作坊"到"现代化工厂"
想象一下你在家做饭 vs 开餐厅:
- **在家做饭**: 想吃什么就做什么,很自由
- **开餐厅**: 需要标准化的菜谱、规范的操作流程、统一的原材料采购
前端开发也一样:
- **小项目**: 怎么写都行
- **大项目**: 需要统一的代码规范、自动化工具、标准化流程
:::
### 6.2 构建工具:Webpack → Vite
**Webpack** (传统):
- 工作方式:**先打包,后服务**
- 启动时: 打包所有代码 → 启动服务器
- 问题:**慢**。项目越大,启动越慢(可能要等 30 秒)
**Vite** (现代):
- 工作方式:**按需编译**
- 启动时: 不打包,直接启动服务器
- 浏览器请求哪个文件,就实时编译哪个
- 优势:**快**。通常 1 秒内启动
| 对比项 | Webpack | Vite | 提升 |
| -------- | ------- | ------ | ------------ |
| 冷启动 | 30s+ | <1s | **快 30 倍** |
| 热更新 | 3-5s | <100ms | **快 30 倍** |
| 配置文件 | 几百行 | 几十行 | **大幅简化** |
::: tip 💡 为什么 Vite 这么快?
**Webpack** 就像**整备家当搬家**:先把所有东西打包,再出门。
**Vite** 就像**轻装旅行**:只带必需品,用到什么再买什么。
在开发环境,大多数时候你只需要修改几个文件,Vite 只编译这几个文件,当然快。
::: info 💡 如何选择?
- **内容网站**(博客、文档):优先 SSG
- **需要 SEO 的动态网站**(电商、新闻):使用 SSR
- **后台管理系统**:使用 CSR
- **混合需求**:考虑 Nuxt/Next.js 的混合渲染
:::
---
## 7. 总结:演进的本质
## 6. 第四阶段:工程化与构建工具(2015s-2020s
前端技术的演进,本质上是在解决两个问题:
::: tip 🤔 核心问题
**为什么前端需要"工程化"?构建工具到底在做什么?** 理解工程化,才能看懂现代前端项目的工作流程。
:::
### 7.1 效率:从手动到自动
### 6.1 为什么需要"工程化"
| 时代 | 开发方式 | 效率 |
| --------- | ------------------------ | ---------- |
| **2000s** | 手写 HTML/CSS/JS | ⭐ |
| **2010s** | jQuery + 手动 DOM 操作 | ⭐⭐ |
| **2020s** | Vue/React + 数据驱动 | ⭐⭐⭐ |
| **现在** | 组件化 + 工程化 + 自动化 | ⭐⭐⭐⭐⭐ |
前端项目越来越大,不能再靠"手动引入脚本"。
### 7.2 规模:从个人到团队
**工程化**就是用工具和规范,让开发更高效、代码更可靠、协作更顺畅。
| 时代 | 项目规模 | 协作方式 |
| --------- | ---------- | ----------------------- |
| **2000s** | 几个文件 | 单人就能维护 |
| **2010s** | 几十个文件 | 小团队,容易冲突 |
| **2020s** | 几百个文件 | 中团队,需要规范 |
| **现在** | 几千个文件 | 大团队,需要完整工程体系 |
::: tip 💡 工程化 = 从"手工作坊"到"现代化工厂"
想象一下你在家做饭 vs 开餐厅:
- **在家做饭**:想吃什么就做什么,很自由
- **开餐厅**:需要标准化的菜谱、规范的操作流程、统一的原材料采购
前端开发也一样:
- **小项目**:怎么写都行
- **大项目**:需要统一的代码规范、自动化工具、标准化流程
:::
### 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 ReactUI 库
**核心理念**:只负责视图层,其他问题交给社区
```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 调用
### 8.2 如果你有基础
### 9.2 如果你有基础
**进阶方向**:
**进阶方向**
- **工程化**: 学习 Vite/Webpack,理解构建流程
- **性能优化**: 学习懒加载、代码分割、缓存策略
- **TypeScript**: 为代码加上类型,提升可靠性
- **服务端渲染**: 学习 Nuxt/Next.js,解决 SEO 和首屏问题
- **工程化**学习 Vite/Webpack理解构建流程
- **性能优化**学习懒加载、代码分割、缓存策略
- **TypeScript**为代码加上类型提升可靠性
- **服务端渲染**学习 Nuxt/Next.js解决 SEO 和首屏问题
---
## 9. 名词速查表 (Glossary)
## 10. 你现在应该能识别的代码
| 名词 | 英文 | 用人话解释 |
| ---------------- | ----------------------- | --------------------------------------------- |
| **DOM** | Document Object Model | 文档对象模型。用对象树表示页面,可被 JS 读写。 |
| **jQuery** | - | 早期流行的 JS 库,简化了 DOM 操作。 |
| **Vue/React** | - | 现代前端框架,采用数据驱动和组件化开发。 |
| **组件** | Component | 可复用的 UI 单元,如按钮、卡片、导航栏。 |
| **MPA** | Multi-Page Application | 多页应用。每次跳转都重新加载整个页面。 |
| **SPA** | Single-Page Application | 单页应用。只加载一次,后续切换不刷新页面。 |
| **路由** | Routing | 管理页面之间切换的规则和过程。 |
| **SSR** | Server-Side Rendering | 服务端渲染。服务器生成 HTML 后发给浏览器。 |
| **SSG** | Static Site Generation | 静态站点生成。构建时预渲染页面为静态 HTML。 |
| **Webpack** | - | 传统打包工具,先打包后服务。 |
| **Vite** | - | 现代构建工具,按需编译,速度极快。 |
| **响应式** | Responsive Design | 页面自动适配不同屏幕尺寸的设计。 |
| **媒体查询** | Media Query | CSS 的条件判断,根据屏幕宽度应用不同样式。 |
| **命令式** | Imperative | 告诉程序"怎么做"。 |
| **声明式** | Declarative | 告诉程序"要什么"。 |
| **数据驱动** | Data-Driven | 只修改数据,界面自动更新。 |
| **Tree Shaking** | - | 摇树优化。自动移除未使用的代码,减小包体积。 |
| **代码分割** | Code Splitting | 把代码分成多个小块,按需加载。 |
通过阅读本章,你应该能够:
- ✅ 理解前端技术演进的脉络和原因
- ✅ 区分 Vue、React、Svelte、Angular 的特点
- ✅ 理解"命令式"和"声明式"的区别
- ✅ 掌握"数据驱动"的核心思想
- ✅ 知道组件化开发的价值
- ✅ 了解 CSR、SSR、SSG 的适用场景
- ✅ 理解构建工具(Webpack、Vite)的作用
- ✅ 能根据项目选择合适的框架和技术栈
::: info 💡 实际应用
当你用 AI 做项目时,你可以这样告诉它:
- "这是一个需要 SEO 的博客网站,用 NuxtVue 的 SSR 框架)"
- "这是一个后台管理系统,用 Vue + Element Plus,不需要 SSR"
- "这是一个性能要求高的 Web 应用,考虑使用 Svelte"
- "项目已经用 React 了,继续用 React 生态的库"
:::
---
## 总结
## 名词速查表
前端技术的演进,本质上是**从"手工"到"工业化"的进化**:
- **2000s**: 手工时代,简单直接
- **2010s**: 工具化时代,开始有框架
- **2020s**: 工业化时代,组件化 + 工程化
- **现在**: 智能化时代,AI 辅助开发
理解这个演进,你就能:
- 知道为什么要有 Vue/React
- 理解"数据驱动"的价值
- 明白工程化的必要性
- 快速上手新技术
**下一步建议**:
- 如果你想快速上手,学习 **Vue 3** (推荐) 或 **React**
- 如果你想深入理解,学习 **Vite** 构建流程
- 如果你想提升代码质量,学习 **TypeScript**
祝你学习愉快!
| 名词 | 英文 | 用人话解释 |
|------|------|-----------|
| **DOM** | Document Object Model | 文档对象模型。用对象树表示页面,可被 JS 读写。 |
| **jQuery** | - | 早期流行的 JS 库,简化了 DOM 操作。 |
| **Vue/React** | - | 现代前端框架,采用数据驱动和组件化开发。 |
| **组件** | Component | 可复用的 UI 单元,如按钮、卡片、导航栏。 |
| **MPA** | Multi-Page Application | 多页应用。每次跳转都重新加载整个页面。 |
| **SPA** | Single-Page Application | 单页应用。只加载一次,后续切换不刷新页面。 |
| **路由** | Routing | 管理页面之间切换的规则和过程。 |
| **SSR** | Server-Side Rendering | 服务端渲染。服务器生成 HTML 后发给浏览器。 |
| **SSG** | Static Site Generation | 静态站点生成。构建时预渲染页面为静态 HTML。 |
| **CSR** | Client-Side Rendering | 客户端渲染。浏览器通过 JS 生成页面。 |
| **Webpack** | - | 传统打包工具,先打包后服务。 |
| **Vite** | - | 现代构建工具,按需编译,速度极快。 |
| **响应式** | Responsive Design | 页面自动适配不同屏幕尺寸的设计。 |
| **媒体查询** | Media Query | CSS 的条件判断,根据屏幕宽度应用不同样式。 |
| **命令式** | Imperative | 告诉程序"怎么做"。 |
| **声明式** | Declarative | 告诉程序"要什么"。 |
| **数据驱动** | 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[] = ["张三", "李四", "王五"]
// 写法 2Array<类型>
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 必须有某个属性?"
:::