dodola · blog

前端流量控制常用手段

1,507 words 5 min read #前端#JavaScript#流量控制#性能优化 前端 前端性能优化 1/3

[!abstract]+ 问题描述 如何解决页面请求接口的大规模并发问题?

在需要处理大规模请求的情境中,做好流量控制可以提升系统稳定性和性能。

防抖/节流

防抖(Debounce)

在事件触发后,延迟执行函数,若在延迟期间再次出发,则重新计时,如在搜索框输入、调整窗口大小时。

实现

function debounce(fn, wait) {
let timeout;
return function () {
let context = this;
let args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function () {
fn.apply(context, args);
}, wait);
};
}
const sample = function () {
console.log("xxx");
};
window.addEventListener("resize", debounce(sample, 300));

为什么在  debounce  函数中使用  let

在  debounce  函数中,timeout  变量需要满足以下条件:

  1. 块级作用域timeout  只需要在  debounce  函数内部有效,不需要泄漏到外部作用域。
  2. 可重新赋值:每次调用返回的函数时,timeout  需要被重新赋值(通过  clearTimeout  和  setTimeout)。
  3. 不需要提升timeout  不需要在声明前访问,因此不需要  var  的提升行为。

let  完美符合这些需求:

  • 它提供了块级作用域,避免变量泄漏。
  • 它允许重新赋值,适合存储定时器 ID。
  • 它不会提升,避免了潜在的逻辑错误。

如果使用  var

  • timeout  会泄漏到外部作用域,可能导致意外行为。
  • 虽然可以重新赋值,但作用域规则不如  let  清晰。

如果使用  const

  • timeout  不能被重新赋值,无法满足  debounce  的逻辑需求。

[!tip]+ var、let和const 作用域区别var是函数作用域,letconst是块级作用域。

变量升级var可以升级(初始值是undefined),letconst不能变量升级,是暂时性死区。

变量提升指的是在代码执行前,js 引擎将变量和函数的声明提示到作用域的顶部,也就是说可以在声明之前使用变量或函数,但赋值操作会保留在原位置。

Example:变量

console.log(a); // 输出: undefined
var a = 10;
console.log(a); // 输出: 10

实际执行顺序:

var a;
console.log(a); // 输出: undefined
a = 10;
console.log(a); // 输出: 10

Example:函数

foo(); // 输出: "Hello"
function foo() {
console.log("Hello");
}

实际执行顺序:

function foo() {
console.log("Hello");
}
foo(); // 输出: "Hello"

重新赋值varlet可以重新赋值,const不可以。

适用场景

var:旧代码、全局变量

let:块级作用域、需要重新赋值

const:常量、不需要重新赋值

节流(Throttle)

在规定时间内,函数只执行一次,多余触发被忽略,适用于滚动事件、按钮点击等情景。

实现

function throttle(fn, limit) {
let inthrottle;
return function () {
let context = this;
let args = arguments;
if (!inthrottle) {
fn.apply(context, args);
inthrottle = true;
setTimeout(() => {
inthrottle = false;
}, limit);
}
};
}
const sample = function () {};
window.addEventListener("scroll", throttle(sample, 2000));

注意事项

  • 这两个函数都返回一个新的函数,这个新函数会包装传入的原始函数,并根据防抖或节流的逻辑来调用它。
  • 防抖和节流的区别在于,防抖是在事件触发后等待一段时间再执行,而节流是确保事件触发后的一段时间内只执行一次。
  • 这两个函数都可以接受任意数量的参数,并将它们传递给原始函数。

请求队列

所用算法:滑动窗口

每次只处理长度为maxConcurrent的窗口内的事件。

实现

