autocomplete

6 阅读8分钟

一、带注释的完整源码

<template>
  <!-- 使用过渡动画,动画名称为 el-zoom-in-top -->
  <transition name="el-zoom-in-top" @after-leave="doDestroy">
    <!-- 建议列表容器,语义化为区域 -->
    <div
      v-show="showPopper"
      class="el-autocomplete-suggestion el-popper"
      :class="{ 'is-loading': !parent.hideLoading && parent.loading }"  <!-- 加载状态类 -->
      :style="{ width: dropdownWidth }"  <!-- 动态宽度控制 -->
      role="region">  <!-- 无障碍访问:标记为区域 -->
      
      <!-- 滚动条组件包裹建议列表 -->
      <el-scrollbar
        tag="ul"  <!-- 滚动区域标签类型为 ul -->
        wrap-class="el-autocomplete-suggestion__wrap"  <!-- 滚动容器类名 -->
        view-class="el-autocomplete-suggestion__list">  <!-- 滚动内容类名 -->
        
        <!-- 加载状态提示 -->
        <li v-if="!parent.hideLoading && parent.loading"><i class="el-icon-loading"></i></li>
        
        <!-- 建议列表内容(插槽) -->
        <slot v-else></slot>
      </el-scrollbar>
    </div>
  </transition>
</template>

<script>
  // 导入Popper.js核心功能
  import Popper from 'element-ui/src/utils/vue-popper';
  // 导入事件分发混合
  import Emitter from 'element-ui/src/mixins/emitter';
  // 导入滚动条组件
  import ElScrollbar from 'element-ui/packages/scrollbar';

  export default {
    components: { ElScrollbar },  <!-- 注册滚动条组件 -->
    mixins: [Popper, Emitter],  <!-- 混入Popper和Emitter功能 -->

    componentName: 'ElAutocompleteSuggestions',  <!-- 组件标识 -->

    data() {
      return {
        parent: this.$parent,  <!-- 获取父组件实例 -->
        dropdownWidth: ''  <!-- 下拉框宽度(动态计算) -->
      };
    },

    props: {
      options: {  <!-- Popper配置(实际未使用) -->
        default() {
          return {
            gpuAcceleration: false
          };
        }
      },
      id: String  <!-- 建议列表的id(用于无障碍) -->
    },

    methods: {
      // 选择建议项时触发父组件事件
      select(item) {
        this.dispatch('ElAutocomplete', 'item-click', item);  <!-- 分发事件 -->
      }
    },

    updated() {
      // 内容更新后更新弹出框位置
      this.$nextTick(_ => {
        this.popperJS && this.updatePopper();  <!-- 调用Popper更新 -->
      });
    },

    mounted() {
      // 设置Popper所需的关键元素
      this.$parent.popperElm = this.popperElm = this.$el;  <!-- 弹出框元素 -->
      this.referenceElm = this.$parent.$refs.input.$refs.input || this.$parent.$refs.input.$refs.textarea;  <!-- 参考元素(输入框) -->
      this.referenceList = this.$el.querySelector('.el-autocomplete-suggestion__list');  <!-- 列表元素 -->
      this.referenceList.setAttribute('role', 'listbox');  <!-- 无障碍:设置为列表框 -->
      this.referenceList.setAttribute('id', this.id);  <!-- 设置id用于关联 -->
    },

    created() {
      // 监听父组件的可见性事件
      this.$on('visible', (val, inputWidth) => {
        this.dropdownWidth = inputWidth + 'px';  <!-- 动态计算宽度 -->
        this.showPopper = val;  <!-- 控制显示 -->
      });
    }
  };
</script>

二、学习笔记(Markdown格式)

Element UI Autocomplete Suggestions 组件深度解析

一、核心设计思想

该组件是Element UI Autocomplete组件的下拉建议列表容器,核心关注点:

  1. 语义化布局:使用role="region"role="listbox"提升无障碍访问
  2. 动态定位:通过Popper.js实现精准定位
  3. 内容隔离:使用插槽实现内容与布局解耦

