前端手写

35 阅读5分钟

手写

debounce 防抖

function debounce(fn, delay, immediate = false) {
  let timer = null;

  function debounced(...args){
    const context = this;
    if(timer) clearTimeout(timer);

    if(immediate && !timer) {
      fn.apply(context, args);
    }

    timer = setTimeout(() => {
      timer = null;
      if(!immediate) {
        fn.apply(context, args);
      }
    }, delay)
  }

  debounced.cancel = function(){
    if(timer) clearTimeout(timer);
    timer = null;
  }

  return debounced;
}

throttle 节流

function throttle(fn) {
  let canRun = true;

  function throttled (...args){
    const context = this;
    if(canRun) {
      canRun = false;
      fn.apply(context, args);
    }
  }

  throttled.enable = function(){
    canRun = true;
  }

  throttled.disable = function(){
    canRun = false;
  }

  return throttled;
}

深度优先搜索

function findPathInArrayTree(arrayTree, target) {

  function dfs(nodes, pathSoFar){
    for(const node of nodes) {
      const newPath = [...pathSoFar, node.value]

      if(node.value === target) return newPath;

      if(node.children && node.children.length > 0) {
        const res = dfs(node.children, newPath);
        if(res) return res;
      }
    }
    return null;
  }

  return dfs(arrayTree, []);
}

const tree = [  { value: 1, children: [] },
  { value: 2, children: [
      { value: 3, children: [] },
      { value: 4, children: [
          { value: 5, children: [] }
      ] }
  ] }
];

console.log(findPathInArrayTree(tree, 5));

定时器

function mySetTimeout(fn, delay){
  const start = new Date();

  function tick(){
    if(new Date() - start >= delay) {
      fn();
    } else {
      requestAnimationFrame(tick); // 用动画帧循环检查
    }
  }

  tick();
}

Promise.all

function promiseAll(promises){
  return new Promise((resolve, reject) => {
    if(!Array.isArray(promises)) {
      return reject(new TypeError('Argument must be an array'))
    }
    const results = [];
    let count = 0;

    promises.forEach((item,index) => {
      Promise.resolve(item).then(value => {
        results[index] = value;
        count += 1;
        if(count === promises.length) {
          resolve(results);
        }
      }).catch(error => {
        reject(error);
      })
    })

    if(!promises.length) {
      resolve([]);
    }
  })
}

深拷贝

function deepClone(obj, map = new WeakMap()){
  // 处理 null
  if(obj === null) return obj;

  // 处理非对象类型
  if(typeof obj !== 'object') return obj;

  // 处理循环引用(array、object可能存在循环引用)
  if(map.has(obj)) return map.get(obj);

  let clone = null;

  // 处理Array
  if(Array.isArray(obj)) {
    clone = [];
    map.set(obj, clone);
    for(let i=0; i<obj.length;i++) {
      clone[i] = deepClone(obj[i], map);
    }
    return clone;
  }

  // 处理正则
  if(obj instanceof RegExp ) {
    return new RegExp(obj.source, obj.flags);
  }
  // 处理Date
  if(obj instanceof Date ) {
    return new Date(obj);
  }

  // 处理Error
  if(obj instanceof Error ) {
    return new Error(obj);
  }

  // 处理Object
  if(obj instanceof Object){
    // 这里不是{}
    clone = Object.create(Object.getPrototypeOf(obj));
    map.set(obj, clone);
    for(let key of Object.keys(obj)) {
      clone[key] = deepClone(obj[key], map);
    }
    return clone;
  }
}

Tree组件

  1. 调用组件
<MyTree data={mockTreeData} />

2. Tree组件

