在前端开发中,我们经常会遇到「大量数据渲染卡顿」「频繁事件触发导致性能损耗」「自定义定时逻辑」等问题,今天就来拆解7个高频实用的JS代码片段,用「通俗话术+专业解析」的方式,讲懂每一行代码的作用、核心原理和实际应用场景,帮你吃透这些前端必备技能。
一、万条数据渲染优化:避免卡顿的核心技巧
前端渲染大量数据(比如10万条)时,直接一次性插入DOM会导致主线程阻塞,页面出现卡顿、掉帧,甚至浏览器崩溃。这段代码的核心思路是「分批渲染+文档片段+requestAnimationFrame」,从根源上减少DOM操作带来的性能损耗。
核心代码(带详细注释)
// 延迟0ms执行,让DOM先渲染完毕,避免阻塞主线程
// 这里的setTimeout(fn, 0)不是真的延迟,而是把回调放到宏任务队列,等待当前同步代码和DOM渲染完成后执行
setTimeout(() => {
// 总共需要渲染10万条数据(实际开发中可根据需求调整)
const total = 100000;
// 每一批次渲染20条,防止一次性渲染过多导致卡顿
// 批次大小可优化:一般建议20-50条,过多仍会卡顿,过少则渲染次数过多
const once = 20;
// 计算总共需要分多少批次(向上取整,避免最后一批数据不足20条被遗漏)
const loopCount = Math.ceil(total / once);
// 记录当前已经渲染了多少批次,用于判断是否渲染完成
let countOfRender = 0;
// 获取页面中的ul容器,所有li都会插入到这个容器中
const ul = document.querySelector('ul');
// 每一批次添加DOM的函数:核心是「减少DOM操作次数」
function add() {
// 创建文档片段(DocumentFragment),临时存放当前批次的li
// 重点:fragment不属于页面DOM树,向它添加子元素不会触发页面重排重绘,相当于“临时仓库”
const fragment = document.createDocumentFragment();
// 循环生成当前批次的20个li
for (let i = 0; i < once; i++) {
const li = document.createElement('li');
// 给li填充随机数字(实际开发中可替换为真实业务数据,比如列表项内容)
li.innerText = Math.floor(Math.random() * total);
// 先放入片段中,此时不触发任何页面渲染
fragment.appendChild(li);
}
// 一次性将20条li插入ul,只触发一次DOM重排(关键优化点)
// 对比:如果每次循环都appendChild到ul,会触发20次重排,性能极差
ul.appendChild(fragment);
// 已渲染批次+1,更新渲染进度
countOfRender += 1;
// 继续执行下一批渲染
loop();
}
// 控制渲染节奏:使用requestAnimationFrame,跟随浏览器刷新频率执行
// 浏览器每秒刷新约60次(16.67ms/帧),requestAnimationFrame会在每帧开始时执行回调
// 好处:避免渲染操作与浏览器刷新冲突,保证页面流畅不卡顿
function loop() {
// 如果还没渲染完所有批次,就继续下一批
if (countOfRender < loopCount) {
window.requestAnimationFrame(add);
}
}
// 启动第一轮渲染
loop();
}, 0);
关键知识点解析
-
setTimeout(fn, 0):不是延迟执行,而是将回调推入宏任务队列,确保当前同步代码和DOM初始化完成后再执行,避免DOM未挂载时查询不到ul容器。
-
分批渲染:将10万条数据拆分为5000批次(100000/20),每次只渲染20条,降低单次DOM操作的压力。
-
DocumentFragment:前端性能优化神器,作为临时DOM容器,所有操作都在内存中完成,最后一次性插入页面,只触发1次重排重绘,比直接操作真实DOM性能提升10倍以上。
-
requestAnimationFrame:与setTimeout相比,它能跟随浏览器刷新频率执行,避免“掉帧”,尤其适合大量DOM渲染、动画等场景,保证页面流畅度。
实际应用场景
大数据列表渲染(如后台管理系统的订单列表、日志列表)、长列表滚动加载(结合滚动事件,滚动到底部时加载下一批),避免一次性渲染大量数据导致页面卡死。
二、手撕防抖:解决频繁触发的“性能杀手”
防抖(Debounce)的核心逻辑:频繁触发同一事件时,只在最后一次触发后,延迟指定时间执行回调函数。比如搜索框输入、窗口resize、滚动事件,频繁触发会导致性能损耗,防抖能有效“合并”触发次数。
1. 基础版防抖(延迟执行)
// 防抖:频繁触发时,只在**最后一次触发后延迟执行**
function debounce(callback, wait) {
// 定时器标识:用闭包保存,避免污染全局变量,且能在多次调用中共享状态
let timer = null;
// 返回一个可调用的包装函数,接收原函数的参数
return function (...args) {
// 保存原this(解决事件回调中this指向window的问题,比如btn点击事件中this应指向btn)
const context = this;
// 再次触发时,清除之前的定时器 → 重新计时(核心:取消上一次的延迟执行)
if (timer) clearTimeout(timer);
// 新建定时器:延迟 wait 毫秒后执行回调
timer = setTimeout(() => {
// 恢复原函数的this指向和参数,保证回调函数执行时上下文正确
callback.apply(context, args);
// 执行完清空timer,方便垃圾回收,避免内存泄漏
timer = null;
}, wait);
};
}
2. 完整版防抖(支持立即执行/延迟执行)
基础版防抖是“延迟执行”(触发后等待wait时间再执行),但实际开发中有时需要“立即执行”(第一次触发就执行,之后频繁触发不执行,直到wait时间后才可再次执行),比如按钮点击防重复提交。
// 完整版防抖:支持 立即执行(immediate=true) / 延迟执行(immediate=false)
function debounce(callback, wait, immediate) {
let timer = null; // 闭包缓存定时器,共享状态
return function () {
// 每次进入先清除上一次定时器 → 重新计时(无论立即还是延迟,都要取消上一次)
if (timer) clearTimeout(timer);
// ========== 立即执行模式 ==========
if (immediate) {
// timer为null时表示可以立即执行(首次触发或wait时间已过)
const callNow = !timer;
// 设置定时器:wait时间后把timer置空,解锁“立即执行”权限
// 作用:这段时间内再次触发,callNow会为false,不会执行回调
timer = setTimeout(() => {
timer = null;
}, wait);
// 满足立即执行条件时,调用原函数,恢复this和参数
if (callNow) {
callback.apply(this, arguments);
}
// ========== 常规延迟执行模式 ==========
} else {
// 延迟wait时间执行,每次触发都重置定时器
timer = setTimeout(() => {
callback.apply(this, arguments);
timer = null; // 执行后清空,垃圾回收
}, wait);
}
};
}
关键知识点解析
-
闭包的作用:保存timer变量,让多次触发的事件能共享同一个定时器标识,实现“清除上一次定时器”的逻辑,这是防抖的核心。
-
this指向修复:事件回调中this默认指向window(如addEventListener中的回调),通过context = this + apply(context, args),让原函数this指向正确的元素(如按钮、输入框)。
-
立即执行vs延迟执行:
-
立即执行(immediate=true):适合防重复提交(按钮点击后立即执行,wait时间内不可再次点击);
-
延迟执行(immediate=false):适合搜索框联想(输入停止后wait时间,再发送请求,避免频繁请求接口)。
-
实际应用场景
搜索框输入联想、窗口resize事件(调整窗口大小时,避免频繁计算布局)、滚动事件(滚动到底部加载更多,避免频繁触发)、按钮防重复提交。
三、手撕节流:固定频率执行,避免过度触发
节流(Throttle)的核心逻辑:频繁触发同一事件时,按照固定的时间间隔执行回调函数,无论触发多少次,都不会超过这个频率。和防抖的区别:防抖是“最后一次触发后执行”,节流是“固定频率持续执行”。
1. 立即触发版节流(停止触发后不执行最后一次)
// 节流:固定频率执行,立即触发,停止触发后不执行最后一次
function throttle(callback, wait) {
// 上一次执行回调的时间戳(初始为0,确保第一次触发能立即执行)
let previous = 0;
return function(...args) {
// 获取当前时间戳
const now = Date.now();
// 核心逻辑:当前时间 - 上一次执行时间 >= 等待时间,才执行回调
if (now - previous >= wait) {
// 执行回调,恢复this和参数
callback.apply(this, args);
// 更新上一次执行时间戳为当前时间,开始下一个周期
previous = now;
}
};
}
2. 延迟触发版节流(停止触发后仍执行最后一次)
立即触发版节流的问题:如果停止触发时,距离上一次执行已经超过wait时间,不会执行最后一次触发的回调。延迟触发版可以解决这个问题,适合需要“收尾”的场景(如滚动加载,即使停止滚动,也要执行最后一次加载逻辑)。
// 节流:固定频率执行,延迟触发,停止触发后仍执行最后一次
function throttle(callback, wait) {
let timer = null; // 用定时器控制延迟执行
return function(...args) {
const context = this;
// 核心逻辑:没有定时器才创建,不重置计时(保证固定频率)
if (!timer) {
timer = setTimeout(() => {
// 延迟wait时间执行回调
callback.apply(context, args);
// 执行后清空定时器,允许下一次创建
timer = null;
}, wait);
}
};
}
关键知识点解析
-
时间戳版(立即触发):通过对比当前时间和上一次执行时间,控制执行频率,优点是简单高效,缺点是停止触发后不会执行最后一次。
-
定时器版(延迟触发):通过定时器控制执行时机,优点是停止触发后仍会执行最后一次,缺点是首次触发会延迟wait时间才执行。
-
防抖vs节流:
-
防抖:合并多次触发,只执行最后一次(比如搜索输入);
-
节流:控制触发频率,固定间隔执行(比如滚动加载、鼠标移动绘制)。
-
实际应用场景
滚动事件(监听滚动位置,固定频率更新导航栏状态)、鼠标移动事件(绘制canvas,避免频繁重绘)、resize事件(固定频率调整页面布局)、高频点击事件(如游戏中的攻击按钮,控制点击频率)。
四、自定义定时器:可递增延迟的MySetInterval
原生setInterval的缺点:间隔时间固定,无法实现“每次执行延迟递增”的需求(比如第一次延迟100ms,第二次延迟200ms,第三次延迟300ms...)。这段代码通过类封装,实现了“基础延迟+递增步长”的自定义定时器,灵活满足复杂定时需求。
核心代码(带详细注释)
class MySetInterval {
/**
* @param {Function} fn 要执行的函数(回调函数)
* @param {number} base 基础延迟 a(第一次执行的延迟时间)
* @param {number} step 每次递增 b(每次执行的延迟比上一次多b ms)
* @param {...any} args 传递给 fn 的参数(可选)
*/
constructor(fn, base, step, ...args) {
this.fn = fn; // 要执行的回调函数
this.base = base; // 基础延迟时间(ms)
this.step = step; // 延迟递增步长(ms)
this.args = args; // 传递给回调函数的参数
this.count = 0; // 记录执行次数(用于计算当前延迟)
this.timer = null; // 定时器ID,用于停止定时器
}
// 启动定时器
start() {
// 计算当前批次的延迟:a + count * b(第一次count=0,延迟a;第二次count=1,延迟a+b,以此类推)
const delay = this.base + this.count * this.step;
// 用setTimeout模拟递归执行,实现“递增延迟”
this.timer = setTimeout(() => {
// 执行用户传入的回调函数,并传递参数
this.fn(...this.args);
// 执行次数+1,为下一次延迟计算做准备
this.count++;
// 递归调用start,启动下一次定时执行
this.start();
}, delay);
}
// 停止定时器(必须有,避免内存泄漏)
stop() {
// 清除当前定时器
clearTimeout(this.timer);
// 清空timer,方便垃圾回收,也避免重复停止
this.timer = null;
}
}
// 使用示例
const timer = new MySetInterval(() => {
console.log('自定义定时器执行');
}, 100, 50); // 第一次延迟100ms,第二次150ms,第三次200ms...
timer.start(); // 启动
// timer.stop(); // 停止(需要时调用)
关键知识点解析
-
类封装优势:通过class封装,将定时器的状态(count、timer、base等)挂载到实例上,避免全局变量污染,同时方便调用start和stop方法,逻辑更清晰。
-
递增延迟实现:通过count记录执行次数,每次执行后count+1,下一次延迟 = 基础延迟 + count * 递增步长,实现“每次延迟递增”的效果。
-
递归setTimeout:没有使用原生setInterval,而是用setTimeout递归调用start方法,避免setInterval可能出现的“时间漂移”(比如回调执行时间过长,导致下一次执行延迟偏差)。
实际应用场景
倒计时递增(比如活动倒计时,后期每秒增加延迟,营造紧迫感)、轮播图渐变(每次切换的延迟递增,实现慢放效果)、接口重试(失败后重试,每次重试延迟递增,避免频繁请求接口)。
五、重写setTimeout:用requestAnimationFrame模拟
原生setTimeout的缺点:执行时间不精确,受主线程阻塞影响(比如主线程有耗时操作,setTimeout的回调会延迟执行)。而requestAnimationFrame(rAF)会跟随浏览器刷新频率执行(16.67ms/帧),用它模拟setTimeout,能让延迟执行更精确,同时避免主线程阻塞导致的偏差。
核心代码(带详细注释)
// 用 requestAnimationFrame 模拟 setTimeout,提升执行精度
let setTimeout = (fn, timeout, ...args) => {
const start = Date.now(); // 记录定时器启动的时间戳
let timer; // 保存rAF的标识,用于取消定时器
// 循环执行函数,每帧检查是否达到设定的延迟时间
const loop = () => {
// 注册下一次rAF回调,保证循环执行,跟随浏览器刷新频率
timer = window.requestAnimationFrame(loop);
// 获取当前时间戳
const now = Date.now();
// 核心逻辑:当前时间 - 启动时间 >= 设定的延迟时间,执行回调
if (now - start >= timeout) {
// 执行用户传入的回调函数,恢复this和参数
fn.apply(this, args);
// 执行完成后,取消rAF循环,避免无限执行
window.cancelAnimationFrame(timer);
}
};
// 启动rAF循环,开始计时
window.requestAnimationFrame(loop);
};
// 使用示例(和原生setTimeout用法一致)
setTimeout(() => {
console.log('用rAF模拟的setTimeout执行');
}, 1000);
关键知识点解析
-
精度提升原理:原生setTimeout的延迟是“最小延迟”,如果主线程忙碌,回调会被推迟;而rAF每帧(16.67ms)执行一次loop,每次都检查时间差,一旦达到设定延迟就执行回调,精度更高。
-
循环终止:通过cancelAnimationFrame(timer)取消rAF循环,避免回调执行后仍继续循环,造成性能损耗。
-
用法兼容:模拟后的setTimeout用法和原生一致,无需修改现有代码,直接替换即可提升执行精度。
实际应用场景
需要精确延迟执行的场景(如动画同步、定时更新DOM)、避免主线程阻塞导致延迟偏差的场景(如复杂页面中的定时任务)。
六、模拟sleep函数:让代码“暂停”指定时间
JS中没有原生的sleep函数(即“暂停代码执行指定时间,再继续执行后面的代码”),但可以通过Promise+setTimeout模拟。核心思路:返回一个Promise,在setTimeout延迟后resolve,通过await等待Promise完成,实现代码“暂停”效果。
核心代码(带详细注释)
// 休眠函数:等待 time 毫秒后,再继续执行后面的代码
// 核心:通过Promise包裹setTimeout,用resolve触发后续代码执行
function sleep(time) {
// 返回一个Promise对象,pending状态表示“正在休眠”
return new Promise(function (resolve) {
// 定时 time 毫秒后,执行resolve(),让Promise变为fulfilled状态
// resolve()无参数,仅用于通知“休眠结束”
setTimeout(resolve, time);
});
}
// 使用示例(必须配合async/await,因为await只能在async函数中使用)
async function test() {
console.log('开始执行');
await sleep(2000); // 暂停2000ms(2秒)
console.log('2秒后执行'); // 2秒后才会打印这句话
await sleep(1000); // 再暂停1秒
console.log('再1秒后执行');
}
关键知识点解析
-
Promise的作用:用Promise包裹setTimeout,将“延迟执行”转化为“异步等待”,配合await使用,实现代码的“线性暂停”,避免回调地狱。
-
async/await依赖:sleep函数返回Promise,必须在async函数中用await调用,才能实现“暂停”效果;如果不用await,代码会继续执行,不会暂停。
-
非阻塞特性:sleep是异步暂停,不会阻塞主线程,其他异步任务(如接口请求、DOM渲染)可以在sleep期间正常执行,避免页面卡顿。
实际应用场景
代码分步执行(如引导页步骤切换,每步间隔1秒)、接口请求重试(失败后sleep1秒再重试)、模拟加载动画(sleep指定时间后隐藏加载框)。
七、版本号对比:实现语义化版本排序
在前端开发中,经常需要对版本号进行排序(如npm包版本、项目版本),版本号格式通常为“x.y.z”(如1.0.0、2.3.4、1.10.2),直接字符串排序会出现错误(如1.10.2会排在1.2.0前面),这段代码能正确对比版本号大小并排序。
核心代码(带详细注释)
// 版本号对比排序:接收版本号数组,返回按升序排列的数组
var compareVersion = function (versions) {
// 用数组的sort方法排序,核心是自定义排序规则
return versions.sort((version1, version2) => {
// 1. 将版本号按“.”分割,转为数字数组(如"1.10.2" → [1,10,2])
// map(Number):将分割后的字符串转为数字,避免字符串比较的误差
let s1 = version1.split('.').map(Number);
let s2 = version2.split('.').map(Number);
// 2. 逐位对比版本号(从左到右,依次对比主版本、次版本、修订版本)
// 循环次数取两个版本号数组的最大长度,不足的位补0(如1.0 → [1,0,0])
for (let i = 0; i < s1.length || i < s2.length; i++) {
const v1 = s1[i] || 0; // 版本1当前位的值,无则补0
const v2 = s2[i] || 0; // 版本2当前位的值,无则补0
// 若当前位不相等,直接返回差值(正数表示v1>v2,负数表示v1<v2)
if (v1 !== v2) {
return v1 - v2;
}
}
// 3. 若所有位都相等,按原字符串排序(处理版本号格式完全一致的情况)
return version1.localeCompare(version2);
})
};
// 使用示例
const versions = ['1.10.2', '1.2.0', '2.3.4', '1.0.0', '1.5'];
console.log(compareVersion(versions));
// 输出:["1.0.0", "1.2.0", "1.5", "1.10.2", "2.3.4"]
关键知识点解析
-
版本号分割与转数字:split('.')将版本号分割为数组,map(Number)转为数字数组,避免“10”作为字符串比“2”小的问题(字符串比较是按字符编码,'10' < '2')。
-
逐位对比逻辑:从左到右对比每一位版本号,先对比主版本(第一位),主版本大的版本号更大;主版本相等则对比次版本(第二位),以此类推;不足的位补0(如1.5 → 1.5.0)。
-
兜底逻辑:若所有位都相等(如1.0.0和1.0.0),用localeCompare按字符串排序,保证排序的稳定性。
实际应用场景
npm包版本排序、后台管理系统的版本日志排序、APP版本更新提示(对比当前版本和最新版本,判断是否需要更新)。
总结
以上7个代码片段,覆盖了前端开发中「性能优化」「事件处理」「定时任务」「版本对比」四大核心场景,每段代码都包含“核心逻辑+详细注释+知识点解析+实际应用”,既能直接复制使用,也能帮你理解背后的原理。
重点记住:前端性能优化的核心是「减少DOM操作」「避免频繁触发事件」「合理利用异步」,而防抖、节流、分批渲染、rAF都是实现这些目标的关键手段;自定义定时器、sleep、版本对比则是解决实际业务场景的实用工具,掌握这些,能让你的代码更高效、更健壮。