JavaScript基础选择题在不同框架里的写法差异

作者:远帆时间:2026-11-07 20:53:46浏览:216

深入对比原生JavaScript、Vue 3、React与Svelte实现基础选择题(单选题)组件的写法差异。从响应式数据绑定、DOM更新机制、受控与非受控模式、状态派生到无障碍访问,提供完整实战代码与原理解析。

前端架构与组件化 代码实现对比 Vanilla JS · Vue 3 · React · Svelte
JavaScript基础选择题在不同框架里的写法差异
图 0:JavaScript 基础选择题在不同技术栈下的开发范式概览

在前端交互组件中,单项选择题是检验组件化设计与响应式能力的经典案例:它既包含了选项渲染、单选排他逻辑、受控或双向数据绑定,又需要处理提交拦截、结果校验、动态反馈样式及解释文本展开。虽然表面上看只是点击单选框并比对答案,但原生 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(是否已提交判题,布尔值)。从交互开始到结束,整个组件经历四个阶段:

  1. 选项渲染与排他绑定:将选项列表映射为带有单选含义的视图控件,保证同一时间只能选中一个选项;
  2. 选项捕获与状态同步:用户发生点击操作时,及时捕获选项标识并同步到组件内存状态;
  3. 提交拦截与答案判决:用户点击提交按钮时,进行非空检测(防止未选直接提交)并冻结表单,对比 selectedKey === correctKey;
  4. 视觉反馈与解析展示:将正确选项置为绿底高亮,若选错则将用户所选项置为红底标记,同时将下方解析容器展开。
JavaScript基础选择题在不同框架里的写法差异
图 1:基础选择题组件的交互模型与状态流转链路

二、四大技术栈下的完整实现对比

接下来,我们在相同的数据与交互规格下,逐一实现原生 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} 的内置样式指令省去了手动拼接类名字符串的麻烦,代码量在所有实现中最精简。

JavaScript基础选择题在不同框架里的写法差异
图 2:四大框架在单选数据绑定与视图同步机制上的技术流向对比

三、题目答案判定与交互反馈执行步骤

在编写选择题组件时,很多初学者容易将“选值”与“判题”混淆在一起,或者在提交后没有有效锁定组件,造成状态错乱。以下是实现答题校验时的规范逻辑步骤:

JavaScript基础选择题在不同框架里的写法差异
图 3:基础选择题答案校验与交互反馈执行步骤流程
  1. 步骤一:选项捕获与即时互斥
    单选框必须赋予统一的 name 属性或受控状态。用户在点击某一选项时,不仅当前选项需要被标记为已选,其它已选项必须由浏览器或状态树自动取消选中,确保全局始终只有一个激活值。
  2. 步骤二:提交拦截与防抖保护
    在提交事件(Submit)触发时,首要检查是判断 selectedKey !== null。若为空,应当通过轻量提示告知用户完成作答,杜绝空值提交;同时,若 isSubmitted === true,必须立刻拦截,防止快速重复点击造成多次判题或向后端连续发送评分请求。
  3. 步骤三:核心答案比对与状态冻结
    执行判题逻辑时,推荐采用派生状态(Derived State)而非将结果存为独立自由变量。例如在 React 中直接通过 const isCorrect = selectedKey === quizData.correctKey 计算,在 Vue 中使用 computed。判题完成后,将所有 input[type="radio"] 与提交按钮置为 disabled,冻结操作区。
  4. 步骤四:差异化视觉反馈与解析渲染
    提交后必须给用户提供明确的正误指示。规范的做法是:正确项使用低饱和度绿色高亮;如果用户选错,则选错项以柔和红色标示,同时依然将正确的选项标出,避免用户产生疑惑;最后在下方展开带有解析文本(explanation)的内容容器。

四、容易混淆的写法辨析与避坑指南

在实际开发选择题组件时,网络上存在一些看似合理但实际存在隐患的写法。我们需要对这些常见混淆点进行具体辨析。

辨析一:基础选择题是否可以直接用 <div @click> 模拟,而丢弃原生 <input type="radio">?

常见误区:很多现代 UI 库或初学者为了方便定制样式,直接使用 <div class="option" @click="select(opt.key)"> 模拟选项,完全抛弃了原生的单选框元素。

深入辨析:这种写法看似摆脱了浏览器原生 radio 默认样式的限制,省去了处理隐藏原件的步骤,在普通鼠标点击下运作良好。然而它存在严重的可访问性(Accessibility, a11y)缺陷:键盘用户使用 Tab 键无法聚焦到选项,使用上下方向键无法在同组单选选项之间切换,屏幕阅读器也无法播报当前是“单选题第几个选项”。

正确做法:依然保留原生的 <input type="radio"> 与关联的 <label> 语义,通过 CSS 的 appearance: none; 或将单选圆圈设置为视觉隐藏,再利用 :checked 伪类或框架类名装饰自定义图标,兼顾美观与无障碍标准。

辨析二:在 Vue 或 React 中,是否可以通过 ref 获取 DOM 后直接 addClass?

常见误区:受原生 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基础选择题在不同框架里的写法差异
图 4:各框架在开发体验、状态维护与生态适用场景上的综合选型矩阵

六、总结

实现一个基础选择题组件,其核心本质在于管理选项的排他性选择状态,并在校验阶段提供确定性的视觉反馈。通过横向对比可以发现:

  • 原生 JavaScript 适合对加载体积有极限要求、不需要复杂工程化脚手架的静态说明文档嵌入场景,但需要开发者严格自行维护状态与 DOM 的对应关系;
  • Vue 3 凭借 v-model 和响应式系统,在表单与选择类组件开发上提供了最符合直觉的心智体验,是各类中后台题库与交互式学习平台的首选;
  • React 严谨的单向数据流与受控组件哲学,使得选择题在应对题目暂存、时间旅行调试、多题型混合嵌套与复杂答题卡联动时具备更高的稳健性与可预测性;
  • Svelte 独创的 bind:group 与无运行时虚拟 DOM 特性,在保持极简语法的同时输出了极小体积的高性能代码,非常适合轻量级答题插件与移动端轻页面。

在实际工程中,遵循语义化的原生单选标签结构、以数据状态为唯一真实来源派生样式、并在提交阶段做好防抖与异常拦截,是编写任何高质量选择题组件不可或缺的核心基石。

作者最新文章
相关文章
更多