Files

375 lines
7.7 KiB
Vue
Raw Permalink Normal View History

<!--
VueReactComparisonDemo.vue
用可视化方式对比 Vue vs React语法状态更新渲染心智模型
-->
<template>
<div class="vr-demo">
<div class="header">
<div class="title">
Vue vs React它们哪里像哪里不一样
</div>
<div class="subtitle">
选一个标签页然后点+1看看背后发生了什么示意
</div>
</div>
<div class="tabs">
<button
v-for="t in tabs"
:key="t.key"
class="tab"
:class="{ active: currentTab === t.key }"
@click="currentTab = t.key"
>
{{ t.label }}
</button>
</div>
<div class="grid">
<div class="panel">
<div class="panel-title">
Vue
</div>
<div class="preview">
<div class="row">
count: <strong>{{ count }}</strong>
</div>
<button
class="btn vue"
@click="inc('vue')"
>
+1
</button>
</div>
<div class="code">
<div class="code-title">
典型写法示意
</div>
<pre><code class="language-vue">{{ vueCode }}</code></pre>
</div>
</div>
<div class="panel">
<div class="panel-title">
React
</div>
<div class="preview">
<div class="row">
count: <strong>{{ count }}</strong>
</div>
<button
class="btn react"
@click="inc('react')"
>
+1
</button>
</div>
<div class="code">
<div class="code-title">
典型写法示意
</div>
<pre><code class="language-jsx">{{ reactCode }}</code></pre>
</div>
</div>
</div>
<div class="what">
<div class="what-title">
点击 +1 时发生了什么
</div>
<div class="steps">
<div
v-for="(s, idx) in steps"
:key="idx"
class="step"
:class="{ highlight: idx === lastStepIndex }"
>
<span class="num">{{ idx + 1 }}</span>
<span class="text">{{ s }}</span>
</div>
</div>
<div class="note">
说明这是为了建立心智模型的<strong>简化示意</strong>真实框架内部更复杂
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const tabs = [
{ key: 'syntax', label: '语法(Template vs JSX' },
{ key: 'state', label: '状态更新(ref vs useState' },
{ key: 'render', label: '渲染心智模型' }
]
const currentTab = ref('syntax')
const count = ref(1)
const lastClicked = ref('vue')
const lastStepIndex = ref(-1)
const inc = (who) => {
lastClicked.value = who
count.value += 1
// 简单动画:把最后一步高亮一下
lastStepIndex.value = 2
setTimeout(() => (lastStepIndex.value = -1), 600)
}
const vueCode = computed(() => {
if (currentTab.value === 'syntax') {
// NOTE: Avoid literal closing script tag inside a script block (HTML parser would terminate early).
return [
`<template>`,
` <button @click="count++">+1</button>`,
` <div>count: {{ count }}</div>`,
`</template>`,
``,
`<script setup>`,
`import { ref } from 'vue'`,
`const count = ref(1)`,
`</scr` + `ipt>`
].join('\n')
}
if (currentTab.value === 'state') {
return `import { ref } from 'vue'
const count = ref(1)
function inc() {
count.value++
}`
}
return `// Vue:响应式系统会“追踪依赖”
// count 变了 -> 用到 count 的地方自动更新`
})
const reactCode = computed(() => {
if (currentTab.value === 'syntax') {
return `function App() {
const [count, setCount] = useState(1)
return (
<>
<button onClick={() => setCount(count + 1)}>+1</button>
<div>count: {count}</div>
</>
)
}`
}
if (currentTab.value === 'state') {
return `const [count, setCount] = useState(1)
function inc() {
setCount(count + 1)
}`
}
return `// Reactstate 变了 -> 组件函数重新执行(重新渲染)
// 然后 React 决定哪些 DOM 需要更新`
})
const steps = computed(() => {
if (currentTab.value === 'syntax') {
return [
'你写 UI 的方式:Vue 常用 TemplateReact 常用 JSX',
'点击按钮触发事件处理函数',
'count 更新后,界面显示跟着变'
]
}
if (currentTab.value === 'state') {
return [
'Vue:用 ref/ reactive 保存状态;React:用 useState 保存状态',
lastClicked.value === 'vue'
? '你修改了 count.value'
: '你调用 setCount(...)',
'框架把变化反映到界面'
]
}
return [
'Vue:更偏“依赖追踪”,谁用到了 count,就更新谁',
'React:更偏“重新执行组件函数”,得到新的 UI 描述',
'最终都会只更新需要变化的 DOM(避免全量重画)'
]
})
</script>
<style scoped>
.vr-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.tabs {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.tab {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
padding: 0.35rem 0.75rem;
border-radius: 999px;
font-size: 0.85rem;
cursor: pointer;
}
.tab.active {
border-color: #3b82f6;
color: #1d4ed8;
background: rgba(59, 130, 246, 0.12);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1rem;
}
.panel {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 12px;
padding: 0.75rem;
}
.panel-title {
font-weight: 800;
margin-bottom: 0.75rem;
}
.preview {
border: 1px dashed var(--vp-c-divider);
border-radius: 10px;
padding: 0.9rem;
background: var(--vp-c-bg-soft);
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.row {
font-size: 0.95rem;
}
.btn {
border: none;
padding: 0.45rem 0.8rem;
border-radius: 10px;
color: #fff;
cursor: pointer;
font-weight: 700;
font-size: 0.85rem;
}
.btn.vue {
background: #22c55e;
}
.btn.react {
background: #0ea5e9;
}
.code {
margin-top: 0.9rem;
}
.code-title {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.35rem;
}
pre {
margin: 0;
padding: 0.75rem;
border-radius: 10px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
overflow: auto;
}
code {
font-family: var(--vp-font-family-mono);
font-size: 0.8rem;
color: var(--vp-c-text-1);
}
.what {
margin-top: 1rem;
border-top: 1px dashed var(--vp-c-divider);
padding-top: 1rem;
}
.what-title {
font-weight: 700;
margin-bottom: 0.6rem;
}
.steps {
display: grid;
gap: 0.5rem;
}
.step {
display: flex;
gap: 0.6rem;
align-items: flex-start;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 10px;
padding: 0.55rem 0.65rem;
}
.step.highlight {
border-color: rgba(34, 197, 94, 0.5);
background: rgba(34, 197, 94, 0.08);
}
.num {
width: 1.6rem;
height: 1.6rem;
border-radius: 999px;
background: rgba(99, 102, 241, 0.15);
color: #4338ca;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: 0.85rem;
flex: 0 0 auto;
}
.text {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.35;
}
.note {
margin-top: 0.7rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
</style>