JavaScript 防抖与节流进阶:从原理到实战

当用户疯狂点击按钮、疯狂滚动页面、疯狂输入搜索关键词时,应用还能流畅运行吗?防抖(Debounce)和节流(Throttle)是应对高频事件的终极武器。本文将从源码层面深入理解它们的差异,并实现一个支持立即执行、延迟执行、取消功能、记录参数的完整版本。

前言:高频事件带来的挑战

我们先来看一些简单的场景:

javascript 复制代码
window.addEventListener('resize', () => {
    // 窗口大小改变时重新计算布局
    recalcLayout(); // 一秒可能触发几十次!
});

searchInput.addEventListener('input', () => {
    // 用户每输入一个字符就发起搜索请求
    fetchSearchResults(input.value); // 浪费大量请求!
});

window.addEventListener('scroll', () => {
    // 滚动时加载更多数据
    loadMoreData(); // 滚动一下触发几十次!
});

在这些场景中,当事件触发频率远高于我们需要的处理频率,就会出现卡顿、闪屏等现象,这就是防抖和节流要解决的核心问题。

理解防抖与节流的本质差异

核心概念对比

类型 防抖 节流
概念 将多次高频操作合并为一次,仅在最后一次操作后的延迟时间到达时执行 保证在单位时间内只执行一次,稀释执行频率
场景示例 电梯关门:等最后一个人进来后才关门,中间如果有人进来就重新计时 地铁安检:无论多少人排队,每秒钟只能通过一个人
执行次数 只执行最后一次 定期执行,不保证最后一次
频率 N次高频调用 → 1次执行 N次高频调用 → N/间隔时间次执行

适用场景对比

防抖场景

  • 搜索框输入(用户停止输入后才搜索)
  • 窗口大小调整(窗口调整完成后重新计算)
  • 表单验证(用户输完才验证)
  • 自动保存(停止编辑后保存)
  • 按钮防连点(避免重复提交)

节流场景

  • 滚动加载更多(滚动过程中定期检查)
  • 动画帧(控制动画执行频率)
  • 游戏循环(固定帧率)
  • 鼠标移动事件(实时位置但不过度频繁)
  • DOM元素拖拽(平滑移动)

防抖函数实现

基础防抖实现

javascript 复制代码
function debounce(fn, delay) {
  let timer = null;

  return function (...args) {
    // 每次调用都清除之前的定时器
    if (timer) {
      clearTimeout(timer);
    }

    // 设置新的定时器
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null;
    }, delay);
  };
}

支持立即执行的防抖

javascript 复制代码
function debounceEnhanced(fn, delay, immediate = false) {
  let timer = null;
  let lastContext = null;
  let lastArgs = null;
  let lastResult = null;
  let callCount = 0;

  return function (...args) {
    lastContext = this;
    lastArgs = args;

    // 第一次调用且需要立即执行
    if (immediate && !timer) {
      lastResult = fn.apply(lastContext, lastArgs);
      callCount++;
      console.log(`立即执行 (调用 #${callCount})`);
    }

    // 清除之前的定时器
    if (timer) {
      clearTimeout(timer);
    }

    // 设置延迟执行
    timer = setTimeout(() => {
      // 如果不是立即执行模式,或者已经执行过立即执行
      if (!immediate) {
        lastResult = fn.apply(lastContext, lastArgs);
        callCount++;
        console.log(`延迟执行 (调用 #${callCount})`);
      }

      // 清理
      timer = null;
      lastContext = null;
      lastArgs = null;
    }, delay);

    return lastResult;
  };
}

完整版防抖(支持取消、取​​消、参数记录)

javascript 复制代码
class DebouncedFunction {
  constructor(fn, delay, options = {}) {
    this.fn = fn;
    this.delay = delay;
    this.immediate = options.immediate || false;
    this.maxWait = options.maxWait || null;

    this.timer = null;
    this.lastArgs = null;
    this.lastContext = null;
    this.lastResult = null;
    this.lastCallTime = null;
    this.lastInvokeTime = null;

    // 参数历史记录
    this.history = [];
    this.maxHistory = options.maxHistory || 10;

    // 调用次数统计
    this.stats = {
      callCount: 0,
      invokedCount: 0,
      canceledCount: 0
    };
  }

