You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Ruby on Rails+Stimulus中Slim Select防抖搜索实现遇问题求助

Slim Select防抖搜索问题(Rails + Stimulus JS)

背景

我在Ruby on Rails项目中搭配Stimulus JS使用Slim Select已有6个月。本季度团队做性能优化,已将搜索功能改为Slim Select的Ajax请求且可正常运行,现在需要实现防抖搜索以避免每次按键都请求服务器,但尝试后遇到问题。

原控制器代码

import { Controller } from "@hotwired/stimulus"
import SlimSelect from 'slim-select'
import debounce from "debounce";
import { authToken } from '../global'

export default class extends Controller {
  connect() {
    this.initializeInstance();
  }

  disconnect() {
    this.slimSelect.destroy();
  }

  initializeInstance() {
    this.searchIt = this.searchIt.bind(this);

    const options = {
      select: this.element,
      settings: {
        hideSelectedOption: true,
        placeholderText: this.placeholder,
        allowDeselect: true,
        maxValuesShown: 100,
      }
    }

    if (this.optionsUrl) {
      options.settings.searchingText = 'Searching...'
      options.events = {
        search: this.searchIt,
        afterOpen: () => {
          this.slimSelect.search('');
        }
      }
    }

    this.slimSelect = new SlimSelect(options)

    // Auto-populate the select with data from the prepopulateUrl
    // This comes in handy for applying filters to the select
    if (this.prepopulateUrl) {
      fetch(this.prepopulateUrl, {
        method: 'GET',
        headers: {
          'Accept': 'application/json',
          'X-CSRF-Token': authToken(),
        },
      })
      .then(response => response.json())
      .then(data => {
        const fieldMappings = this.fieldMappings;
        const options = data.map(item => ({ text: item[fieldMappings.text], value: item[fieldMappings.value] }));
        const selectedValues = data.map(item => item[fieldMappings.value]);
  
        this.slimSelect.setData(options);
        this.slimSelect.setSelected(selectedValues);
      })
      .catch(error => {
        console.error('Error:', error);
      });
    }
  }

  reinitializeInstance() {
    this.disconnect();
    this.initializeInstance();
  }

  get optionsUrl () {
    return this.element.dataset.optionsUrl
  }

  get placeholder () {
    return this.element.getAttribute('placeholder')
  }

  get fieldMappings() {
    return JSON.parse(this.element.dataset.fieldsMapping)
  }

  get prepopulateUrl() {
    return this.element.dataset.prepopulateUrl
  }

  searchIt (search, currentData) {
    return new Promise((resolve, reject) => {
      const url = this.optionsUrl.replace('${search}', search);

      fetch(url, {
        method: 'GET',
        headers: {
          'Accept': 'application/json',
          'X-CSRF-Token': authToken(),
        },
      })
      .then((response) => response.json())
      .then(data => {
        const fieldMappings = this.fieldMappings;

        // Take the data and create an array of options
        // excluding any that are already selected in currentData
        const options = data.filter((obj) => {
                          return !currentData.some((optionData) => {
                            return optionData.value === obj[fieldMappings.value]
                          })
                        })
                        .map((item) => {
                          return {
                            text: item[fieldMappings.text],
                            value: item[fieldMappings.value],
                          }
                        })

        resolve([...options, ...currentData]);
      })
    });
  }
}

尝试的防抖实现

我将search事件替换为防抖函数,输入搜索内容时功能正常,但首次打开搜索框会报错:

if (this.optionsUrl) {
      options.settings.searchingText = 'Searching...'
      options.events = {
        search: debounce(this.searchIt, 300),
        afterOpen: () => {
          this.slimSelect.search('');
        }
      }
}

错误现象

首次打开搜索框时出现如下错误:
错误截图

我也尝试在afterOpen中使用debounce(this.slimSelect.search(''), 300),但问题仍未解决。


解决方案

问题根源在于:

  1. Slim Select的search事件要求返回Promise,但默认的防抖函数不会传递原函数的Promise返回值,首次打开触发search('')时,Slim Select无法获取到预期的Promise,导致报错。
  2. afterOpen中调用this.slimSelect.search('')会触发防抖后的搜索函数,延迟执行的结果无法被Slim Select及时获取。

可以通过以下方式修复:

方案1:使用支持leading参数的防抖库

如果你的debounce库(如lodash.debounce或新版debounce包)支持leading配置,让首次调用立刻执行,同时确保防抖函数返回原函数的Promise:

initializeInstance() {
  // 绑定防抖函数,设置leading: true让首次调用立即执行
  this.debouncedSearch = debounce(this.searchIt.bind(this), 300, { leading: true });

  const options = {
    select: this.element,
    settings: {
      hideSelectedOption: true,
      placeholderText: this.placeholder,
      allowDeselect: true,
      maxValuesShown: 100,
    }
  }

  if (this.optionsUrl) {
    options.settings.searchingText = 'Searching...'
    options.events = {
      search: (search, currentData) => {
        // 确保返回防抖函数的Promise结果
        return this.debouncedSearch(search, currentData);
      },
      afterOpen: () => {
        // 首次打开直接调用原searchIt,避免防抖延迟
        this.searchIt('', this.slimSelect.data).then(data => {
          this.slimSelect.setData(data);
        });
      }
    }
  }

  this.slimSelect = new SlimSelect(options);

  // 原预填充代码保持不变
}

方案2:自定义支持Promise的防抖函数

如果你的debounce库不支持leading参数,自己实现一个能传递Promise的防抖函数:

// 自定义防抖函数,支持Promise返回值和首次立即执行
function debouncePromise(func, wait) {
  let timeoutId;
  return function(...args) {
    return new Promise((resolve, reject) => {
      if (timeoutId) clearTimeout(timeoutId);
      
      // 首次调用立即执行
      if (!timeoutId) {
        func.apply(this, args).then(resolve).catch(reject);
      }

      timeoutId = setTimeout(() => {
        func.apply(this, args).then(resolve).catch(reject);
        timeoutId = null;
      }, wait);
    });
  };
}

然后在控制器中导入并使用:

// 替换原debounce导入
// import debounce from "debounce";
import debouncePromise from './debounce-promise';

// ...

initializeInstance() {
  this.debouncedSearch = debouncePromise(this.searchIt.bind(this), 300);

  // 后续事件配置同方案1
}

这样修改后,首次打开搜索框会立刻加载初始数据,输入搜索内容时触发防抖逻辑避免频繁请求,同时保证Slim Select能正确获取到Promise返回值,不会出现报错。


内容的提问来源于stack exchange,提问作者mohsin_jamshaid

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 20:13:11