const MyTree = (props: IMyTree) => {
  const { data, onSelect, renderLabel } = props;

  // 展开
  const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set([]));
  // 选中项
  const [selectedIds, setSelectedIds] = useState<string>();

  // 展开折叠
  const toggleExpand = useCallback(
    (id: string) => {
      const currentExpandedIds = new Set(expandedIds);
      if (currentExpandedIds.has(id)) {
        currentExpandedIds.delete(id);
      } else {
        currentExpandedIds.add(id);
      }
      setExpandedIds(currentExpandedIds);
    },
    [expandedIds]
  );

  const toggleSelect = useCallback(
    (id: string) => {
      if (selectedIds === id) {
        return;
      }
      setSelectedIds(id);
      onSelect?.(id);
    },
    [selectedIds, onSelect]
  );

  return (
    <div className="my-tree-wrapper">
      {data.length <= 0 ? (
        <div>暂无数据</div>
      ) : (
        data.map((node) => (
          <MyTreeItem
            key={node.id}
            node={node}
            level={0}
            expandedIds={expandedIds}
            toggleExpand={toggleExpand}
            selectedIds={selectedIds}
            toggleSelect={toggleSelect}
            renderLabel={renderLabel}
          />
        ))
      )}
    </div>
  );
};

3. TreeItem组件

const MyTreeItem = (props: IMyTreeItem) => {
  const {
    node,
    level,
    expandedIds,
    toggleExpand,
    selectedIds,
    toggleSelect,
    renderLabel,
  } = props;

  // 包含子元素
  const hasChildren = useMemo(
    () => node.children && node.children.length > 0,
    [node]
  );

  // 展开项
  const isExpanded = useMemo(
    () => expandedIds.has(node.id),
    [expandedIds, node]
  );

  // 选中项
  const isSelected = useMemo(
    () => selectedIds === node.id,
    [selectedIds, node]
  );

  const handleClick = useCallback(() => {
    if(hasChildren) {
      toggleExpand(node.id);
    } else {
      toggleSelect(node.id);
    }
  },[hasChildren, toggleExpand, toggleSelect, node]);

  return (
    <div className={`my-tree-item-wrapper ${isSelected ? "is-selected" : ""}`} style={{ paddingLeft: level * 16 }}>
      {/* 当前元素 */}
      <div
        className={'my-tree-item-content'}
        onClick={handleClick}
      >
        {hasChildren && (
          <span className={"my-tree-item-content-expand"}>
            {isExpanded ? "-" : "+"}
          </span>
        )}
        <span className="my-tree-item-content-label">
          {renderLabel
            ? renderLabel({ node, isExpanded, level, isSelected })
            : node.label}
        </span>
      </div>
      {/* 子元素 */}
      {hasChildren && isExpanded && (
        <div className="my-tree-item-children-content">
          {node.children?.map((child) => (
            <MyTreeItem
              key={child.id}
              node={child}
              level={level + 1}
              expandedIds={expandedIds}
              toggleExpand={toggleExpand}
              selectedIds={selectedIds}
              toggleSelect={toggleSelect}
              renderLabel={renderLabel}
            />
          ))}
        </div>
      )}
    </div>
  );
};

可选中的List

  1. 调用组件
<MyList data={mockData} />

2. List组件

export default function MyList({ data }: {data: IListData[]}){
  const [checkedIds, setCheckedIds] = useState<Set<number>>(new Set([]));

  return (
    <div>
      {data.map(node => (
        <MyListItem
          key={node.id}
          node={node}
          checkedIds={checkedIds}
          setCheckedIds={setCheckedIds}
        />
      ))}
    </div>
  );
}

3. ListItem组件

export default function MyListItem(props: {
  node: IListData;
  checkedIds: Set<number>;
  parentNode?: IListData;
  setCheckedIds: (val: Set<number>) => void;
}) {
  const { node, checkedIds, setCheckedIds } = props;

  const isChecked = useMemo(() => checkedIds.has(node.id), [node, checkedIds]);

  const toggleCheck = useCallback(() => {
    if (node.children?.length) {
      // 父节点
      const childIds = new Set(node.children.map((item) => item.id));
      if (checkedIds.has(node.id)) {
        // 同时消除掉所有的子元素
        const newCheckedIds = new Set(
          [...checkedIds].filter(
            (item) => !childIds.has(item) && item !== node.id
          )
        );
        setCheckedIds(newCheckedIds);
      } else {
        const newCheckedIds = new Set([...checkedIds, node.id, ...childIds]);
        setCheckedIds(newCheckedIds);
      }
    } else {
      // 子节点
      const newChecked = new Set([...checkedIds]);
      if (checkedIds.has(node.id)) {
        // 取消子节点
        newChecked.delete(node.id);
      } else {
        // 选中子节点
        newChecked.add(node.id);
      }
      setCheckedIds(newChecked);
    }
  }, [checkedIds, node, setCheckedIds]);

  return (
    <div>
      <label>
        <input type="checkbox" checked={isChecked} onChange={toggleCheck} />
        {node.label}
      </label>
      {node.children && node.children.length > 0 && (
        <div style={{ paddingLeft: "12px" }}>
          {node.children.map((child) => (
            <MyListItem
              key={child.id}
              node={child}
              parentNode={node}
              checkedIds={checkedIds}
              setCheckedIds={setCheckedIds}
            />
          ))}
        </div>
      )}
    </div>
  );
}

