前端流量控制常用手段

系列 - 前端八股文基础
问题预设
如何解决页面请求接口的大规模并发问题?

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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  函数中,timeout  变量需要满足以下条件:

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

let  完美符合这些需求:

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

如果使用  var

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

如果使用  const

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

作用域区别var是函数作用域,letconst是块级作用域。

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

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

Example:变量

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

实际执行顺序:

1
2
3
4
var a;
console.log(a); // 输出: undefined
a = 10;
console.log(a); // 输出: 10

Example:函数

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

实际执行顺序:

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

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

适用场景

var:旧代码、全局变量

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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的窗口内的事件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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' ]

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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();

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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);
});

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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 调用、资源加载等场景。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 分流等。

相关内容