class RequestQueue {
constructor(maxConcurrent) {
this.maxConcurrent = maxConcurrent; // 最大并发数
this.queue = []; // 请求队列
this.currentlyRunning = 0; // 当前正在运行的请求数
}
add(request) {
return new Promise((resolve, reject) => {
this.queue.push({ request, resolve, reject });
this.processQueue();
});
}
processQueue() {
if (this.queue.length > 0 && this.currentlyRunning < this.maxConcurrent) {
const { request, resolve, reject } = this.queue.shift();
this.currentlyRunning++;
request()
.then(resolve)
.catch(reject)
.finally(() => {
this.currentlyRunning--;
this.processQueue();
});
}
}
}
function fetchData(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`Data from ${url}`);
}, 1000);
});
}
const urls = ["url1", "url2", "url3", "url4", "url5"];
const requests = urls.map((url) => () => fetchData(url));
const myRequestQueue = new RequestQueue(2);
Promise.all(requests.map((request) => myRequestQueue.add(request)))
.then((data) => console.log(data))
.catch((err) => console.error(err));
// 1s 后输出
// [ 'Data from url1', 'Data from url2', 'Data from url3', 'Data from url4', 'Data from url5' ]

分页加载

分批加载数据,减少单次请求量,应用于长列表、分页数据。

示例

let currentPage = 1;
const pageSize = 20;
let isLoading = false;
function loadMoreData() {
if (isLoading) {
return;
}
isLoading = true;
fetch(`/aoi/items?page=${currentPage}&limit=${pageSize}`)
.then((response) => response.json())
.then((data) => {
// 处理数据并更新页面
const container = document.getElementById("container");
data.forEach((item) => {
const div = document.createElement("div");
div.innerHTML = item.name;
container.appendChild(div);
});
currentPage++;
isLoading = false;
})
.catch((error) => {
console.error(error);
isLoading = false;
});
}
// 监听滚动事件
window.addEventListener("scroll", () => {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight) {
loadMoreData();
}
});
// 初始化加载
loadMoreData();

懒加载(lazy Load)

延迟加载非关键资源,减少初始负载,如图片、视频、长列表。

实现

document.addEventListener("DOMContentLoaded", function () {
const lazyImages = document.querySelectorAll("img.lazy");
const lazyLoad = function () {
lazyImages.forEach((img) => {
if (
img.getBoundingClientRect().top < window.innerHeight &&
img.getBoundingClientRect().bottom > 0 &&
getComputedStyle(img).display !== "none"
) {
img.src = img.dataset.src;
img.classList.remove("lazy");
}
});
};
lazyLoad();
window.addEventListener("scroll", lazyLoad);
});

请求重试

请求失败后,按策略重试,应用于网络不稳定、服务端错误等情景。

实现

function fetchWithRetry(url, options, retries = 3) {
return fetch(url, options).catch((err) =>
retries > 0
? fetchWithRetry(url, options, retries - 1)
: Promise.reject(err),
);
}

缓存

缓存请求结果,减少重复请求,应用于静态资源、频繁请求的数据。

实现

const cache = new Map();
async function fetchWithCache(url) {
if (cache.has(url)) {
return cache.get(url);
}
const response = await fetch(url);
cache.set(url, response);
return response;
}

限流

限制单位时间内的请求次数,应用于 API 调用、资源加载等场景。

实现

class RateLimiter {
constructor(limit, interval) {
this.limit = limit;
this.interval = interval;
this.queue = [];
this.times = [];
}
add(request) {
this.queue.push(request);
this.run();
}
run() {
const now = Date.now();
this.times = this.times.filter((time) => now - time < this.interval);
if (this.times.length < this.limit && this.queue.length) {
const request = this.queue.shift();
this.times.push(now);
request();
}
if (this.queue.length) {
setTimeout(() => this.run(), this.interval - (now - this.times[0]));
}
}
}

反思

封装请求队列属于前端开发主导的限制请求行为。

防抖、节流属于用户交互层面上的设计。可以查阅Lodash的实现思路。

此外还有分页、滚动加载、可视区绘制等措施。

再再此外还可以从服务器端有一些限制流量的措施,缓解高并发压力,如 Nginx 分流等。

Comments

Quiet notes for this article.