You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue2测验组件编译与挂载报错求助:模板根元素与监听问题

Vue2测验组件两处错误的修复方案

错误1:模板编译警告(组件模板必须包含且仅包含一个根元素)

原因

Vue2组件模板要求只能有一个顶级根节点,你的模板直接并列了问题容器、结果容器、错题容器三个独立div,不符合语法规范。

修复方法

将所有模板内容包裹在一个根<div>内,确保整个模板只有一个顶级元素。


错误2:mounted钩子中的TypeError(无法读取null的addEventListener属性)

原因

  1. 元素选择器不匹配:比如selectAnswer方法里用document.querySelectorAll('.answers span'),但模板里的选项span类名是answer,无.answers父容器,导致找不到元素;全局使用document.querySelector容易超出组件范围,应该用组件内的this.$el.querySelector。
  2. 初始隐藏元素无法获取:wrongAnswersBtn初始时wrongAnswers为0,被v-show隐藏,mounted阶段DOM中不存在该元素,querySelector返回null,调用addEventListener直接报错。
  3. 函数上下文错误:calculateResult用箭头函数定义,导致this不指向Vue实例,无法访问组件数据。
  4. 字段名不一致:questions数组里的正确答案字段是correctAnswer,但代码里多处写成correct_answer,判断逻辑失效。
  5. 方法变量错误:backBtn方法里的this.answer未定义,应该操作this.currentQuestion或this.counter。

修复方法

  1. 替换所有document.querySelector为this.$el.querySelector,仅在组件DOM范围内查找元素。
  2. 改用Vue原生事件绑定(@click)替代原生addEventListener,Vue会自动处理元素存在性问题,避免null报错。
  3. 将calculateResult改为普通函数,确保this指向Vue实例。
  4. 统一字段名:将questions里的correctAnswer改为correct_answer,或代码判断逻辑改为question.selected == question.correctAnswer。
  5. 修正backBtn方法的变量错误,改为操作this.currentQuestion。

修正后的完整代码

HTML部分

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

<div id="app1">
  <display-question></display-question>
</div>

JS部分