  /**
   * 执行函数
   */
  _invoke() {
    const time = Date.now();
    this.stats.invokedCount++;
    this.lastInvokeTime = time;

    // 记录参数历史
    if (this.lastArgs) {
      this.history.push({
        args: [...this.lastArgs],
        timestamp: time,
        type: this.timer ? 'delayed' : 'immediate'
      });

      // 限制历史记录数量
      if (this.history.length > this.maxHistory) {
        this.history.shift();
      }
    }

    // 执行原函数
    this.lastResult = this.fn.apply(this.lastContext, this.lastArgs);

    // 清理
    this.lastArgs = null;
    this.lastContext = null;

    return this.lastResult;
  }

  /**
   * 调用防抖函数
   */
  call(...args) {
    const now = Date.now();
    this.stats.callCount++;
    this.lastArgs = args;
    this.lastContext = this;
    this.lastCallTime = now;

    // 立即执行模式处理
    if (this.immediate && !this.timer) {
      this._invoke();
    }

    // 清除现有定时器
    if (this.timer) {
      clearTimeout(this.timer);
    }

    // 最大等待时间处理
    if (this.maxWait && this.lastInvokeTime) {
      const timeSinceLastInvoke = now - this.lastInvokeTime;
      if (timeSinceLastInvoke >= this.maxWait) {
        this._invoke();
        return this.lastResult;
      }
    }

    // 设置新的定时器
    this.timer = setTimeout(() => {
      // 非立即执行模式,或者已经执行过立即执行
      if (!this.immediate) {
        this._invoke();
      }
      this.timer = null;
    }, this.delay);

    return this.lastResult;
  }

  /**
   * 取​​消当前待执行的防抖
   */
  cancel() {
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
      this.stats.canceledCount++;
    }

    this.lastArgs = null;
    this.lastContext = null;
  }

  /**
   * 立即执行并取消后续
   */
  flush() {
    if (this.lastArgs) {
      this._invoke();
      this.cancel();
    }
    return this.lastResult;
  }

  /**
   * 判断是否有待执行的任务
   */
  pending() {
    return this.timer !== null;
  }

  /**
   * 获取调用历史
   */
  getHistory() {
    return [...this.history];
  }

  /**
   * 获取统计信息
   */
  getStats() {
    return { ...this.stats };
  }

  /**
   * 重置状态
   */
  reset() {
    this.cancel();
    this.history = [];
    this.stats = {
      callCount: 0,
      invokedCount: 0,
      canceledCount: 0
    };
    this.lastResult = null;
    this.lastCallTime = null;
    this.lastInvokeTime = null;
  }
}

节流函数实现

基础节流实现

javascript 复制代码
function throttleTimer(fn, interval) {
  let timer = null;

  return function (...args) {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, interval);
    }
  };
}

完整版节流(支持首尾执行)

javascript 复制代码
class ThrottledFunction {
  constructor(fn, interval, options = {}) {
    this.fn = fn;
    this.interval = interval;
    this.leading = options.leading !== false; // 是否立即执行
    this.trailing = options.trailing !== false; // 是否最后执行

    this.timer = null;
    this.lastArgs = null;
    this.lastContext = null;
    this.lastResult = null;
    this.lastInvokeTime = 0;

    // 参数历史
    this.history = [];
    this.maxHistory = options.maxHistory || 10;

    // 统计信息
    this.stats = {
      callCount: 0,
      invokedCount: 0,
      throttledCount: 0
    };
  }

  /**
   * 执行函数
   */
  _invoke() {
    const now = Date.now();
    this.lastInvokeTime = now;
    this.stats.invokedCount++;

    // 记录历史
    if (this.lastArgs) {
      this.history.push({
        args: [...this.lastArgs],
        timestamp: now,
        type: 'executed'
      });

      if (this.history.length > this.maxHistory) {
        this.history.shift();
      }
    }

    // 执行函数
    this.lastResult = this.fn.apply(this.lastContext, this.lastArgs);
    this.lastArgs = null;
    this.lastContext = null;
  }

