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

遍历数据时生成多数组而非单数组的JS Firebase问题排查

问题:预约管理项目中数据重复渲染问题排查

我正在学习JavaScript,开发一个预约管理学习项目。从Firebase获取数据后存入数组对象按日期排序,但页面显示的条目远多于数据库实际的10条记录。执行console.log(allRecords)时得到10个数组而非单个数组,相关代码及数据库结构如下:

// Showing appointments of the current and previous month.
let startDateArr = []
let startMonthArr = []
let allRecords = []
let update = false

const showingApts = (doc) => {

  let newTime = new Date(doc.data().dateInMills.at(-1) * 1000)

  allRecords.push({
    id: doc.id,
    name: doc.data().aptName,
    slot: doc.data().aptTimeSlot,
    email: doc.data().aptEmail,
    day: doc.data().aptDay.at(-1),
    type: doc.data().aptType,
    date: doc.data().aptStartDate.at(-1),
    month: doc.data().aptStartMonth.at(-1),
    year: doc.data().aptStartYear.at(-1),
    status: doc.data().appointmentStatus,
    softDelete: doc.data().softDelete,
    time: newTime
  })

  allRecords.sort(function(a, b) {
    return a.time - b.time
  })

  // Fetching names from profiles db.
  db.collection('profiles').onSnapshot((querySnapshotProfiles) => {
    querySnapshotProfiles.forEach((profileDoc) => {
      if (profileDoc.data().aptEmail.at(-1) === doc.data().aptEmail) {
        startDateArr.push(doc.data().aptStartDate.at(-1))
        startMonthArr.push(doc.data().aptStartMonth.at(-1))
      }
    })
  })

  setTimeout(() => {
    let aptStartDate = startDateArr.at(-1)
    let aptStartMonth = startMonthArr.at(-1)

    if (doc.data().aptStartDate.at(-1) === aptStartDate && doc.data().aptStartMonth.at(-1) === aptStartMonth) {
      return update = true
    }

    for (let record of allRecords) {

      let currentMonthAppointments = `
    
    <div class="flex flex-col justify-center align-middle tableRow12">
                <div class="grid grid-cols-6 text-center py-4 place-items-center text-xs border-b border-gray-200  hover:bg-blue-100 ease-in-out duration-300 text-blue-600 font-semibold px-2" data-id="${ record.id }">
               
                <span>
                  <span class='scheduleName'>${ record.name } </span>
                  <span class='scheduleEmail block text-[10px] text-gray-400 font-medium'>${ record.email } </span>
                  <div class='text-center w-full mt-2 font-medium'><button class='addMoreApt ${ update === true ? 'inline' : 'hidden' } text-center rounded-md bg-rose-600 p-2 text-white cursor-pointer text-[10px] '>Add 5 more appointments</button></div>
                </span>
                <span>${ record.slot }</span>
                <span>
                  <span> ${ record.day }, </span>
                  <span>${ record.date }-${ record.month }-${ record.year } </span>
                </span>
                <span>${ record.type }</span>

              </div>
              </div>
    `
      scheduleTableRows.innerHTML += currentMonthAppointments
    }
  }, 1000)
}

// Clearing old rows in the table
const rowsToDelete = () => {
  let tableRowsToDelete = document.querySelectorAll('.tableRow12')
  for (let rowsToDelete of tableRowsToDelete) {
    rowsToDelete.remove()
  }
}

数据库结构

  • 预约记录结构:All records
  • Profile数据结构:Db2

问题原因

  • 全局数组未重置,重复累加:allRecords是全局变量,每次调用showingApts都会往数组里追加新数据,没有在每次数据更新时清空,导致数组持续膨胀。
  • 嵌套实时监听重复触发:在showingApts内部嵌套profiles集合的onSnapshot实时监听,每条预约数据都会触发一次profile监听,多次执行startDateArr和startMonthArr的push操作,进一步加剧数据冗余。
  • 渲染逻辑错误:循环追加所有历史数据:每次showingApts执行后,setTimeout内会遍历整个allRecords数组并将内容追加到DOM,而非仅渲染当前新增记录。比如处理第1条数据时渲染10条,处理第2条时渲染11条,最终总条数为10+9+...+1=55条,远多于实际的10条。