Vue.component('display-question',{
  data: function(){
    return{
      counter: 0,
      currentQuestion: 0,
      answered: 0,
      showWrongQuestion: false,
      wrongQuestions: [],
      wrongAnswers: 0,
      correctAnswers: 0,
      message: "Enter your answer here",
      WhatAnswer: "default",
      questions: [
        { 
          question: "What is 2+4 = ?",
          answer:[ '2','4','6','8' ], 
          correct_answer: 2,
          selected: null,
          sense: 0
        },
        {
          question: "What is 22+44 = ?",
          answer:[ '33','44','55','66' ],
          correct_answer: 3,
          selected: null,
          sense: 0
        }
      ]
    }
  },
  
  methods: {
    backBtn: function () {
      if (this.currentQuestion > 0) {
        this.currentQuestion -= 1;
        const nextBtn = this.$el.querySelector('.next-btn');
        if(nextBtn) nextBtn.setAttribute('disabled', '');
      }
    },
    calculateResult: function(questions) {
      let correct = 0;
      
      for(let i=0; i< questions.length; i++) {
        if(this.questions[i].selected == questions[i].correct_answer) {
          correct++;
        }
      }
      return (correct / questions.length) * 100;
    },
    selectAnswer: function(a) {
      const choice = a.currentTarget;
      const answers = this.$el.querySelectorAll('.answer');
      const nextBtn = this.$el.querySelector('.next-btn');
      
      answers.forEach(answer => {
        if(answer.classList.contains('selected')) {
          answer.classList.remove('selected');
        }
      });
      
      choice.classList.add('selected');
      this.questions[this.currentQuestion].selected = parseInt(choice.dataset.index);
      if(nextBtn) nextBtn.removeAttribute('disabled');
    },
    handleNextClick() {
      this.answered < this.questions.length ? this.answered++ : '';
      
      if(this.currentQuestion < (this.questions.length -1)) {    
        this.currentQuestion++;
        const answers = this.$el.querySelectorAll('.answer');
        answers.forEach(answer => {
          if(answer.classList.contains('selected')) {
            answer.classList.remove('selected');
          }
        });
        const nextBtn = this.$el.querySelector('.next-btn');
        if(nextBtn) nextBtn.setAttribute('disabled', '');
      } else if(this.currentQuestion >= (this.questions.length -1)) {
        this.questions.forEach( (question) => {
          if(question.selected == question.correct_answer && question.sense ==0) {
            this.correctAnswers++;
            question.sense = 1;
          } else if(question.selected != question.correct_answer && question.sense ==0) {
            this.wrongAnswers++;
            question.sense = 1;
            this.wrongQuestions.push({
              answers: question.answer,
              question: question.question,
              correct_answer: question.correct_answer,
              selected: question.selected
            });
          }
        });
        const result = this.$el.querySelector('.result');
        const questionContainer = this.$el.querySelector('.question-container');
        if(result) result.classList.add('active');
        if(questionContainer) questionContainer.classList.add('blur');
      }
    },
    handleCloseResult() {
      const result = this.$el.querySelector('.result');
      const questionContainer = this.$el.querySelector('.question-container');
      if(result) result.classList.remove('active');
      if(questionContainer) questionContainer.classList.remove('blur');
    },
    handleWrongAnswersClick() {      
      const result = this.$el.querySelector('.result');
      const wrongQuestions = this.$el.querySelector('.wrong-questions');
      if(result) result.classList.remove('active');
      if(wrongQuestions) wrongQuestions.classList.add('active');
    },
    handleShowMyResults() {
      const result = this.$el.querySelector('.result');
      const wrongQuestions = this.$el.querySelector('.wrong-questions');
      if(result) result.classList.add('active');
      if(wrongQuestions) wrongQuestions.classList.remove('active');
    }
  },
  template: `
    <div class="quiz-container">
      <div class="question-container" v-if="counter < questions.length"> 
        <h2>Question No. {{counter+1}} : {{questions[counter].question}}</h2>
        <br> 
        <span class="answer" v-for="(answer, index) in questions[currentQuestion].answer" :key="index" :data-index="index" @click="selectAnswer">{{ answer }}</span>
        <p>
          <button @click="backBtn">BACK</button>
          <button class="next-btn" disabled @click="handleNextClick">{{ currentQuestion < (questions.length -1) ? "Next" : "Result!" }}</button>  
        </p>
      </div>
      <div class="result">
        <div class="success"></div>
        <h2>You have successfully finished the quiz, and your score is:</h2>
        <h1 :class="[(Number(((correctAnswers / questions.length)*100)).toFixed(2) >= 50) ? 'green' : 'red']">{{ Number(((correctAnswers / questions.length) *100)).toFixed(2) }}%</h1>
        <small><b>{{ correctAnswers }}</b>Correct,<b>{{ wrongAnswers }}</b>Wrong</small>
        <button class="close" @click="handleCloseResult">close</button>
        <button class="show-wrong-ones" v-show="wrongAnswers > 0" @click="handleWrongAnswersClick">Wrong answers</button>
      </div>
      <div class="wrong-questions">
        <h2 v-if="wrongQuestions.length > 1">Your wrong Questions</h2>
        <h2 v-else-if="wrongQuestions.length == 1">Your wrong Question</h2>
        <div class="wrong-one" v-for="question in wrongQuestions" :key="question.question">
          <h3>{{ question.question }}</h3>
          <div class="answers-container">
            <span class="selected">{{ question.answers[question.selected] }}</span>
            <span class="correct">{{question.answers[question.correct_answer] }}</span>
          </div>
        </div>
        <button id="return-to-result" @click="handleShowMyResults">Show my result</button>
      </div>
    </div>
  `,
})

var test1 = new Vue({
  el: "#app1",
  data: {},
});

内容的提问来源于stack exchange,提问作者James Pumkin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 14:01:35