Promise × 定时器全场景手写

🥇 01. 并发限制调度器(异步霸榜 No.1)

场景:你要发 100 个请求,但后端限流,每次只能发 N 个。
交互:可以在中途暂停执行,获取已执行的结果。

🤔考点:工程思维能力


🌈实现:模拟一个迷你版"浏览器资源调度器",这个调度器的核心本质,是通过「running 计数」「idx 游标」「runNext 自驱动」三者配合,实现一个动态的任务池。它保证任务源源不断执行,但同时不会超过给定的并发上限。

  1. 调用方法
javascript 复制代码
export function MyWork() {
  // 生成调度器
  const scheduler = limitRequests(tasks, 3);

  function handleStart() {
    scheduler.start().then((res) => {
      console.log("所有任务完成!");
      console.log("结果:", res);
    });
  }

  function handleEnd() {
    console.log("暂停完成执行~");
    scheduler.stop();
  }

  return (
    <div>
      <button onClick={handleStart}>开始</button>
      <button onClick={handleEnd}>暂停</button>
    </div>
  );
}
  1. 自定义调度器
javascript 复制代码
export function limitRequests(tasks, limit) {
  const res = [] // 存所有任务返回的 Promise,用来最终 Promise.all
  let idx = 0 // 当前处理到第几个任务
  let running = 0 // 当前正在执行的任务数量(关键的并发控制变量)
  let stopped = false // 用于标识是否已停止

  // 暴露的停止执行的方法
  function stop() {
    stopped = true
  }

  function start() {
    return new Promise((resolve, reject) => {
      function runNext() {
        // 执行队列处理完毕或者已暂停,返回结果
        if (running === 0 && stopped) {
          return resolve(Promise.all(res))
        }

        // 正在执行的任务数量不超过单次限制,存在未执行的任务
        while (running < limit && idx < tasks.length) {
          // 如果停止标志为 true,阻止新的任务加入
          if (stopped) {
            return
          }
          // 获取当前任务并执行
          const cur = tasks[idx++]()
          res.push(cur)
          running++
          cur.then(() => {
            running--
            runNext()
          }).catch(reject)
        }
      }

      runNext()
    })
  }

  return { stop, start }
}
  1. 模拟异步方法、准备数据
javascript 复制代码
// 创建100个任务
export const tasks = Array.from({ length: 100 }, (_, i) => () => fetchData(i))

// 模拟异步请求方法
export function fetchData(id: number) {
  return new Promise(resolve => {
    const time = Math.random() * 2000
    console.log(`开始任务: ${id}`)

    setTimeout(() => {
      console.log(`完成任务: ${id}`)
      resolve(id)
    }, time)
  })
}
  1. 自定义hook
ini 复制代码
const useLimitRequests = (tasks: any[], limit: number)=> {
  const resultRef = useRef<number[]>([]); // 用 useRef 存储任务结果,避免重新渲染
  const isStop = useRef<boolean>(false);
  const idx = useRef<number>(0);
  const reunning = useRef<number>(0);

  const onStop = useCallback(() => {
    isStop.current = true;
  },[]);

  const onStart = useCallback(() => {
    return new Promise((resolve, reject) => {
      function nextRun(){
        if(reunning.current === 0 && isStop.current) {
          return resolve(Promise.all(resultRef.current));
        }

        while(idx.current < tasks.length && reunning.current < limit){
          if(isStop.current){
            return;
          }

          const curTaskRes = tasks[idx.current]();
          idx.current += 1;
          resultRef.current.push(curTaskRes)
          reunning.current += 1;

          curTaskRes.then(() => {
            reunning.current -= 1;
            nextRun();
          }).catch((error: any) => reject(error))
        }
      }

      nextRun();
    })
  },[isStop, limit, tasks]);

  return { onStop, onStart};
}

🥈 02. 支持指数退避的重试(Backoff Retry)

场景:接口偶尔报错,你希望自动重试 3 次,每次等待时间翻倍。

💡 可靠性思维能力


  1. 自定义重试方法
scss 复制代码
function retry(fn, times = 3, delay = 500) {
  return new Promise((resolve, reject) => {
    const attempt = (n, d) => {
      fn().then(resolve).catch(err => {
        if (n === 0) return reject(err)
        setTimeout(() => attempt(n - 1, d * 2), d)
      })
    }
    attempt(times, delay)
  })
}
  1. 调用
javascript 复制代码
  function handleRetry() {
    retry(mockRequest, 3, 500)
      .then((result) => console.log(result)) // 如果请求成功,输出结果
      .catch((error) => console.log(error)); // 如果重试失败,输出错误
  }