解决方法

1. 重置全局数组,避免累加

在预约数据的主查询回调开头,清空所有全局数组,确保每次处理都是基于最新的数据库数据:

// 假设主查询入口是这样,在onSnapshot回调开头重置数组
db.collection('appointments').onSnapshot(querySnapshot => {
  allRecords = [];
  startDateArr = [];
  startMonthArr = [];
  querySnapshot.forEach(showingApts);
  // 统一调用渲染函数,不在showingApts内单独渲染
  renderAppointments();
});

2. 分离数据获取与渲染,取消嵌套监听

提前缓存profile数据,避免在showingApts内嵌套实时监听,减少重复触发:

// 提前缓存所有profile数据
let profileCache = {};
db.collection('profiles').get().then(querySnapshot => {
  querySnapshot.forEach(doc => {
    const email = doc.data().aptEmail.at(-1);
    profileCache[email] = {
      startDate: doc.data().aptStartDate.at(-1),
      startMonth: doc.data().aptStartMonth.at(-1)
    };
  });
});

// 修改showingApts,从缓存取数据
const showingApts = (doc) => {
  let newTime = new Date(doc.data().dateInMills.at(-1) * 1000);
  const aptEmail = doc.data().aptEmail;
  const profileData = profileCache[aptEmail];

  allRecords.push({
    id: doc.id,
    name: doc.data().aptName,
    slot: doc.data().aptTimeSlot,
    email: aptEmail,
    day: doc.data().aptDay.at(-1),
    type: doc.data().aptType,
    date: doc.data().aptStartDate.at(-1),
    month: doc.data().aptStartMonth.at(-1),
    year: doc.data().aptStartYear.at(-1),
    status: doc.data().appointmentStatus,
    softDelete: doc.data().softDelete,
    time: newTime
  });

  if (profileData) {
    startDateArr.push(profileData.startDate);
    startMonthArr.push(profileData.startMonth);
  }

  allRecords.sort((a, b) => a.time - b.time);
};

3. 统一渲染逻辑,避免重复追加

单独编写渲染函数,每次渲染前先清空旧行,再生成完整的DOM内容赋值给容器,而非追加:

const renderAppointments = () => {
  rowsToDelete(); // 先清空旧行
  let html = '';
  allRecords.forEach(record => {
    // 基于record信息判断update状态,替代全局变量
    const update = startDateArr.includes(record.date) && startMonthArr.includes(record.month);
    html += `
      <div class="flex flex-col justify-center align-middle tableRow12">
        <div class="grid grid-cols-6 text-center py-4 place-items-center text-xs border-b border-gray-200 hover:bg-blue-100 ease-in-out duration-300 text-blue-600 font-semibold px-2" data-id="${ record.id }">
          <span>
            <span class='scheduleName'>${ record.name } </span>
            <span class='scheduleEmail block text-[10px] text-gray-400 font-medium'>${ record.email } </span>
            <div class='text-center w-full mt-2 font-medium'>
              <button class='addMoreApt ${ update ? 'inline' : 'hidden' } text-center rounded-md bg-rose-600 p-2 text-white cursor-pointer text-[10px] '>Add 5 more appointments</button>
            </div>
          </span>
          <span>${ record.slot }</span>
          <span>
            <span> ${ record.day }, </span>
            <span>${ record.date }-${ record.month }-${ record.year } </span>
          </span>
          <span>${ record.type }</span>
        </div>
      </div>
    `;
  });
  scheduleTableRows.innerHTML = html; // 直接赋值,而非追加
};

4. 移除不必要的setTimeout

通过缓存profile数据和统一渲染逻辑,无需再用setTimeout等待数据,确保数据就绪后直接渲染。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 04:15:38