  /**
   * 调用节流函数
   */
  call(...args) {
    const now = Date.now();
    this.stats.callCount++;
    this.lastArgs = args;
    this.lastContext = this;

    // 检查是否在节流期内
    const timeSinceLastInvoke = now - this.lastInvokeTime;
    const isThrottled = timeSinceLastInvoke < this.interval;

    if (isThrottled) {
      this.stats.throttledCount++;

      // 如果需要尾部执行
      if (this.trailing) {
        // 清除现有的尾部执行定时器
        if (this.timer) {
          clearTimeout(this.timer);
        }

        // 设置尾部执行定时器
        const remainingTime = this.interval - timeSinceLastInvoke;
        this.timer = setTimeout(() => {
          if (this.lastArgs) {
            this._invoke();
          }
          this.timer = null;
        }, remainingTime);
      }

      return this.lastResult;
    }

    // 不在节流期内
    if (this.leading) {
      // 头部执行
      this._invoke();
    } else if (this.trailing) {
      // 延迟执行
      if (this.timer) {
        clearTimeout(this.timer);
      }
      this.timer = setTimeout(() => {
        if (this.lastArgs) {
          this._invoke();
        }
        this.timer = null;
      }, this.interval);
    }

    return this.lastResult;
  }

  /**
   * 取消尾部执行
   */
  cancel() {
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
    }
    this.lastArgs = null;
    this.lastContext = null;
  }

  /**
   * 立即执行并取消尾部执行
   */
  flush() {
    if (this.lastArgs) {
      this._invoke();
      this.cancel();
    }
    return this.lastResult;
  }

  /**
   * 判断是否有尾部待执行
   */
  pending() {
    return this.timer !== null;
  }

  /**
   * 获取历史记录
   */
  getHistory() {
    return [...this.history];
  }

  /**
   * 获取统计信息
   */
  getStats() {
    return { ...this.stats };
  }

  /**
   * 重置状态
   */
  reset() {
    this.cancel();
    this.history = [];
    this.stats = {
      callCount: 0,
      invokedCount: 0,
      throttledCount: 0
    };
    this.lastInvokeTime = 0;
    this.lastResult = null;
  }
}

进阶实现与组合优化

支持最大等待时间的防抖

支持最大等待时间的防抖 ,就是确保函数至少每隔 maxWait 时间执行一次:

javascript 复制代码
function debounceMaxWait(fn, delay, maxWait) {
  let timer = null;
  let lastArgs = null;
  let lastContext = null;
  let lastInvokeTime = null;
  let maxTimer = null;

  const invoke = () => {
    lastInvokeTime = Date.now();
    fn.apply(lastContext, lastArgs);
    lastArgs = null;
    lastContext = null;
  };

  const startMaxWaitTimer = () => {
    if (maxTimer) clearTimeout(maxTimer);

    maxTimer = setTimeout(() => {
      if (lastArgs) {
        console.log('达到最大等待时间,强制执行');
        invoke();
      }
    }, maxWait);
  };

  return function (...args) {
    lastArgs = args;
    lastContext = this;

    // 清除现有延迟定时器
    if (timer) {
      clearTimeout(timer);
    }

    // 设置最大等待时间定时器
    if (maxWait && !lastInvokeTime) {
      startMaxWaitTimer();
    }

    // 设置新的延迟定时器
    timer = setTimeout(() => {
      invoke();
      timer = null;

      if (maxTimer) {
        clearTimeout(maxTimer);
        maxTimer = null;
      }
    }, delay);
  };
}

动态调整延迟时间的防抖

根据调用频率动态调整等待时间:

javascript 复制代码
function debounceAdaptive(fn, baseDelay, options = {}) {
  const {
    minDelay = 100,
    maxDelay = 1000,
    factor = 0.8
  } = options;

  let timer = null;
  let lastArgs = null;
  let lastContext = null;
  let callTimes = [];
  let currentDelay = baseDelay;

  const calculateDelay = () => {
    // 计算最近1秒内的调用频率
    const now = Date.now();
    callTimes = callTimes.filter(t => now - t < 1000);
    const frequency = callTimes.length;

    // 根据频率调整延迟
    if (frequency > 10) {
      // 高频调用,增加延迟
      currentDelay = Math.min(currentDelay * (1 + frequency / 100), maxDelay);
    } else if (frequency < 2) {
      // 低频调用,减少延迟
      currentDelay = Math.max(currentDelay * factor, minDelay);
    }

    return currentDelay;
  };

  return function (...args) {
    callTimes.push(Date.now());
    lastArgs = args;
    lastContext = this;

    if (timer) {
      clearTimeout(timer);
    }

    const delay = calculateDelay();
    console.log(`  当前延迟: ${Math.round(delay)}ms (调用频率: ${callTimes.length}/秒)`);

    timer = setTimeout(() => {
      fn.apply(lastContext, lastArgs);
      timer = null;
    }, delay);
  };
}