复杂工具类型

// 1️⃣ DeepPartial
// 把对象所有属性及子属性都变成可选
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

// 2️⃣ DeepReadonly
// 把对象所有属性及子属性都变成只读
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
// 3️⃣ UnionToIntersection
// 把联合类型转成交叉类型
type UnionToIntersection<U> = 
  (U extends any ? (k: U) => void : never) extends
  (k: infer I) => void ? I : never;

// 5️⃣ Flatten
// 将嵌套数组展开为一维数组类型
type Flatten<T extends any[]> = T extends [infer First, ...infer Rest]
  ? First extends any[]
    ? [...Flatten<First>, ...Flatten<Rest>]
    : [First, ...Flatten<Rest>]
  : [];

// 8️⃣ ObjectValues
// 获取对象所有值的联合类型
type ObjectValues<T> = T[keyof T];

call、apply、bind

// 替换this指针指向,传递参数
Function.prototype.myCall = function(context, ...args){
  context = context ?? globalThis; // context表示this新的指向
  fn = Symbol('fn'); // 避免重名
  context[fn] = this; // this表示调用mycall的函数, 等于 newObj[fn] = this
  const res = context[fn](...args); // newObj(...args)
  delete context[fn];
  return res;
}

Function.prototype.myApply = function(context, args) {
  context = context ?? globalThis;
  fn = Symbol('fn');
  context[fn] = this;
  const res = Array.isArray(args) ? context[fn](...args) : context[fn]();
  delete context[fn]
  return res;
}

Function.prototype.myBind = function(context, ...args){
  const fn = this;
  function boundFn(...rest){
    if(this instanceof boundFn) {
      return new fn(...args, ...rest);
    }
    return fn.apply(context, [...rest, ...args]);
  }

  boundFn.prototype = Object.create(fn.prototype);

  return boundFn;
}

第一个不重复字符(字符串 + 哈希)

function firstUniqChar(str){
  const map = new Map();

  for(let s of str) {
    let count = map.get(s) ?? 0;
    count += 1;
    map.set(s, count);
  }

  for(let i=0;i<str.length;i++) {
    if(map.get(str[i]) === 1) {
      return i;
    }
  }

  return -1;
}

console.log(firstUniqChar("lleetcode"));

判断字符串是否是一个 只考虑字母数字并忽略大小写的回文字符串

export function isPalindrome(str){
  let s = str.replace(/[^a-zA-Z0-9]/g, "");
  s = s.toLocaleLowerCase();

  if(s.length <= 1) return true;

  let leftIdx = 0;
  let rightIdx = s.length - 1;

  while(leftIdx <= rightIdx) {
    if(s[leftIdx] === s[rightIdx]) {
      leftIdx++;
      rightIdx--;
    } else {
      return false;
    }
  }

  return true;
}

console.log(isPalindrome("A++++ man, a plan, a canal: Panama"))

有效括号(栈)

function isValid(str){
  const stack = [];

  const map = {
    '(': ')',
    '{': '}',
    '[': ']'
  }

  for(let s of str){
    // 右半部分,需要出栈对比
    if(!map[s]){
      const lastS = stack.pop();
      if(map[lastS] === s){
        continue;
      } else {
        stack.push(lastS);
      }
    } else {
      stack.push(s);
    }
  }

  return stack.length <= 0;
}

Promise 并发控制

async function limitPromises(funcs, limit){
  const result = [];
  const running = [];

  for(let fn of funcs){
    const p = fn().then((res) => {
      result.push(res);
      running.splice(running.findIndex(p), 1);
    })
    running.push(p);

    if(running.length >= limit) {
      await Promise.race(running);
    }
  }

  await Promise.all(running);

  return result;
}

