JavaScript基础选择题在不同框架里的写法差异
深入对比原生JavaScript、Vue 3、React与Svelte实现基础选择题(单选题)组件的写法差异。从响应式数据绑定、DOM更新机制、受控与非受控模式、状态派生到无障碍访问,提供完整实战代码与原理解析。
在前端交互组件中,单项选择题是检验组件化设计与响应式能力的经典案例:它既包含了选项渲染、单选排他逻辑、受控或双向数据绑定,又需要处理提交拦截、结果校验、动态反馈样式及解释文本展开。虽然表面上看只是点击单选框并比对答案,但原生 JavaScript、Vue 3、React 与 Svelte 在实现这一功能时,底层所依赖的状态同步模型与 DOM 更新机制却有着本质区别。本文以一套标准的单选题数据为基础,横向拆解四大技术栈的具体代码实现,剖析它们在心智模型、代码组织与性能权衡上的核心差异。
一、基础选择题的交互模型与数据流转
无论采用何种前端框架,一个健壮的“基础选择题(单选题)”组件都建立在相同的数据契约与状态流转过程之上。在开发之前,我们需要明确标准题目的数据结构模型:
// 统一单选题数据契约
const quizData = {
stem: "以下哪个不是 JavaScript 的原始数据类型(Primitive Type)?",
options: [
{ key: "A", text: "String" },
{ key: "B", text: "Number" },
{ key: "C", text: "Object" },
{ key: "D", text: "Boolean" }
],
correctKey: "C",
explanation: "Object 是引用类型(Reference Type),其他如 String、Number、Boolean、Symbol、BigInt、null、undefined 均为原始类型。"
};
围绕上述题目数据,前端组件需要维护两个核心状态变量:selectedKey(用户当前选中的选项键值,初始为 null)与 isSubmitted(是否已提交判题,布尔值)。从交互开始到结束,整个组件经历四个阶段:
- 选项渲染与排他绑定:将选项列表映射为带有单选含义的视图控件,保证同一时间只能选中一个选项;
- 选项捕获与状态同步:用户发生点击操作时,及时捕获选项标识并同步到组件内存状态;
- 提交拦截与答案判决:用户点击提交按钮时,进行非空检测(防止未选直接提交)并冻结表单,对比
selectedKey === correctKey; - 视觉反馈与解析展示:将正确选项置为绿底高亮,若选错则将用户所选项置为红底标记,同时将下方解析容器展开。
二、四大技术栈下的完整实现对比
接下来,我们在相同的数据与交互规格下,逐一实现原生 JavaScript、Vue 3、React 与 Svelte 的完整代码,观察它们如何管理状态与操作视图。
1. 原生 JavaScript:命令式 DOM 操作与事件监听
在没有框架支持的原生环境中,开发者必须直接面对 DOM API。我们需要手动创建 DOM 结构,通过事件监听器捕捉单选框的 change 事件,并在提交时显式查找节点、比对结果、手动操作类名(classList)并注入文本。
<!-- 原生 HTML 结构 -->
<div id="quiz-container" class="quiz-card">
<h3 id="quiz-stem"></h3>
<form id="quiz-form">
<div id="quiz-options"></div>
<button type="submit" id="submit-btn">提交答案</button>
</form>
<div id="quiz-feedback" class="feedback-panel" style="display: none;"></div>
</div>
<script>
(function() {
const stemEl = document.getElementById('quiz-stem');
const optionsEl = document.getElementById('quiz-options');
const formEl = document.getElementById('quiz-form');
const submitBtn = document.getElementById('submit-btn');
const feedbackEl = document.getElementById('quiz-feedback');
let selectedKey = null;
let isSubmitted = false;
// 1. 渲染题干与选项
stemEl.textContent = quizData.stem;
optionsEl.innerHTML = quizData.options.map(opt => `
<label class="option-item" data-key="${opt.key}">
<input type="radio" name="quiz_opt" value="${opt.key}">
<span class="opt-label">${opt.key}. ${opt.text}</span>
</label>
`).join('');
// 2. 监听选项变更
optionsEl.addEventListener('change', (e) => {
if (isSubmitted) return;
if (e.target.name === 'quiz_opt') {
selectedKey = e.target.value;
}
});
// 3. 处理提交与判定
formEl.addEventListener('submit', (e) => {
e.preventDefault();
if (isSubmitted) return;
if (!selectedKey) {
alert('请先选择一个答案!');
return;
}}
isSubmitted = true;
submitBtn.disabled = true;
// 禁用所有单选框
const radios = optionsEl.querySelectorAll('input[name="quiz_opt"]');
radios.forEach(r => r.disabled = true);
const isCorrect = selectedKey === quizData.correctKey;
const optionLabels = optionsEl.querySelectorAll('.option-item');
// 手动操作类名赋予视觉反馈
optionLabels.forEach(label => {
const key = label.getAttribute('data-key');
if (key === quizData.correctKey) {
label.classList.add('correct');
} else if (key === selectedKey && !isCorrect) {
label.classList.add('wrong');
}
});
// 渲染解析面板
feedbackEl.style.display = 'block';
feedbackEl.className = `feedback-panel ${isCorrect ? 'correct' : 'wrong'}`;
feedbackEl.innerHTML = `
<strong>${isCorrect ? '回答正确!' : '回答错误。'}</strong>
<p>正确答案是:${quizData.correctKey}。${quizData.explanation}</p>
`;
});
})();
</script>
实现特征:原生代码无需任何编译构建步骤,体积为零,但它要求开发者时刻在内存状态(selectedKey、isSubmitted)与页面真实 DOM 之间手动搭建桥梁。一旦题目支持动态题目切换或重做,就必须编写大量清空样式、重置属性的冗余命令。
2. Vue 3:Composition API 与 v-model 双向绑定
在 Vue 3 单文件组件(SFC)中,借助 <script setup> 语法与响应式 API ref,我们可以通过 v-model 直接绑定单选按钮。视图中的样式则完全通过数据驱动的动态类名(:class)自动计算。
<template>
<div class="quiz-card">
<h3>{{ quizData.stem }}</h3>
<div class="options-group">
<label
v-for="opt in quizData.options"
:key="opt.key"
class="option-item"
:class="getOptionClass(opt.key)"
>
<input
type="radio"
name="vue_quiz"
:value="opt.key"
v-model="selectedKey"
:disabled="isSubmitted"
/>
<span>{{ opt.key }}. {{ opt.text }}</span>
</label>
</div>
<button @click="handleSubmit" :disabled="isSubmitted">
提交答案
</button>
<div v-if="isSubmitted" class="feedback-panel" :class="isCorrect ? 'correct' : 'wrong'">
<strong>{{ isCorrect ? '回答正确!' : '回答错误。' }}</strong>
<p>正确答案是:{{ quizData.correctKey }}。{{ quizData.explanation }}</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const selectedKey = ref(null);
const isSubmitted = ref(false);
const isCorrect = computed(() => selectedKey.value === quizData.correctKey);
function getOptionClass(key) {
if (!isSubmitted.value) return '';
if (key === quizData.correctKey) return 'correct';
if (key === selectedKey.value && !isCorrect.value) return 'wrong';
return '';
}
function handleSubmit() {
if (!selectedKey.value) {
alert('请先选择一个答案!');
return;
}
isSubmitted.value = true;
}
</script>
实现特征:Vue 3 的 v-model 作用于 radio 上时,会自动监听 change 事件并对比单选框的 :value 属性。开发者无需感知底层的 DOM 选择器,只要声明 selectedKey,样式的切换和禁用状态全都由响应式系统全自动派生。
3. React:useState 与受控组件的单向数据流
React 坚持单向数据流与数据不可变性原则,表单控件通常设计为“受控组件”(Controlled Component)。每个单选框的 checked 属性必须显式与状态挂钩,并通过 onChange 传递更新函数。
import React, { useState } from 'react';
export default function QuizCard() {
const [selectedKey, setSelectedKey] = useState(null);
const [isSubmitted, setIsSubmitted] = useState(false);
const isCorrect = selectedKey === quizData.correctKey;
const handleSubmit = (e) => {
e.preventDefault();
if (!selectedKey) {
alert('请先选择一个答案!');
return;
}
setIsSubmitted(true);
};
const getOptionClassName = (key) => {
let cls = 'option-item';
if (!isSubmitted) return cls;
if (key === quizData.correctKey) cls += ' correct';
else if (key === selectedKey && !isCorrect) cls += ' wrong';
return cls;
};
return (
<div className="quiz-card">
<h3>{quizData.stem}</h3>
<form onSubmit={handleSubmit}>
<div className="options-group">
{quizData.options.map((opt) => (
<label key={opt.key} className={getOptionClassName(opt.key)}>
<input
type="radio"
name="react_quiz"
value={opt.key}
checked={selectedKey === opt.key}
disabled={isSubmitted}
onChange={() => !isSubmitted && setSelectedKey(opt.key)}
/>
<span>{opt.key}. {opt.text}</span>
</label>
))}
</div>
<button type="submit" disabled={isSubmitted}>
提交答案
</button>
</form>
{isSubmitted && (
<div className={`feedback-panel ${isCorrect ? 'correct' : 'wrong'}`}>
<strong>{isCorrect ? '回答正确!' : '回答错误。'}</strong>
<p>正确答案是:{quizData.correctKey}。{quizData.explanation}</p>
</div>
)}
</div>
);
}
实现特征:在 React 中没有类似 v-model 的双向指令,checked={selectedKey === opt.key} 保证了状态的唯一可信源(Single Source of Truth)。当调用 setSelectedKey 时,组件函数重新执行,JSX 重新求值并经过 Virtual DOM 差分算法计算补丁并提交到真实 DOM。
4. Svelte:编译期响应式与 bind:group 指令
Svelte 抛弃了运行时虚拟 DOM,在编译阶段分析依赖关系。面对单选框组,Svelte 提供了专为单选框/复选框组量身打造的 bind:group 指令,写法极为凝练。
<script>
let selectedKey = null;
let isSubmitted = false;
$: isCorrect = selectedKey === quizData.correctKey;
function handleSubmit() {
if (!selectedKey) {
alert('请先选择一个答案!');
return;
}
isSubmitted = true;
}
</script>
<div class="quiz-card">
<h3>{quizData.stem}</h3>
<div class="options-group">
{#each quizData.options as opt (opt.key)}
<label
class="option-item"
class:correct={isSubmitted && opt.key === quizData.correctKey}
class:wrong={isSubmitted && opt.key === selectedKey && !isCorrect}
>
<input
type="radio"
name="svelte_quiz"
value={opt.key}
bind:group={selectedKey}
disabled={isSubmitted}
/>
<span>{opt.key}. {opt.text}</span>
</label>
{/each}
</div>
<button on:click={handleSubmit} disabled={isSubmitted}>
提交答案
</button>
{#if isSubmitted}
<div class="feedback-panel" class:correct={isCorrect} class:wrong={!isCorrect}>
<strong>{isCorrect ? '回答正确!' : '回答错误。'}</strong>
<p>正确答案是:{quizData.correctKey}。{quizData.explanation}</p>
</div>
{/if}
</div>
实现特征:Svelte 的 bind:group={selectedKey} 会自动追踪拥有同一组变量的所有 radio 节点,当其中某个被选中时,将该节点的 value 赋值给变量。同时,class:correct={condition} 的内置样式指令省去了手动拼接类名字符串的麻烦,代码量在所有实现中最精简。
三、题目答案判定与交互反馈执行步骤
在编写选择题组件时,很多初学者容易将“选值”与“判题”混淆在一起,或者在提交后没有有效锁定组件,造成状态错乱。以下是实现答题校验时的规范逻辑步骤:
-
步骤一:选项捕获与即时互斥
单选框必须赋予统一的name属性或受控状态。用户在点击某一选项时,不仅当前选项需要被标记为已选,其它已选项必须由浏览器或状态树自动取消选中,确保全局始终只有一个激活值。 -
步骤二:提交拦截与防抖保护
在提交事件(Submit)触发时,首要检查是判断selectedKey !== null。若为空,应当通过轻量提示告知用户完成作答,杜绝空值提交;同时,若isSubmitted === true,必须立刻拦截,防止快速重复点击造成多次判题或向后端连续发送评分请求。 -
步骤三:核心答案比对与状态冻结
执行判题逻辑时,推荐采用派生状态(Derived State)而非将结果存为独立自由变量。例如在 React 中直接通过const isCorrect = selectedKey === quizData.correctKey计算,在 Vue 中使用computed。判题完成后,将所有input[type="radio"]与提交按钮置为disabled,冻结操作区。 -
步骤四:差异化视觉反馈与解析渲染
提交后必须给用户提供明确的正误指示。规范的做法是:正确项使用低饱和度绿色高亮;如果用户选错,则选错项以柔和红色标示,同时依然将正确的选项标出,避免用户产生疑惑;最后在下方展开带有解析文本(explanation)的内容容器。
四、容易混淆的写法辨析与避坑指南
在实际开发选择题组件时,网络上存在一些看似合理但实际存在隐患的写法。我们需要对这些常见混淆点进行具体辨析。
常见误区:很多现代 UI 库或初学者为了方便定制样式,直接使用 <div class="option" @click="select(opt.key)"> 模拟选项,完全抛弃了原生的单选框元素。
深入辨析:这种写法看似摆脱了浏览器原生 radio 默认样式的限制,省去了处理隐藏原件的步骤,在普通鼠标点击下运作良好。然而它存在严重的可访问性(Accessibility, a11y)缺陷:键盘用户使用 Tab 键无法聚焦到选项,使用上下方向键无法在同组单选选项之间切换,屏幕阅读器也无法播报当前是“单选题第几个选项”。
正确做法:依然保留原生的 <input type="radio"> 与关联的 <label> 语义,通过 CSS 的 appearance: none; 或将单选圆圈设置为视觉隐藏,再利用 :checked 伪类或框架类名装饰自定义图标,兼顾美观与无障碍标准。
常见误区:受原生 JavaScript 习惯的影响,一些开发者在判题时使用 React 的 useRef 或 Vue 的 $refs,在点击提交时手动执行 node.classList.add('correct')。
深入辨析:现代框架的核心优势是“声明式与数据驱动(Data-Driven)”。如果跨过响应式系统直接操作 DOM 节点的类名,会导致框架内部维护的虚拟 DOM 状态与浏览器真实 DOM 状态脱节。例如,在用户切换到下一题时,组件重新使用相同的 DOM 节点渲染新题,上一次手动添加的 correct 类名可能会因为未被数据驱动追踪而意外残留,引发难以排查的显示 Bug。所有视图样式的变更,都应该通过数据状态派生计算。
深入辨析:如果是单页仅展示一道选择题的微件,局部维护 selectedKey 完全没有问题;但如果是包含 20 道选择题的考试试卷,把 selectedKey 留在每个子组件内部会导致上层“答题卡概览”或“一键交卷按钮”无法获知答题进度。此时应将每道题的作答结果通过“状态提升(Lifting State Up)”或统一状态管理存储在父级对象中(如 answers: { [questionId]: 'C' }),子组件仅作为纯展示与事件触发的受控单元。
五、各框架写法综合对比与选型考量
综合上述分析,我们将原生 JavaScript、Vue 3、React 和 Svelte 在实现基础选择题时的关键技术指标整理对比如下:
| 对比维度 | 原生 JavaScript | Vue 3 (Composition) | React (Hooks) | Svelte |
|---|---|---|---|---|
| 单选绑定方式 | 事件监听 + 读取 value |
v-model="selectedKey" |
checked + onChange 显式受控 |
bind:group={selectedKey} |
| 状态到视图的驱动 | 命令式修改 classList |
响应式 Proxy 自动追踪 | State 触发组件全量重执行 | 编译期注入局部更新代码 |
| 代码冗余度与样板代码 | 高(需手动处理节点同步) | 极低(内置指令丰富) | 中(需显式声明受控闭环) | 最低(语法直观紧凑) |
| 题库扩展与重构维护 | 维护成本高,易产生状态泄漏 | 结构清晰,适合后台与学习页 | 数据流可回溯,适合复杂复合题 | 体积轻巧,适合微站与轻量模块 |
| 运行时依赖开销 | 0 KB(无依赖) | ~30-40 KB(框架运行时) | ~40-45 KB(React + ReactDOM) | ~2 KB(编译产物原生直出) |
六、总结
实现一个基础选择题组件,其核心本质在于管理选项的排他性选择状态,并在校验阶段提供确定性的视觉反馈。通过横向对比可以发现:
- 原生 JavaScript 适合对加载体积有极限要求、不需要复杂工程化脚手架的静态说明文档嵌入场景,但需要开发者严格自行维护状态与 DOM 的对应关系;
- Vue 3 凭借
v-model和响应式系统,在表单与选择类组件开发上提供了最符合直觉的心智体验,是各类中后台题库与交互式学习平台的首选; - React 严谨的单向数据流与受控组件哲学,使得选择题在应对题目暂存、时间旅行调试、多题型混合嵌套与复杂答题卡联动时具备更高的稳健性与可预测性;
- Svelte 独创的
bind:group与无运行时虚拟 DOM 特性,在保持极简语法的同时输出了极小体积的高性能代码,非常适合轻量级答题插件与移动端轻页面。
在实际工程中,遵循语义化的原生单选标签结构、以数据状态为唯一真实来源派生样式、并在提交阶段做好防抖与异常拦截,是编写任何高质量选择题组件不可或缺的核心基石。
Next.js App Router 怎么实现嵌套路由与布局组件
2026-11-03 17:58:37
React 中使用 ECharts 实现 3D 旋转扇形图
2026-11-01 03:46:31
React 脚手架 Create React App 使用详解
2026-10-31 17:53:01
前端获取Excel表格数据并在浏览器展示方法实例
2026-10-31 15:23:51
JS前端白屏成因解析与React Error Boundary容灾方案
2026-10-30 22:58:07