实际应用场景实战

搜索框自动补全

javascript 复制代码
class SearchAutoComplete {
  constructor(options = {}) {
    this.searchAPI = options.searchAPI || this.mockSearchAPI;
    this.minLength = options.minLength || 2;
    this.debounceDelay = options.debounceDelay || 300;
    this.maxResults = options.maxResults || 10;
    this.cacheResults = options.cacheResults !== false;

    // 搜索缓存
    this.cache = new Map();

    // 创建防抖搜索函数
    this.debouncedSearch = this.createDebouncedSearch();

    // 请求计数器
    this.requestCount = 0;
    this.cacheHitCount = 0;
  }

  /**
   * 模拟搜索API
   */
  async mockSearchAPI(query) {
    // 模拟网络延迟
    await new Promise(resolve => setTimeout(resolve, 200));

    // 模拟搜索结果
    const results = [];
    const prefixes = ['apple', 'banana', 'orange', 'grape', 'watermelon'];

    for (let i = 1; i <= 5; i++) {
      results.push({
        id: i,
        text: `${query} 结果 ${i}`,
        category: prefixes[i % prefixes.length]
      });
    }

    return results;
  }

  /**
   * 创建防抖搜索函数
   */
  createDebouncedSearch() {
    const searchFn = async (query) => {
      // 检查缓存
      if (this.cacheResults && this.cache.has(query)) {
        this.cacheHitCount++;
        return this.cache.get(query);
      }

      // 执行真实搜索
      this.requestCount++;
      console.log(`  🌐 [请求#${this.requestCount}] "${query}"`);

      try {
        const results = await this.searchAPI(query);

        // 存入缓存
        if (this.cacheResults) {
          this.cache.set(query, results);

          // 限制缓存大小
          if (this.cache.size > 50) {
            const oldestKey = this.cache.keys().next().value;
            this.cache.delete(oldestKey);
          }
        }

        return results;
      } catch (error) {
        console.error(`搜索失败: ${query}`, error);
        return [];
      }
    };

    // 使用完整版防抖
    return debounceComplete(searchFn, this.debounceDelay, {
      immediate: false,
      maxWait: 1000
    });
  }

  /**
   * 用户输入处理
   */
  onInput(query) {

    // 忽略空查询
    if (!query || query.length < this.minLength) {
      console.log('  查询太短,忽略');
      return Promise.resolve([]);
    }

    // 执行防抖搜索
    return this.debouncedSearch(query)
      .then(results => {
        const limited = results.slice(0, this.maxResults);
        console.log(`返回 ${limited.length} 条结果`);
        this.renderResults(limited);
        return limited;
      })
      .catch(error => {
        console.error('搜索失败:', error);
        return [];
      });
  }

  /**
   * 渲染搜索结果
   */
  renderResults(results) {
    // 实际项目中这里会更新DOM
    console.log(' 搜索结果:');
    results.slice(0, 3).forEach((result, i) => {
      console.log(`    ${i + 1}. ${result.text}`);
    });
    if (results.length > 3) {
      console.log(`... 等 ${results.length} 条`);
    }
  }

  /**
   * 清空缓存
   */
  clearCache() {
    this.cache.clear();
    this.cacheHitCount = 0;
    console.log('搜索缓存已清空');
  }

  /**
   * 获取统计信息
   */
  getStats() {
    return {
      requestCount: this.requestCount,
      cacheHitCount: this.cacheHitCount,
      cacheSize: this.cache.size,
      pending: this.debouncedSearch.pending(),
      debounceStats: this.debouncedSearch.getStats?.()
    };
  }
}

无限滚动加载

javascript 复制代码
console.log('\n=== 无限滚动加载 ===\n');