长度为 k 的滑动窗口,返回每个窗口最大值

function maxSlidingWindow(nums, k){
  const res = [];

  for(let i=0;i<=nums.length - k;i++) {
    const curArray = nums.slice(i, i+k);
    const maxNum = Math.max(...curArray);
    res.push(maxNum);
  }

  return res;
}

console.log(maxSlidingWindow([1,3,-1,-3,5,3,6,7], 3));

移动零

function moveZeroes(nums){
  let s=0;
  for(let f=1;f<nums.length;f++) {
    if(nums[s] === 0 && nums[f] !== 0) {
      [nums[s], nums[f]] = [nums[f], nums[s]];
      s++;
    } else if(nums[s] !== 0) {
      s++;
    }
  }
}

找出数组中两数和为目标值的下标

function twoSum(nums, target){
  const map = new Map();

  for(let i=0;i<nums.length;i++) {
    const otherNum = target - nums[i];
    if(map.has(otherNum)) {
      return [map.get(otherNum), i];
    } else {
      map.set(nums[i], i);
    }
  }
  return [];
}

Tree 查找目标值

function findPathInArrayTree(arrayTree, target) {

  function dfs(nodes, pathSoFar){
    for(const node of nodes) {
      const newPath = [...pathSoFar, node.value]

      if(node.value === target) return newPath;

      if(node.children && node.children.length > 0) {
        const res = dfs(node.children, newPath);
        if(res) return res;
      }
    }
    return null;
  }

  return dfs(arrayTree, []);
}

const tree = [  { value: 1, children: [] },
  { value: 2, children: [
      { value: 3, children: [] },
      { value: 4, children: [
          { value: 5, children: [] }
      ] }
  ] }
];

console.log(findPathInArrayTree(tree, 5));

Promise + SetTimeout

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

业务场景:要发 100 个请求,但后端限流,每次只能发 N 个

扩展交互:可以在中途暂停执行,获取执行的结果

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

  1. 业务调用
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>
  );
}

2. 创建调度器

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 }
}

3. 模拟异步方法、准备数据

// 创建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)
  })
}

4. 自定义hook实现功能

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};
}

image.png

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

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

  1. 创建重试方法
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)
  })
}

2. 业务调用

  function handleRetry() {
    retry(mockRequest, 3, 500)
      .then((result) => console.log(result)) // 如果请求成功,输出结果
      .catch((error) => console.log(error)); // 如果重试失败,输出错误
  }

image.png

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

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

  1. 自定义函数实现
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);
  })
}

2. 模拟延迟异步方法

export function slowTask() {
  return new Promise((resolve) => {
    setTimeout(() => resolve('Task completed'), 2000); // 模拟一个 3 秒的任务
  });
}

3. 业务调用

  function handleTimeOut() {
    withTimeout(slowTask, 1000) // 设置 1 秒超时
      .then((result) => console.log(result)) // 如果任务完成,输出结果
      .catch((error) => console.log(error)); // 如果超时,输出超时错误
  }

image.png

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

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

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

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

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

  return result;
}

2. 模拟异步请求

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')
];

3. 调用

async function handleEquence(){
    const result = await runInSequence(tasks);
    console.log('All tasks completed', result);
  }

image.png

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

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

业务场景: 1. 多个任务依赖同一个条件:比如,多个组件在等待某个数据加载完成后再开始执行某个操作。 2. 等待多个异步任务的准备:多个异步任务可能依赖某个资源,只有当该资源准备好时,才能继续执行后续操作。 3. 协调并发任务的开始:不同的任务或组件可以等待一个共同的“开始信号”,一旦信号发送,所有等待的任务就可以同时开始。

  1. 自定义class
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 = []; // 清空队列
  }
}

2. 调用

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);
  }

image.png

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

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

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

  1. 自定义class
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()
  }
}

2. 模拟请求

function printMessage() {
  console.log("Task is running...");
}

export const pausableInterval = new PausableInterval(printMessage, 1000);

3. 调用

  function handleStartInterval() {
    pausableInterval.start();

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

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

image.png

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

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

  1. 自定义方法
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` 时间后执行
  };
}

2. 模拟短期内多次触发

function searchQuery(query) {
  console.log("Searching for:", query);
}

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

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