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)
  }
}
相关推荐
h***34631 小时前
MS SQL Server 实战 排查多列之间的值是否重复
android·前端·后端
本地跑没问题1 小时前
Rect深入学习
前端
北辰alk1 小时前
跨域难题终结者:Vue项目中优雅解决跨域问题的完整指南
前端
吹水一流1 小时前
为什么 SVG 能在现代前端中胜出?
前端
小皮虾1 小时前
拒绝卡顿!小程序图片本地“极速”旋转与格式转换,离屏 Canvas 性能调优实战
前端·javascript·微信小程序
小熊哥7221 小时前
一个有趣的CSS题目
前端
小时前端1 小时前
性能优化:从“用户想走”到“愿意留下”的1.8秒
前端·面试
进阶的鱼1 小时前
关于微前端框架wujie的一次企业级应用实践demo?
前端·vue.js·react.js
凯心1 小时前
React 中没有 v-model,如何优雅地处理表单输入
前端·vue.js·react.js