🥉 03. 带超时控制的 Promise(Timeout Promise)

场景:请求超 3 秒自动失败,不等了。

🕒 超时包装器


  1. 自定义函数实现
javascript 复制代码
export function withTimeout(fn, ms){
  // 存放定时器
  let timer = null;

  // 超时函数
  const timeOut = () => new Promise((_, reject) => {
    timer = setTimeout(() => reject(new Error('超时了')), ms);
  });

  // Promise.race 会返回一个结果, fn 目标函数
  return Promise.race([fn(), timeOut()]).finally(() => {
    clearTimeout(timer);
  })
}
  1. 模拟延迟异步方法
javascript 复制代码
export function slowTask() {
  return new Promise((resolve) => {
    setTimeout(() => resolve('Task completed'), 2000); // 模拟一个 3 秒的任务
  });
}
  1. 调用
javascript 复制代码
  function handleTimeOut() {
    withTimeout(slowTask, 1000) // 设置 1 秒超时
      .then((result) => console.log(result)) // 如果任务完成,输出结果
      .catch((error) => console.log(error)); // 如果超时,输出超时错误
  }

🚢 04. 串行任务:一步一步稳扎稳打

每个任务会按顺序一个接一个地执行,直到上一个任务完成后,才会开始下一个任务

📌 场景:分片上传、表单分步骤提交


  1. 自定义方法
javascript 复制代码
export async function runInSequence(tasks){
  const result = [];

  for (const task of tasks) {
    const res = await task();
    result.push(res);
  }

  return result;
}
  1. 模拟异步请求
typescript 复制代码
export const fetchData = (task: any) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`Task ${task} completed`);
      resolve(`Result of ${task}`);
    }, 1000); // 每个任务延迟 1 秒
  });
};

// 定义任务列表
export const tasks = [
  () => fetchData('task1'),
  () => fetchData('task2'),
  () => fetchData('task3')
];
  1. 调用
javascript 复制代码
async function handleEquence(){
    const result = await runInSequence(tasks);
    console.log('All tasks completed', result);
  }

⌚️ 05. Promise 版"多方等待 ready"机制

这个机制用于让多个任务或组件等待某个条件(如 ready() 方法被调用)满足后再继续执行

应用场景

  • 多个任务依赖同一个条件:比如,多个组件在等待某个数据加载完成后再开始执行某个操作

  • 等待多个异步任务的准备:多个异步任务可能依赖某个资源,只有当该资源准备好时,才能继续执行后续操作。

  • 协调并发任务的开始:不同的任务或组件可以等待一个共同的"开始信号",一旦信号发送,所有等待的任务就可以同时开始。


  1. 自定义class
typescript 复制代码
export class Waiter{
  queue: any[];
  readyFlag: boolean;

  constructor(){
    this.queue = []; // 所有等待的任务
    this.readyFlag = false; // 是否已经准备好
  }

  wait(){
    // 条件已经准备好了,直接返回一个已解决的 Promise
    if(this.readyFlag) {
      return Promise.resolve();
    } else {
      // 将该任务的 Promise 放入 queue 队列中,等待
      return new Promise((r) => this.queue.push(r));
    }
  }


  ready(){
    this.readyFlag = true; // 设置条件已准备好
    this.queue.forEach(r => r()); // 遍历队列并触发所有等待的任务
    this.queue = []; // 清空队列
  }
}
  1. 调用
scss 复制代码
function handleReady() {
    const waiter = new Waiter();

    // 任务 1:等待条件准备好后执行
    waiter.wait().then(() => console.log("Task 1 completed"));

    // 任务 2:等待条件准备好后执行
    waiter.wait().then(() => console.log("Task 2 completed"));

    // 任务 3:等待条件准备好后执行
    waiter.wait().then(() => console.log("Task 3 completed"));

    // 在 2 秒后,调用 `ready()`,表示条件准备好,所有任务可以执行
    setTimeout(() => {
      waiter.ready(); // 调用 ready,触发所有等待的任务
    }, 2000);
  }

06. 可暂停 / 恢复的 setInterval(轮询神器)

可以启动、暂停和恢复一个定时任务,而无需重启整个定时器

🌡️ 场景:页面隐藏暂停轮询,返回恢复


  1. 自定义class
kotlin 复制代码
export class PausableInterval{
  delay: number;
  fn: any;
  timer: any;
  running: boolean;

  constructor(fn: any, delay: number){
    this.fn = fn            // 定时任务函数
    this.delay = delay      // 定时器的间隔时间(单位:毫秒)
    this.timer = null       // 存储定时器的标识符
    this.running = false    // 标记定时器是否正在运行
  }

