简介这份资源提供了一套基于JavaScript的手机号批量滚动抽奖实现代码面向需要为年会、晚会或选秀类活动制作现场抽奖环节的前端开发者与活动策划人员。核心解决随机抽取且不重复的中奖号码问题通过Math.random生成随机索引、维护已选集合避免重复、setInterval模拟滚动效果并支持设定中奖人数与奖项等级。资源包共1个文件为PDF格式大小约56KB内容涵盖完整实现思路与可参考的代码示例包括HTML界面结构、样式布局及抽奖逻辑脚本。已有368人学习下载适合希望快速搭建抽奖页面、理解随机不重复算法与定时器滚动机制的读者参考也可作为后台控制中奖号码、防止前端篡改的实践思路来源。1. 手机号批量滚动抽奖从年会大屏到营销活动的通用交互方案年会现场最怕什么不是奖品不够是抽奖环节卡住——大屏上几百个手机号滚成一团主持人喊停结果页面白屏或者号码重复。我做过好几场类似的活动页面手机号批量滚动抽奖这个需求看着简单实际上手会发现三个核心问题号码怎么高效渲染不卡顿、滚动动画怎么做得有节奏感、抽中之后怎么保证不重复且能追溯。这套方案适合前端开发者、活动运营技术支持以及需要快速交付抽奖页面的团队。它不依赖任何后端框架纯 JavaScript 就能跑数据源可以是接口返回的数组也可以是本地写死的测试数据。下面从数据结构设计开始一步步把可复现的代码和参数调优讲清楚。2. 滚动抽奖的底层逻辑为什么不能直接操作 DOM 节点2.1 手机号列表的渲染策略选择很多人第一反应是把所有手机号塞进一个div里用setInterval改textContent。号码少的时候没问题一旦超过 200 个滚动起来就开始掉帧。原因在于每次更新都触发重排和重绘浏览器主线程被占满。我一般会分两层处理可视区域只保留固定数量的号码槽位比如 5 到 7 个滚动时只更新这几个槽位的文本内容而不是移动整个列表。这样 DOM 节点数量恒定性能稳定。具体做法是创建一个容器内部放固定数量的span或div作为号码位。每个位有自己的索引滚动时通过定时器依次改变每个位的textContent。手机号从数据源数组中按顺序取取到末尾就回到开头形成循环。这种方案的好处是无论数据源有 100 条还是 10000 条DOM 结构不变渲染开销恒定。数据源格式建议统一成对象数组每个对象至少包含phone和id两个字段。id用于后续去重和记录中奖结果phone用于展示。如果手机号需要脱敏可以在渲染前做一次映射比如138****1234但抽奖逻辑仍然基于原始号码避免脱敏后无法区分。// 数据源示例模拟 500 个手机号 const phoneList Array.from({ length: 500 }, (_, i) ({ id: user_${i 1}, phone: 138${String(i).padStart(8, 0)}, // 仅用于演示实际从接口获取 })); // 可视区域槽位数量建议 5-7 个太多会显得拥挤 const SLOT_COUNT 5; // 初始化槽位 DOM function initSlots(container) { container.innerHTML ; for (let i 0; i SLOT_COUNT; i) { const slot document.createElement(div); slot.className phone-slot; slot.textContent ---; container.appendChild(slot); } return Array.from(container.children); }上面代码里SLOT_COUNT控制可视号码数量initSlots负责生成固定槽位。注意槽位不要用v-for或循环创建后频繁删除保持引用不变后续只改文本。2.2 滚动动画的定时器与速度曲线滚动效果的核心是定时器频率和每次更新的偏移量。最简单的做法是setInterval每 50 毫秒更新一次所有槽位但这样滚动看起来是匀速的缺乏“抽奖感”。我一般会用变速策略启动时慢中间快停止前再慢下来。实现方式是用requestAnimationFrame配合时间戳计算当前速度系数或者用setTimeout递归每次动态调整间隔。一个可复现的参数组合初始间隔 120ms每次递减 5ms最低降到 30ms持续 2 秒后开始减速每次递增 8ms直到间隔超过 200ms 时停止。这样总时长大约 3.5 秒节奏感明显。注意减速阶段不要突然停否则视觉上很生硬。// 滚动控制对象 const roller { timer: null, speed: 120, // 当前间隔 ms minSpeed: 30, // 最快间隔 maxSpeed: 200, // 最慢间隔 phase: idle, // idle | accelerating | decelerating startTime: 0, }; // 更新所有槽位文本 function updateSlots(slots, data, offset) { slots.forEach((slot, index) { const item data[(offset index) % data.length]; slot.textContent item.phone; }); } // 启动滚动 function startRoll(slots, data) { let offset 0; roller.phase accelerating; roller.speed 120; roller.startTime Date.now(); function step() { offset (offset 1) % data.length; updateSlots(slots, data, offset); const elapsed Date.now() - roller.startTime; if (roller.phase accelerating) { roller.speed Math.max(roller.minSpeed, roller.speed - 5); if (elapsed 2000) { roller.phase decelerating; roller.speed roller.minSpeed; } } else if (roller.phase decelerating) { roller.speed Math.min(roller.maxSpeed, roller.speed 8); if (roller.speed roller.maxSpeed) { stopRoll(); return; } } roller.timer setTimeout(step, roller.speed); } step(); } function stopRoll() { clearTimeout(roller.timer); roller.timer null; roller.phase idle; }这段代码里offset是当前数据索引偏移量每次加 1 并取模保证循环。accelerating阶段间隔从 120ms 降到 30msdecelerating阶段从 30ms 升到 200ms 后停止。实际使用时可以把2000这个加速时长做成可配置参数根据现场节奏调整。2.3 中奖号码的确定与去重滚动停止后需要从当前可视槽位中选一个作为中奖号码。常见做法是取中间槽位或者随机选一个槽位。但要注意如果直接取当前显示的号码可能会和之前中奖的号码重复。所以需要一个已中奖集合wonSet在停止时检查当前槽位号码是否已在集合中如果重复就继续滚动一小段或者直接跳过。更稳妥的方式是滚动停止后从数据源中过滤掉已中奖的号码然后随机选一个作为最终结果再把这个号码对应的槽位文本改成中奖号码。这样视觉上可能有一点点跳变但能保证不重复。如果要求视觉连贯可以在减速阶段就预先计算好目标号码让滚动自然停在这个号码上。实现方法是在开始减速时随机选一个未中奖的号码计算它距离当前 offset 的步数然后调整减速曲线让最后一步正好落在目标上。// 已中奖集合 const wonSet new Set(); // 从剩余号码中随机选一个 function pickWinner(data, wonSet) { const available data.filter(item !wonSet.has(item.id)); if (available.length 0) return null; const randomIndex Math.floor(Math.random() * available.length); return available[randomIndex]; } // 停止后处理中奖 function handleStop(slots, data) { const winner pickWinner(data, wonSet); if (!winner) { console.warn(所有号码已抽完); return; } wonSet.add(winner.id); // 将中间槽位显示为中奖号码 const middleIndex Math.floor(slots.length / 2); slots[middleIndex].textContent winner.phone; // 可以在这里触发回调比如记录日志或更新 UI console.log(中奖:, winner.phone); }pickWinner过滤掉已中奖的id保证不重复。handleStop把中奖号码显示在中间槽位并加入wonSet。注意wonSet存的是id而不是phone因为手机号可能重复虽然概率低但id是唯一标识。3. 从零搭建可运行的抽奖页面HTML 结构与样式要点3.1 最小化 HTML 骨架与容器布局一个完整的抽奖页面不需要太复杂的结构。核心是一个滚动容器、一个开始按钮、一个停止按钮或者一个按钮切换状态以及一个显示中奖记录的列表。容器用flex布局让槽位垂直排列每个槽位固定高度和行高字体用等宽字体避免数字跳动时宽度变化。div classlottery-container div classphone-roller idroller !-- 槽位由 JS 动态生成 -- /div div classcontrols button idstartBtn开始抽奖/button button idstopBtn disabled停止/button /div ul classwinner-list idwinnerList/ul /div样式上.phone-roller设置固定高度和overflow: hidden每个.phone-slot高度一致用line-height居中。字体大小建议 32px 到 48px根据屏幕宽度调整。背景用深色文字用亮色对比度高远处也能看清。.phone-roller { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 300px; overflow: hidden; background: #1a1a2e; border-radius: 12px; padding: 20px; } .phone-slot { font-family: Courier New, monospace; font-size: 36px; font-weight: bold; color: #e0e0e0; height: 60px; line-height: 60px; letter-spacing: 2px; transition: color 0.1s; } .phone-slot.active { color: #ffd700; text-shadow: 0 0 10px rgba(255, 215, 0, 0.5); }.active类用于高亮中间槽位可以在停止时给中间槽位加上这个类增强视觉反馈。注意过渡时间不要超过 0.2 秒否则会显得拖沓。3.2 按钮状态管理与事件绑定开始和停止按钮需要互斥开始后禁用开始按钮启用停止按钮反之亦然。如果只有一个按钮可以用状态变量控制文字和点击行为。事件绑定用addEventListener避免内联onclick。const startBtn document.getElementById(startBtn); const stopBtn document.getElementById(stopBtn); const rollerEl document.getElementById(roller); const winnerListEl document.getElementById(winnerList); let slots initSlots(rollerEl); let isRolling false; startBtn.addEventListener(click, () { if (isRolling) return; isRolling true; startBtn.disabled true; stopBtn.disabled false; startRoll(slots, phoneList); }); stopBtn.addEventListener(click, () { if (!isRolling) return; // 触发减速停止 roller.phase decelerating; roller.speed roller.minSpeed; // 停止后处理中奖 const checkStop setInterval(() { if (roller.phase idle) { clearInterval(checkStop); isRolling false; startBtn.disabled false; stopBtn.disabled true; handleStop(slots, phoneList); // 更新中奖列表 const winnerPhone slots[Math.floor(slots.length / 2)].textContent; const li document.createElement(li); li.textContent winnerPhone; winnerListEl.appendChild(li); } }, 100); });这里stopBtn点击后把roller.phase设为decelerating滚动逻辑会自动减速直到停止。然后用setInterval轮询phase变为idle后处理中奖。轮询间隔 100ms 足够不会造成性能负担。注意handleStop里已经调用了pickWinner并更新了槽位所以这里直接读中间槽位文本即可。3.3 数据源对接与脱敏处理实际项目中手机号通常从接口获取格式可能是{ phone: 13800138000, name: 张三 }。如果只需要展示手机号可以在渲染前做脱敏。脱敏函数用正则替换中间四位。function maskPhone(phone) { return phone.replace(/(\d{3})\d{4}(\d{4})/, $1****$2); } // 如果数据源包含姓名可以只显示手机号 const displayList phoneList.map(item ({ id: item.id, phone: maskPhone(item.phone), rawPhone: item.phone, // 保留原始号码用于记录 }));注意脱敏后的号码不能用于去重因为不同号码可能脱敏后相同概率极低但存在。所以wonSet仍然存id。如果接口返回的数据量很大比如上万条建议分页加载或者只取前 1000 条用于滚动避免内存占用过高。滚动抽奖本身不需要全量数据只需要一个足够大的池子让视觉上不重复即可。4. 避坑与排查滚动抽奖最常见的 5 个翻车现场4.1 号码滚动时闪烁或跳字现象滚动过程中槽位文字偶尔闪一下或者数字宽度不一致导致左右抖动。原因通常是字体不是等宽字体或者每次更新textContent时触发了额外的样式计算。解决方法是强制使用等宽字体并给槽位设置固定宽度和text-align: center。另外避免在滚动过程中修改槽位的className或其他样式属性只改文本。4.2 停止后中奖号码与显示不一致现象点击停止后中间槽位显示的号码和最终记录到中奖列表的号码不一样。原因可能是handleStop里重新选了中奖号码并改了槽位文本但读取时读的是改之前的文本。解决方法是让handleStop返回中奖对象而不是从 DOM 读取。或者先读取当前槽位文本再决定是否替换。4.3 定时器未清理导致内存泄漏现象页面切换或组件销毁后定时器还在跑控制台不断输出日志。原因是setTimeout递归没有在组件卸载时清除。解决方法是在组件销毁生命周期里调用stopRoll()并确保roller.timer被置空。如果是单页应用路由离开时一定要清理。4.4 手机号重复中奖现象同一个号码被抽中两次。原因可能是wonSet没有及时更新或者pickWinner过滤条件写错。检查wonSet.add是否在每次中奖后都执行以及filter里用的是id还是phone。如果数据源没有id可以用手机号作为唯一键但要确保手机号本身不重复。4.5 大量数据时滚动卡顿现象数据源超过 5000 条时滚动明显掉帧。原因可能是每次updateSlots都从大数组里取模运算虽然计算量不大但频繁的数组访问和字符串拼接会累积开销。优化方法是提前把数据源切片成固定长度的循环缓冲区比如只取前 200 条用于滚动中奖后再从全量数据里校验。或者用requestAnimationFrame替代setTimeout让浏览器在空闲时更新。5. 进阶技巧让抽奖更公平、更可控的 3 个参数5.1 用权重控制中奖概率如果活动需要某些号码中奖概率更高比如内部员工或者 VIP可以在数据源里加weight字段pickWinner时按权重随机。实现方式是计算总权重然后生成一个随机数遍历累加权重直到超过随机数。function pickWinnerWithWeight(data, wonSet) { const available data.filter(item !wonSet.has(item.id)); if (available.length 0) return null; const totalWeight available.reduce((sum, item) sum (item.weight || 1), 0); let random Math.random() * totalWeight; for (const item of available) { random - (item.weight || 1); if (random 0) return item; } return available[available.length - 1]; }weight默认为 1数值越大中奖概率越高。注意权重不要设置得过于悬殊否则容易引起现场质疑。5.2 预设中奖名单与滚动动画的配合有些场合需要内定中奖者但又不能让观众看出来。做法是提前把中奖号码放在数据源的特定位置然后在减速阶段计算步数让滚动正好停在这个号码上。具体实现在startRoll时传入一个targetId减速时计算当前offset到目标号码的距离调整减速步长使最后一步落在目标上。这个技巧需要精确控制定时器建议在测试环境多跑几次校准。5.3 多轮抽奖的状态隔离如果活动要抽多轮一等奖、二等奖、三等奖每轮之间需要重置滚动状态但已中奖集合要保留。可以把wonSet提升为全局每轮开始时重新初始化槽位和滚动参数。注意每轮的数据源可以不同比如一等奖只从特定号码池里抽。用一个配置对象管理每轮的参数避免状态混乱。const rounds [ { name: 一等奖, pool: phoneList.slice(0, 10), count: 1 }, { name: 二等奖, pool: phoneList.slice(10, 50), count: 3 }, { name: 三等奖, pool: phoneList.slice(50), count: 10 }, ]; let currentRound 0; let wonSet new Set(); function nextRound() { if (currentRound rounds.length) return; const round rounds[currentRound]; // 重置滚动状态使用 round.pool 作为数据源 // 抽完 round.count 个后 currentRound }这个结构让多轮抽奖清晰可控每轮的号码池和数量独立配置。实际使用时可以把rounds做成从接口获取方便运营调整。我自己的习惯是任何抽奖页面在上线前至少用 1000 条模拟数据跑 50 次观察有没有重复、卡顿、定时器泄漏。尤其是wonSet的清理时机一定要在每轮开始前确认状态正确。希望帮到你。本文还有配套的精品资源点击获取