class InfiniteScroll {
  constructor(options = {}) {
    this.loadMoreAPI = options.loadMoreAPI || this.mockLoadMoreAPI;
    this.throttleInterval = options.throttleInterval || 200;
    this.threshold = options.threshold || 200;
    this.pageSize = options.pageSize || 20;

    this.currentPage = 0;
    this.hasMore = true;
    this.isLoading = false;
    this.items = [];

    // 创建节流滚动处理函数
    this.throttledScroll = this.createThrottledScroll();

    // 记录最后一次滚动位置
    this.lastScrollPosition = 0;
    this.scrollHistory = [];
  }

  /**
   * 模拟加载更多数据
   */
  async mockLoadMoreAPI(page, pageSize) {
    // 模拟网络延迟
    await new Promise(resolve => setTimeout(resolve, 300));

    // 模拟数据
    const start = page * pageSize;
    const items = [];

    for (let i = 0; i < pageSize; i++) {
      items.push({
        id: start + i,
        title: `项目 ${start + i}`,
        content: `这是第 ${start + i} 个项目的内容`,
        timestamp: Date.now()
      });
    }

    // 模拟没有更多数据
    const hasMore = page < 10;

    return { items, hasMore };
  }

  /**
   * 创建节流滚动处理函数
   */
  createThrottledScroll() {
    const scrollHandler = async (scrollTop, clientHeight, scrollHeight) => {
      const distanceFromBottom = scrollHeight - scrollTop - clientHeight;

      console.log(`滚动位置: ${scrollTop}, 距离底部: ${distanceFromBottom}px`);

      // 记录滚动位置
      this.lastScrollPosition = scrollTop;
      this.scrollHistory.push({
        position: scrollTop,
        timestamp: Date.now()
      });

      // 限制历史记录大小
      if (this.scrollHistory.length > 20) {
        this.scrollHistory.shift();
      }

      // 检查是否需要加载更多
      if (distanceFromBottom < this.threshold) {
        await this.loadMore();
      }
    };

    return throttleComplete(scrollHandler, this.throttleInterval, {
      leading: true,
      trailing: true
    });
  }

  /**
   * 处理滚动事件
   */
  onScroll(event) {
    const target = event.target;
    const scrollTop = target.scrollTop || target.scrollingElement?.scrollTop || 0;
    const clientHeight = target.clientHeight || window.innerHeight;
    const scrollHeight = target.scrollHeight || document.documentElement.scrollHeight;

    this.throttledScroll(scrollTop, clientHeight, scrollHeight);
  }

  /**
   * 加载更多数据
   */
  async loadMore() {
    if (this.isLoading || !this.hasMore) {
      console.log(`${this.isLoading ? '正在加载中' : '没有更多数据'}`);
      return;
    }

    this.isLoading = true;
    this.currentPage++;

    console.log(`加载第 ${this.currentPage} 页数据...`);

    try {
      const result = await this.loadMoreAPI(this.currentPage, this.pageSize);

      this.hasMore = result.hasMore;
      this.items.push(...result.items);

      console.log(`加载完成,当前总条目: ${this.items.length}`);
      console.log(`还有更多: ${this.hasMore}`);

      this.renderItems(result.items);
    } catch (error) {
      console.error('加载失败:', error);
      this.currentPage--; // 回退页数
    } finally {
      this.isLoading = false;
    }
  }

  /**
   * 渲染新加载的项目
   */
  renderItems(newItems) {
    // 实际项目中这里会更新DOM
    console.log('新增项目:');
    newItems.slice(0, 3).forEach((item, i) => {
      console.log(`${item.id}. ${item.title}`);
    });
    if (newItems.length > 3) {
      console.log(`... 等 ${newItems.length} 条`);
    }
  }

  /**
   * 重置到顶部
   */
  reset() {
    this.currentPage = 0;
    this.hasMore = true;
    this.isLoading = false;
    this.items = [];
    this.scrollHistory = [];
    console.log('滚动列表已重置');
  }

  /**
   * 获取滚动统计
   */
  getScrollStats() {
    if (this.scrollHistory.length < 2) {
      return { avgSpeed: 0 };
    }

    const recent = this.scrollHistory.slice(-10);
    let totalSpeed = 0;

    for (let i = 1; i < recent.length; i++) {
      const distance = recent[i].position - recent[i - 1].position;
      const timeDiff = recent[i].timestamp - recent[i - 1].timestamp;
      const speed = distance / timeDiff; // px/ms
      totalSpeed += speed;
    }

    return {
      avgSpeed: totalSpeed / (recent.length - 1),
      scrollCount: this.scrollHistory.length,
      lastPosition: this.lastScrollPosition
    };
  }
}