  start(){
    // 如果定时器已经在运行,直接返回,不做重复启动
    if(this.running) return;

    this.running = true;

    const tick = () => {
      if (!this.running) return // 如果定时器已暂停,则不再继续执行
      this.fn(); // 执行定时任务
      this.timer = setTimeout(tick, this.delay) // 使用 setTimeout 模拟 setInterval
    }

    tick();
  }

  pause() {
    clearTimeout(this.timer);
    this.running = false;
  }

  resume(){
    this.start()
  }
}
  1. 模拟请求
javascript 复制代码
function printMessage() {
  console.log("Task is running...");
}

export const pausableInterval = new PausableInterval(printMessage, 1000);
  1. 调用
scss 复制代码
  function handleStartInterval() {
    pausableInterval.start();

    // 停止定时器
    setTimeout(() => {
      console.log("Pausing the task...");
      pausableInterval.pause();
    }, 3000); // 3秒后暂停

    // 恢复定时器
    setTimeout(() => {
      console.log("Resuming the task...");
      pausableInterval.resume();
    }, 5000); // 5秒后恢复
  }

07. 带最大等待 maxWait 的防抖(搜索框的神)

用于优化那些频繁触发的事件,特别是在搜索框、输入框或滚动等高频率操作中,常常用来减少不必要的计算或请求

💡 场景:搜索请求太多?一招治愈


  1. 自定义方法
javascript 复制代码
function debounce(fn, delay, { maxWait = 0 } = {}) {
  let timer = null; // 存放定时器
  let start = null; // 第一次调用时间

  return function (...args) {
    const now = Date.now();  // 获取当前时间戳
    if (!start) start = now; // 记录第一次调用的时间

    clearTimeout(timer);  // 清除之前的定时器,避免多次触发

    const run = () => { 
      start = null;  // 重置 `start`,表示已经执行过操作
      fn.apply(this, args);  // 执行函数,并传入当前的 `this` 和参数
    };

    // 如果到达 `maxWait` 时间,强制执行 `fn`;否则继续延迟执行
    if (maxWait && now - start >= maxWait) run(); 
    else timer = setTimeout(run, delay);  // 在 `delay` 时间后执行
  };
}
  1. 模拟短期内多次触发
scss 复制代码
function searchQuery(query) {
  console.log("Searching for:", query);
}

const debouncedSearch = debounce(searchQuery, 500, { maxWait: 2000 });

// 模拟用户输入
debouncedSearch("apple");
debouncedSearch("app");
debouncedSearch("appl");
debouncedSearch("apple pie");

08. 可取消的异步任务(不要让旧任务留着捣乱)

场景:页面切换后取消 pending 的 loading。

javascript 复制代码
function cancellable(fn, delay) {
  let timer
  const p = new Promise(resolve => {
    timer = setTimeout(() => resolve(fn()), delay)
  })
  return { promise: p, cancel: () => clearTimeout(timer) }
}

9. 时间窗口限流(搜索框请求合并)

🌈 场景:500ms 内所有输入合并一次请求,节省带宽又快。

javascript 复制代码
function createWindowRequester(fn, ms) {
  let timer = null
  let queue = []

  return function (...args) {
    return new Promise(resolve => {
      queue.push({ args, resolve })

      if (!timer) {
        timer = setTimeout(async () => {
          const batch = [...queue]
          queue = []
          timer = null

          const res = await fn(batch.map(i => i.args))
          batch.forEach((item, i) => item.resolve(res[i]))
        }, ms)
      }
    })
  }
}

10. 带优先级任务调度(Mini Scheduler)

场景:动画、后台任务、预加载策略。

kotlin 复制代码
class Scheduler {
  constructor() {
    this.queue = []
    this.running = false
  }
  add(fn, priority = 0) {
    this.queue.push({ fn, priority })
    this.queue.sort((a, b) => b.priority - a.priority)
    this.run()
  }
  async run() {
    if (this.running) return
    this.running = true
    while (this.queue.length) {
      const job = this.queue.shift()
      await job.fn()
    }
    this.running = false
  }
}

12. 并行预加载 + 串行渲染(列表加载体验优化)

🎨 场景:图片墙"先加载、再有序渲染"。

javascript 复制代码
async function preloadAndRender(urls, render) {
  const preloads = urls.map(url => fetch(url).then(r => r.blob()))
  for (let i = 0; i < preloads.length; i++) {
    const data = await preloads[i]
    render(data, i)
  }
}
相关推荐
恋猫de小郭2 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅10 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端