47377646df
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
543 lines
11 KiB
Vue
543 lines
11 KiB
Vue
<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>
|