最佳实践指南

防抖最佳实践

  • 默认延迟时间:300-500ms(用户输入)、200-300ms(窗口调整)、1000ms(自动保存)
  • 搜索框建议使用防抖,避免频繁请求
  • 表单验证使用防抖,用户输完再验证
  • 提交按钮使用防抖,防止重复提交
  • 需要立即反馈的操作设置 immediate: true

节流最佳实践

  • 滚动加载:200-300ms(平衡响应性和性能)
  • 拖拽事件:16-33ms(约30-60fps)
  • 窗口大小调整:100-200ms
  • 游戏循环:使用 requestAnimationFrame 替代定时器节流
  • 频繁的状态更新:考虑使用 requestAnimationFrame

内存管理实践

  • 组件卸载时取消未执行的防抖/节流
  • 避免在全局作用域创建过多的防抖/节流函数
  • 使用缓存时注意设置最大缓存大小
  • 定期清理过期的缓存数据

调试技巧

  • 添加日志追踪函数调用
  • 记录调用历史便于回溯问题
  • 使用 Stats 统计调用次数和节流情况
  • 开发环境设置更短的延迟时间便于测试

防抖节流选择决策树

text 复制代码
是否需要处理高频事件?
        │
        ├─→ 是
        │   │
        │   ├─→ 是否需要关注最后一次执行?
        │   │   │
        │   │   ├─→ 是 → 使用防抖
        │   │   │   │
        │   │   │   ├─→ 搜索建议、自动保存、表单验证
        │   │   │   └─→ 窗口调整、拖拽结束
        │   │   │
        │   │   └─→ 否 → 使用节流
        │   │       │
        │   │       ├─→ 滚动加载、拖拽中、动画帧
        │   │       └─→ 游戏循环、鼠标移动
        │   │
        │   └─→ 是否需要立即执行?
        │       │
        │       ├─→ 是 → immediate: true
        │       │   │
        │       │   ├─→ 按钮提交(防止双击)
        │       │   └─→ 数据埋点
        │       │
        │       └─→ 否 → immediate: false
        │           │
        │           ├─→ 搜索建议(避免每个字符都请求)
        │           └─→ 自动保存(停止编辑后保存)
        │
        └─→ 否 → 不需要特殊处理

最终建议

  1. 不要盲目使用防抖/节流,先评估是否真的需要
  2. 根据用户体验选择合理的延迟时间
  3. 为防抖/节流函数命名时标明其特性
  4. 在类组件中绑定this时注意上下文
  5. 优先使用成熟的库实现(lodash、underscore)
  6. 理解原理,但不一定需要每次都自己实现
  7. 监控实际效果,根据数据持续优化

结语

防抖和节流是前端性能优化的基本工具,掌握它们不仅能提升应用性能,还能优化用户体验。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

相关推荐
leafyyuki几秒前
两行 CSS 搞定筛选条行尾对齐,Element Plus 表单布局终极方案
前端
着迷不白1 分钟前
六、Bash Shell 与进程管理
前端·chrome
A不落雨滴AI1 分钟前
DKERP 客户端重构:30天从零到一的架构演进之路
前端
Xp021911037 分钟前
知网研学、万方、WPS、大以论文四大排版工具横评,新用户免费排版等你领!
前端·css·html·生活·wps·论文排版
全栈技术负责人7 分钟前
老项目新需求AI前端开发指南
前端·ai编程
周凡12317 分钟前
AI 时代的 Web JavaScript 逆向分析实践与思考
前端·javascript·人工智能
jerryinwuhan22 分钟前
marker BiBERTo解释
java·前端·人工智能
zhoumeina9930 分钟前
分段创建产品,tab 页切换又要保留缓存
前端·javascript
SilentSamsara31 分钟前
命令行工具开发:Click/Typer + 打包为独立二进制
linux·服务器·开发语言·前端·python·青少年编程·fastapi
恋猫de小郭34 分钟前
能在手机本地跑的图像生成模型 Bonsai Image ,效果还不错
前端·aigc·ai编程