二、关键部分分析

1. 语义化与无障碍设计(重点!)

<div role="region">
  <el-scrollbar ...>
    <li v-if="..."><i class="el-icon-loading"></i></li>
    <slot></slot>
  </el-scrollbar>
</div>
this.referenceList.setAttribute('role', 'listbox');
this.referenceList.setAttribute('id', this.id);

💡 为什么重要

  • role="region" 标记为独立区域
  • role="listbox" 使屏幕阅读器识别为列表框
  • id 属性使输入框与列表关联(WCAG 2.0标准)

2. 动态宽度控制(核心功能)

created() {
  this.$on('visible', (val, inputWidth) => {
    this.dropdownWidth = inputWidth + 'px';  <!-- 关键:动态计算宽度 -->
    this.showPopper = val;
  });
}

设计精髓

  • 下拉框宽度 = 输入框宽度(避免内容溢出)
  • 通过字符串拼接确保CSS单位正确(inputWidth + 'px'
  • 记忆点:所有宽度属性必须带单位(px/%等)

3. Popper.js集成(关键难点)

mixins: [Popper, Emitter],
mounted() {
  this.$parent.popperElm = this.popperElm = this.$el;  <!-- 设置弹出框元素 -->
  this.referenceElm = this.$parent.$refs.input.$refs.input || ...;  <!-- 设置参考元素 -->
}

💡 为什么是难点

  • Popper.js需要popperElm(弹出框)和referenceElm(参考元素)
  • 参考元素可能是inputtextarea(需兼容处理)
  • 记忆点popperElmreferenceElm是Popper.js的必需属性

4. 滚动条与内容渲染(易错点)

<el-scrollbar
  tag="ul"
  wrap-class="el-autocomplete-suggestion__wrap"
  view-class="el-autocomplete-suggestion__list">
  <slot></slot>
</el-scrollbar>

⚠️ 常见错误

  • 未设置view-class → 滚动区域样式失效
  • 未用tag="ul" → 语义化错误(列表应为ul
  • 记忆点:滚动条组件需正确设置tagview-class

三、难点分析(必须掌握)

1. 父子组件通信机制(核心难点)

// 父组件触发(ElAutocomplete)
this.$emit('visible', true, inputWidth);

// 子组件监听(ElAutocompleteSuggestions)
created() {
  this.$on('visible', (val, inputWidth) => {
    this.dropdownWidth = inputWidth + 'px';
    this.showPopper = val;
  });
}

为什么是难点

  • Element UI内部组件通信不使用props,而是通过$emit/$on
  • 通信发生在created钩子(比mounted更早)
  • 记忆点:所有内部组件通信都通过$emit/$on,而非props

2. Popper.js的生命周期管理(关键难点)

mounted() {
  // 设置Popper所需的关键元素
  this.$parent.popperElm = this.popperElm = this.$el;
  this.referenceElm = ...;
}

updated() {
  this.$nextTick(_ => {
    this.popperJS && this.updatePopper();  <!-- 更新定位 -->
  });
}

<transition @after-leave="doDestroy">  <!-- 动画结束后销毁 -->

为什么是难点

  • updatePopper()必须在$nextTick中调用(确保DOM更新)
  • doDestroy在动画结束后调用,避免内存泄漏
  • 记忆点:所有Popper.js组件必须在after-leave调用doDestroy

3. 加载状态的智能处理(细节难点)

<li v-if="!parent.hideLoading && parent.loading">
  <i class="el-icon-loading"></i>
</li>

💡 设计亮点

  • hideLoading属性(由父组件控制)决定是否显示加载图标
  • 通过!parent.hideLoading && parent.loading双重判断确保逻辑正确
  • 记忆点:加载状态 = 父组件未隐藏 + 正在加载

四、学习建议

1. 实践验证

<el-autocomplete
  v-model="state"
  :fetch-suggestions="querySearch"
  placeholder="请输入内容">
  <template #default="{ item }">
    <div class="custom-item">
      {{ item.value }}
    </div>
  </template>
</el-autocomplete>

2. 调试技巧

  1. 检查无障碍属性
    • 开发者工具 → Elements → 检查role="listbox"id属性
  2. 验证宽度
    • 修改输入框宽度 → 观察下拉框宽度是否同步变化
  3. 调试Popper
    • mounted中打印this.referenceElm确认元素正确

3. 深入思考

  1. 为什么不用props传递宽度
    • 父组件(ElAutocomplete)需要动态获取输入框宽度
    • 通过事件传递比props更灵活(避免循环依赖)
  2. 如果父组件没有$refs.input会怎样
    • 代码中已做兼容:this.$parent.$refs.input.$refs.input || ...
    • 记忆点:Element UI组件需兼容多种输入类型

💎 总结:这个组件是Element UI内部通信机制无障碍设计的典范。必须掌握:

  1. 通过$on/$emit实现内部通信
  2. Popper.js的生命周期管理(updatePopper + doDestroy
  3. 语义化标签与无障碍属性的正确使用

重点记忆this.$parent.popperElm = this.popperElm = this.$el 这行代码是Popper.js正常工作的关键!

一、带注释的完整源码

<template>
  <!-- 主容器:自动完成输入框,语义化为组合框 -->
  <div
    class="el-autocomplete"
    v-clickoutside="close"  <!-- 点击外部关闭建议列表 -->
    aria-haspopup="listbox"  <!-- 无障碍:表示有下拉列表 -->
    role="combobox"  <!-- 无障碍:组合框角色 -->
    :aria-expanded="suggestionVisible"  <!-- 无障碍:当前是否展开 -->
    :aria-owns="id"  <!-- 无障碍:关联建议列表id -->
  >
    <!-- 内部输入框(基于el-input封装) -->
    <el-input
      ref="input"
      v-bind="[$props, $attrs]"  <!-- 保留所有props和attrs -->
      @input="handleInput"  <!-- 输入事件 -->
      @change="handleChange"  <!-- 值变化事件 -->
      @focus="handleFocus"  <!-- 聚焦事件 -->
      @blur="handleBlur"  <!-- 失焦事件 -->
      @clear="handleClear"  <!-- 清除事件 -->
      @keydown.up.native.prevent="highlight(highlightedIndex - 1)"  <!-- 上箭头 -->
      @keydown.down.native.prevent="highlight(highlightedIndex + 1)"  <!-- 下箭头 -->
      @keydown.enter.native="handleKeyEnter"  <!-- 回车 -->
      @keydown.native.tab="close"  <!-- Tab键关闭 -->
    >
      <!-- 插槽支持(前置/后置/前缀/后缀) -->
      <template slot="prepend" v-if="$slots.prepend">
        <slot name="prepend"></slot>
      </template>
      <template slot="append" v-if="$slots.append">
        <slot name="append"></slot>
      </template>
      <template slot="prefix" v-if="$slots.prefix">
        <slot name="prefix"></slot>
      </template>
      <template slot="suffix" v-if="$slots.suffix">
        <slot name="suffix"></slot>
      </template>
    </el-input>
    
    <!-- 建议列表容器(自定义组件) -->
    <el-autocomplete-suggestions
      visible-arrow
      :class="[popperClass ? popperClass : '']"  <!-- 自定义弹出层类 -->
      :popper-options="popperOptions"  <!-- Popper配置 -->
      :append-to-body="popperAppendToBody"  <!-- 是否追加到body -->
      ref="suggestions"
      :placement="placement"  <!-- 位置(bottom-start等) -->
      :id="id">  <!-- 建议列表id(用于无障碍) -->
      <!-- 建议项列表 -->
      <li
        v-for="(item, index) in suggestions"
        :key="index"
        :class="{'highlighted': highlightedIndex === index}"  <!-- 高亮样式 -->
        @click="select(item)"  <!-- 点击选择 -->
        :id="`${id}-item-${index}`"  <!-- 每项id(无障碍) -->
        role="option"  <!-- 无障碍:选项角色 -->
        :aria-selected="highlightedIndex === index"  <!-- 无障碍:是否选中 -->
      >
        <!-- 插槽支持自定义建议项 -->
        <slot :item="item">
          {{ item[valueKey] }}  <!-- 默认显示valueKey属性的值 -->
        </slot>
      </li>
    </el-autocomplete-suggestions>
  </div>
</template>

<script>
  import debounce from 'throttle-debounce/debounce';
  import ElInput from 'element-ui/packages/input';
  import Clickoutside from 'element-ui/src/utils/clickoutside';
  import ElAutocompleteSuggestions from './autocomplete-suggestions.vue';
  import Emitter from 'element-ui/src/mixins/emitter';
  import Migrating from 'element-ui/src/mixins/migrating';
  import { generateId } from 'element-ui/src/utils/util';
  import Focus from 'element-ui/src/mixins/focus';

  export default {
    name: 'ElAutocomplete',
    mixins: [Emitter, Focus('input'), Migrating],  <!-- 混入事件、焦点和迁移支持 -->
    inheritAttrs: false,  <!-- 不继承attrs(避免与el-input冲突) -->
    componentName: 'ElAutocomplete',  <!-- 组件标识 -->

    components: {
      ElInput,
      ElAutocompleteSuggestions
    },

    directives: { Clickoutside },  <!-- 注册点击外部指令 -->

    props: {
      valueKey: {  <!-- 数据项中显示的键 -->
        type: String,
        default: 'value'
      },
      popperClass: String,  <!-- 自定义弹出层类名 -->
      popperOptions: Object,  <!-- Popper配置 -->
      placeholder: String,  <!-- 占位符 -->
      clearable: {  <!-- 是否可清除 -->
        type: Boolean,
        default: false
      },
      disabled: Boolean,  <!-- 是否禁用 -->
      name: String,  <!-- 输入框name -->
      size: String,  <!-- 尺寸 -->
      value: String,  <!-- 当前值 -->
      maxlength: Number,  <!-- 最大长度 -->
      minlength: Number,  <!-- 最小长度 -->
      autofocus: Boolean,  <!-- 自动聚焦 -->
      fetchSuggestions: Function,  <!-- 数据获取函数 -->
      triggerOnFocus: {  <!-- 是否聚焦时触发 -->
        type: Boolean,
        default: true
      },
      customItem: String,  <!-- 废弃属性(使用插槽代替) -->
      selectWhenUnmatched: {  <!-- 是否匹配未匹配项 -->
        type: Boolean,
        default: false
      },
      prefixIcon: String,  <!-- 前置图标 -->
      suffixIcon: String,  <!-- 后置图标 -->
      label: String,  <!-- label -->
      debounce: {  <!-- 防抖时间 -->
        type: Number,
        default: 300
      },
      placement: {  <!-- 弹出位置 -->
        type: String,
        default: 'bottom-start'
      },
      hideLoading: Boolean,  <!-- 是否隐藏加载状态 -->
      popperAppendToBody: {  <!-- 是否追加到body -->
        type: Boolean,
        default: true
      },
      highlightFirstItem: {  <!-- 是否高亮第一个 -->
        type: Boolean,
        default: false
      }
    },

    data() {
      return {
        activated: false,  <!-- 是否已激活(聚焦状态) -->
        suggestions: [],  <!-- 建议列表 -->
        loading: false,  <!-- 加载状态 -->
        highlightedIndex: -1,  <!-- 高亮项索引 -->
        suggestionDisabled: false  <!-- 禁用建议列表 -->
      };
    },

    computed: {
      // 是否显示建议列表
      suggestionVisible() {
        const suggestions = this.suggestions;
        let isValidData = Array.isArray(suggestions) && suggestions.length > 0;
        return (isValidData || this.loading) && this.activated;
      },
      
      // 生成唯一id(用于无障碍)
      id() {
        return `el-autocomplete-${generateId()}`;
      }
    },

    watch: {
      // 监听建议列表可见性变化
      suggestionVisible(val) {
        let $input = this.getInput();
        if ($input) {
          // 通过broadcast广播给下拉列表组件
          this.broadcast('ElAutocompleteSuggestions', 'visible', [val, $input.offsetWidth]);
        }
      }
    },

    methods: {
      // 迁移配置(废弃属性提示)
      getMigratingConfig() {
        return {
          props: {
            'custom-item': 'custom-item is removed, use scoped slot instead.',
            'props': 'props is removed, use value-key instead.'
          }
        };
      },
      
      // 获取数据(核心逻辑)
      getData(queryString) {
        if (this.suggestionDisabled) return;
        this.loading = true;
        this.fetchSuggestions(queryString, (suggestions) => {
          this.loading = false;
          if (this.suggestionDisabled) return;
          if (Array.isArray(suggestions)) {
            this.suggestions = suggestions;
            this.highlightedIndex = this.highlightFirstItem ? 0 : -1;
          } else {
            console.error('[Element Error][Autocomplete]autocomplete suggestions must be an array');
          }
        });
      },
      
      // 输入处理
      handleInput(value) {
        this.$emit('input', value);
        this.suggestionDisabled = false;
        if (!this.triggerOnFocus && !value) {
          this.suggestionDisabled = true;
          this.suggestions = [];
          return;
        }
        this.debouncedGetData(value);  <!-- 防抖调用 -->
      },
      
      // 值变化处理
      handleChange(value) {
        this.$emit('change', value);
      },
      
      // 聚焦处理
      handleFocus(event) {
        this.activated = true;
        this.$emit('focus', event);
        if (this.triggerOnFocus) {
          this.debouncedGetData(this.value);  <!-- 聚焦时触发 -->
        }
      },
      
      // 失焦处理
      handleBlur(event) {
        this.$emit('blur', event);
      },
      
      // 清除处理
      handleClear() {
        this.activated = false;
        this.$emit('clear');
      },
      
      // 关闭建议列表
      close(e) {
        this.activated = false;
      },
      
      // 回车键处理
      handleKeyEnter(e) {
        if (this.suggestionVisible && this.highlightedIndex >= 0 && this.highlightedIndex < this.suggestions.length) {
          e.preventDefault();
          this.select(this.suggestions[this.highlightedIndex]);
        } else if (this.selectWhenUnmatched) {
          this.$emit('select', {value: this.value});
          this.$nextTick(_ => {
            this.suggestions = [];
            this.highlightedIndex = -1;
          });
        }
      },
      
      // 选择建议项
      select(item) {
        this.$emit('input', item[this.valueKey]);
        this.$emit('select', item);
        this.$nextTick(_ => {
          this.suggestions = [];
          this.highlightedIndex = -1;
        });
      },
      
      // 高亮项处理
      highlight(index) {
        if (!this.suggestionVisible || this.loading) return;
        if (index < 0) {
          this.highlightedIndex = -1;
          return;
        }
        if (index >= this.suggestions.length) {
          index = this.suggestions.length - 1;
        }
        
        // 滚动到高亮项位置
        const suggestion = this.$refs.suggestions.$el.querySelector('.el-autocomplete-suggestion__wrap');
        const suggestionList = suggestion.querySelectorAll('.el-autocomplete-suggestion__list li');
        let highlightItem = suggestionList[index];
        let scrollTop = suggestion.scrollTop;
        let offsetTop = highlightItem.offsetTop;
        
        if (offsetTop + highlightItem.scrollHeight > (scrollTop + suggestion.clientHeight)) {
          suggestion.scrollTop += highlightItem.scrollHeight;
        }
        if (offsetTop < scrollTop) {
          suggestion.scrollTop -= highlightItem.scrollHeight;
        }
        this.highlightedIndex = index;
        
        // 更新无障碍属性
        let $input = this.getInput();
        $input.setAttribute('aria-activedescendant', `${this.id}-item-${this.highlightedIndex}`);
      },
      
      // 获取输入框引用
      getInput() {
        return this.$refs.input.getInput();
      }
    },

    mounted() {
      // 创建防抖函数
      this.debouncedGetData = debounce(this.debounce, this.getData);
      
      // 监听下拉列表的点击事件
      this.$on('item-click', item => {
        this.select(item);
      });
      
      // 设置无障碍属性
      let $input = this.getInput();
      $input.setAttribute('role', 'textbox');
      $input.setAttribute('aria-autocomplete', 'list');
      $input.setAttribute('aria-controls', this.id);
      $input.setAttribute('aria-activedescendant', `${this.id}-item-${this.highlightedIndex}`);
    },

    beforeDestroy() {
      // 销毁下拉列表组件
      this.$refs.suggestions.$destroy();
    }
  };
</script>

二、学习笔记(Markdown格式)

Element UI Autocomplete 组件深度解析

一、核心设计思想

Autocomplete 组件是 Element UI 中高度交互的输入组件,核心关注点:

  1. 无障碍设计:严格遵循WCAG 2.0标准
  2. 性能优化:通过防抖减少请求频率
  3. 内容解耦:通过插槽实现内容与布局分离
  4. 状态管理:精准控制建议列表的显示/隐藏

二、关键部分分析

1. 无障碍设计(重中之重!)

<div
  aria-haspopup="listbox"
  role="combobox"
  :aria-expanded="suggestionVisible"
  :aria-owns="id"
>
// 设置输入框无障碍属性
$input.setAttribute('role', 'textbox');
$input.setAttribute('aria-autocomplete', 'list');
$input.setAttribute('aria-controls', this.id);
$input.setAttribute('aria-activedescendant', `${this.id}-item-${this.highlightedIndex}`);

💡 为什么重要

  • role="combobox":明确表示组合框
  • aria-autocomplete="list":告知屏幕阅读器有自动完成列表
  • aria-activedescendant:动态跟踪当前高亮项
  • 记忆点:无障碍属性必须与DOM结构严格匹配

2. 防抖优化(性能关键)

mounted() {
  this.debouncedGetData = debounce(this.debounce, this.getData);
}
handleInput(value) {
  this.debouncedGetData(value); // 防抖调用
}

设计精髓

  • 使用throttle-debounce库实现防抖
  • 默认300ms防抖(避免频繁请求)
  • 记忆点:所有输入型组件必须使用防抖

3. 父子组件通信(核心难点)

// 父组件(ElAutocomplete)发送可见性事件
watch: {
  suggestionVisible(val) {
    this.broadcast('ElAutocompleteSuggestions', 'visible', [val, $input.offsetWidth]);
  }
}

// 子组件(ElAutocompleteSuggestions)监听
created() {
  this.$on('visible', (val, inputWidth) => {
    this.dropdownWidth = inputWidth + 'px';
    this.showPopper = val;
  });
}

💡 为什么是难点

  • Element UI内部组件不使用props传递数据
  • 通过broadcast(广播)和$on实现通信
  • 记忆点:内部组件通信 = this.broadcast + this.$on

4. 高亮项滚动定位(细节难点)

highlight(index) {
  if (!this.suggestionVisible || this.loading) return;
  // ... 计算滚动位置 ...
  suggestion.scrollTop += highlightItem.scrollHeight; // 滚动
  this.highlightedIndex = index;
  // 更新无障碍属性
  $input.setAttribute('aria-activedescendant', `${this.id}-item-${index}`);
}

⚠️ 常见错误

  • 未处理滚动边界(导致滚动位置错误)
  • 未更新aria-activedescendant(无障碍失效)
  • 记忆点:滚动定位必须在$nextTick中执行(但这里直接操作DOM,需确保DOM已渲染)

三、难点分析(必须掌握)

1. 无障碍属性的完整链路(核心难点)

graph LR
  A[输入框] -->|aria-autocomplete=list| B[建议列表]
  A -->|aria-controls=id| C[建议列表ID]
  A -->|aria-activedescendant=当前项ID| D[当前高亮项]
  D -->|role=option| E[建议项]

为什么必须掌握

  • 无障碍是Web标准强制要求
  • Element UI的无障碍实现是行业标杆
  • 记忆点aria-activedescendant必须指向当前高亮项的ID

2. 防抖与请求逻辑(关键难点)

getData(queryString) {
  if (this.suggestionDisabled) return;
  this.loading = true;
  this.fetchSuggestions(queryString, (suggestions) => {
    this.loading = false;
    // 处理数据...
  });
}

handleInput(value) {
  this.debouncedGetData(value); // 防抖调用
}

💡 设计精髓

  • suggestionDisabled防止无效请求(如清空输入时)
  • loading状态控制加载图标显示
  • 记忆点:数据请求必须包含loading状态管理

3. 事件处理的优先级(易错点)

@keydown.up.native.prevent="highlight(highlightedIndex - 1)"
@keydown.down.native.prevent="highlight(highlightedIndex + 1)"
@keydown.enter.native="handleKeyEnter"

⚠️ 为什么重要

  • native.prevent阻止默认行为(如输入框的上/下键移动光标)
  • 优先级:keydown > input(确保快捷键生效)
  • 记忆点:键盘事件必须用native修饰符

四、重点代码总结(必须记忆)

1. 无障碍属性链(核心)

// 输入框
$input.setAttribute('aria-autocomplete', 'list');
$input.setAttribute('aria-controls', this.id);
$input.setAttribute('aria-activedescendant', `${this.id}-item-${this.highlightedIndex}`);

// 建议项
role="option"
:aria-selected="highlightedIndex === index"

💎 为什么是重点
这是Element UI无障碍设计的完整实现,任何组件都需要遵循此模式

2. 防抖请求逻辑(核心功能)

mounted() {
  this.debouncedGetData = debounce(this.debounce, this.getData);
}

handleInput(value) {
  this.debouncedGetData(value);
}

💎 为什么是重点
防抖是所有输入型组件的标配,Element UI通过此模式实现高性能

3. 父子通信机制(内部核心)

// 父组件
this.broadcast('ElAutocompleteSuggestions', 'visible', [val, $input.offsetWidth]);

// 子组件
this.$on('visible', (val, inputWidth) => {
  this.dropdownWidth = inputWidth + 'px';
  this.showPopper = val;
});

💎 为什么是重点
这是Element UI内部组件通信的通用模式,必须掌握


五、学习建议

1. 实践验证

<el-autocomplete
  v-model="state"
  :fetch-suggestions="querySearch"
  placeholder="请输入内容"
  :trigger-on-focus="false"
  @select="handleSelect">
  <template #default="{ item }">
    <div class="custom-item">
      {{ item.value }}
    </div>
  </template>
</el-autocomplete>

2. 调试技巧

  1. 无障碍验证
    • 使用屏幕阅读器(如NVDA)测试组件
    • 检查aria-activedescendant是否动态更新
  2. 性能测试
    • 快速输入测试防抖效果
    • 检查请求频率是否符合预期
  3. 边界测试
    • 输入空字符串时是否禁用建议列表
    • 选择未匹配项时是否触发selectWhenUnmatched

3. 深入思考

  1. 为什么不用props传递宽度

    • 宽度需要动态获取输入框宽度($input.offsetWidth
    • 通过事件传递比props更灵活(避免循环依赖)
  2. highlightFirstItem的作用

    • 聚焦时自动高亮第一个建议项
    • 提升用户体验(快速选择)
  3. suggestionDisabled的用途

    • 防止在输入为空时触发请求
    • 避免无效的API调用

💎 总结:这个组件完美展示了Element UI的无障碍设计性能优化内部通信机制。必须掌握的要点:

  1. 无障碍属性的完整链路(aria-activedescendant是关键)
  2. 防抖请求的实现逻辑
  3. 父子组件通信的broadcast/$on模式

重点记忆aria-activedescendant必须指向当前高亮项的ID,这是无障碍的核心!