Серия анализа исходного кода элемента 7-Select (раскрывающееся поле выбора)

внешний интерфейс JavaScript Element Vue.js

Введение

Диаграмма раскрывающегося списка элементов выглядит следующим образом.

Это действительно красиво, интерактивный опыт очень хорош, html имеет собственный селектор.<select>, но это слишком некрасиво, да и стили каждого браузера не унифицированы, поэтому чтобы сделать красивый и практичный выпадающий селектор, вы должны сами смоделировать все методы и структуры.Выпадающий селектор Element имеет очень большой количество кода, толькоselect.vueФайл имеет длину почти 1000 строк и состоит из других компонентов Element. Если считать другие компоненты, вам нужно добавить строки 1000. Наконец, этот селектор ссылается на множество утилит и сторонних js, плюс вам нужно добавьте не менее 2000 строк к вышесказанному, чтобы вы могли проанализировать только некоторые из основных принципов.Ниже приведен импорт раскрывающегося селектора

import Emitter from 'element-ui/src/mixins/emitter';
import Focus from 'element-ui/src/mixins/focus';
import Locale from 'element-ui/src/mixins/locale';
import ElInput from 'element-ui/packages/input';
import ElSelectMenu from './select-dropdown.vue';
import ElOption from './option.vue';
import ElTag from 'element-ui/packages/tag';
import ElScrollbar from 'element-ui/packages/scrollbar';
import debounce from 'throttle-debounce/debounce';
import Clickoutside from 'element-ui/src/utils/clickoutside';
import { addClass, removeClass, hasClass } from 'element-ui/src/utils/dom';
import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
import { t } from 'element-ui/src/locale';
import scrollIntoView from 'element-ui/src/utils/scroll-into-view';
import { getValueByPath } from 'element-ui/src/utils/util';
import { valueEquals } from 'element-ui/src/utils/util';
import NavigationMixin from './navigation-mixin';
import { isKorean } from 'element-ui/src/utils/shared';

Однако многое в этом импорте стоит изучить, код официального сайтакликните сюда

HTML структура выпадающего селектора

Давайте сначала проанализируем html-структуру этого выпадающего селектора.Упрощенный html-код выглядит следующим образом.

<template>
    <div class="el-select" >
        <div class="el-select__tags"
        </div>
        
        <el-input></el-input>
        
        <transition>
            <el-select-menu>
            <el-select-menu>
        </transtion>
    </div>
</template>

Самый внешний div оборачивает все дочерние элементы (относительное позиционирование), а первый div внутри — это обертывающий div, отображающий тег раскрывающегося селектора, как показано на рисунке ниже, этот div абсолютно позиционируется, а затем передаетсяtop:50%;transform:translateY(-50%)Центрировать по вертикали внутри самого внешнего div

затем второй<el-input>Это компонент ввода, инкапсулированный элементом.Как упоминалось в предыдущей статье, ширина этого поля ввода такая же, как ширина самого внешнего div, как показано на рисунке ниже, кнопка со стрелкой справа находится в его отступе. позиция

тогда последний<transtion>Это не компонент, это признак анимации перехода Vue, он не будет отображаться, он завернут внутрь<el-select-menu>Это также компонент, инкапсулированный Element, который представляет собой всплывающее раскрывающееся меню и также имеет абсолютное позиционирование.Следовательно, во всем раскрывающемся компоненте только средний ввод является относительным позиционированием, а остальные — абсолютным позиционированием, и вы должны хорошо уметь повторно использовать существующие компоненты, а не переписывать их с нуля.

Анализ исходного кода некоторых функций

Если вы хотите написать все функции, это займет как минимум неделю, поэтому вы можете написать только часть.

Логическая сортировка основного процесса работы выпадающего списка

Далее анализируется основной процесс работы выпадающего списка и процесс передачи данных в нем.
Сначала посмотрите на использование раскрывающегося списка, код официального сайта выглядит следующим образом.

<el-select v-model="value" placeholder="请选择">
    <el-option
      v-for="item in options"
      :key="item.value"
      :label="item.label"
      :value="item.value">
    </el-option>
</el-select>

Часть данных выглядит следующим образом

<script>
  export default {
    data() {
      return {
        options: [{
          value: '选项1',
          label: '黄金糕'
        }, {
          value: '选项2',
          label: '双皮奶'
        }]
        value: ''
      }
    }
  }
</script>

видимый крайний<el-select>Существует v-модель, которая представляет собой использование v-модели компонента.Подробности см. на официальном сайте.Значение изначально пустое.При выборе пункта выпадающего меню значение становится стоимость этого предмета.<el-select>Внутри метки используйте v-for, чтобы зациклить все параметры,<el-option>Это также компонент, инкапсулированный элементом. Можно четко связать событие щелчка с указанным выше. Параметры состоят из метки и значения, которые представляют отображаемый текст и фактическое значение раскрывающегося элемента соответственно, и параметры в данных также предоставляют соответствующий ключ.

Обратите внимание здесь<el-option>вставляется как прорезь в<el-select>, так что в<el-select>должен иметь<slot>для переноса контента, если компонент не содержит , любое переданное в него содержимое будет отброшено. Проверьте html-код и обнаружите, что расположение слота выглядит следующим образом.

<el-select-menu
        <el-scrollbar>
          <el-option>
          </el-option>
          
          <slot></slot>
          
        </el-scrollbar>
        <p
         ...
        </p>
</el-select-menu>

слот содержится в<el-scrollbar>В этом компоненте полосы прокрутки реализация этого компонента представляет собой проверку базовых навыков, немного усложненную, и кодкликните сюда, поэтому все параметры будут помещены в компонент полосы прокрутки.

Запускается, когда пользователь щелкает раскрывающийся список в исходном состоянии.toggleMenuОтображается выпадающее меню, toggleMenu выглядит следующим образом

toggleMenu() {
    if (!this.selectDisabled) {
      if (this.menuVisibleOnFocus) {
        this.menuVisibleOnFocus = false;
      } else {
        this.visible = !this.visible;
      }
      if (this.visible) {
        (this.$refs.input || this.$refs.reference).focus();
      }
    }
},

Из кода видно, что сначала судят, отключено ли оно, если оно в отключенном состоянии, событие не сработает, а потом судятthis.menuVisibleOnFocus, Для чего это?Посмотрев внимательно исходный код, можно увидеть, что в состоянии множественного выбора, то есть несколько тегов могут быть рядом на картинке ниже, а еще одно поле ввода в компоненте (у курсора на картинке ниже). сделано здесь.this.visible = !this.visibleЗатем это предложение для переключения состояния выпадающего меню

После отображения раскрывающегося меню щелчок по параметру закроет раскрывающееся меню и передаст значение родительскому компоненту.Давайте сначала посмотрим на содержимое компонента параметра.

<template>
  <li
    @mouseenter="hoverItem"
    @click.stop="selectOptionClick"
    class="el-select-dropdown__item"
    v-show="visible"
    :class="{
      'selected': itemSelected,
      'is-disabled': disabled || groupDisabled || limitReached,
      'hover': hover
    }">
    <slot>
      <span>{{ currentLabel }}</span>
    </slot>
  </li>
</template>

Очень простой, инкапсулированный элементом li,@mouseenter="hoverItem"Это предложение показывает, что событие hoverItem срабатывает, когда ваша мышь наводит курсор на элемент.Здесь вы можете спросить, почему вы делаете это при наведении курсора мыши? На самом деле, здесь есть такая операция: когда вы наводите указатель мыши на параметр, нажатие клавиши ввода также может привести к выбору элемента.Конечно, вы также можете щелкнуть, поэтому при использовании указателя необходимо обновить, и посмотрите на содержание hoverItem

hoverItem() {
    if (!this.disabled && !this.groupDisabled) {
      this.select.hoverIndex = this.select.options.indexOf(this);
    }
},

???Черный вопросительный знак! Что это делает? Это просто оператор присваивания, не паникуйте, посмотрите сначалаthis.selectЧто это такое? После поиска я обнаружил, что select находится в следующей позиции

inject: ['select'],

Это не реквизит и не данные, этовнедрение зависимости, Основная идея внедрения зависимостей состоит в том, чтобы разрешить компонентам-потомкам доступ к содержимому компонентов-предков, потому что, если это компонент «родитель-потомок», вы можете получить доступ к родительскому компоненту через $parent, но как насчет компонента-прародителя? Таким образом, с внедрением зависимостей использование внедрения зависимостей очень просто, объявите следующий атрибут предоставления в компоненте-предке, значение - это метод или атрибут компонента-предка.

provide: function () {
  return {
    xxMethod: this.xxMethod
  }
}

Затем внутри компонента-потомка объявите следующее

inject: ['xxMethod']

Затем вы можете использовать xxMethod в компонентах-потомках.Оглядываясь назад на выбор инъекции зависимостей компонента option, его местоположение находится в компоненте-предке (а не в родительском компоненте).<el-select>, то есть в компоненте раскрывающегося списка этой статьи, как показано ниже.

 provide() {
      return {
        'select': this
      };
    },

Он возвращает это, которое ссылается на экземпляр компонента раскрывающегося селектора, поэтому его можно передать черезthis.select.hoverIndexСвойство hoverIndex в раскрывающемся селекторе, затем продолжите анализthis.select.hoverIndex = this.select.options.indexOf(this), это предложение означает, что после нажатия Enter присвоить порядковый номер опции, на которой висит мышь в опциях, к hoverIndex, а значит найти порядковый номер приостановленной опции в массиве, а дальше вся остальная логика в<el-select>обработанный. Как упоминалось ранее, нажатие Enter также может быть выбрано при наведении курсора мыши, как это достигается? Можно догадаться, что событие keydown.enter должно быть привязано к инпуту, такое предложение на инпуте есть в исходниках

@keydown.native.enter.prevent="selectOption"

Что за дело с таким количеством модификаторов здесь? Нативный модификатор необходим.На официальном сайте сказано, что v-on можно использовать только в компонентах для мониторинга пользовательских событий.Для мониторинга нативных событий необходимо использовать нативные модификаторы.Предотвратить — предотвратить запуск события ввода по умолчанию, такого как нажав Enter, чтобы отправить форму. , конечно, нет. Затем посмотрите на метод selectOption

 selectOption() {
        if (!this.visible) {
          this.toggleMenu();
        } else {
          if (this.options[this.hoverIndex]) {
            this.handleOptionSelect(this.options[this.hoverIndex]);
          }
        }
      },

Здесь hoverIndex используется для обновления выбранного элемента, см. далееhandleOptionSelectКак обновить выбранный элемент, этот метод проходит в экземпляре параметра

 handleOptionSelect(option, byClick) {
        if (this.multiple) {
          const value = this.value.slice();
          const optionIndex = this.getValueIndex(value, option.value);
          if (optionIndex > -1) {
            value.splice(optionIndex, 1);
          } else if (this.multipleLimit <= 0 || value.length < this.multipleLimit) {
            value.push(option.value);
          }
          this.$emit('input', value);
          this.emitChange(value);
          ...
        } else {
          this.$emit('input', option.value);
          this.emitChange(option.value);
          this.visible = false;
        }
        ...
      },

Здесь сохраняется только основная логика.Видно, что первое, что нужно определить, является ли это состояние множественного выбора, потому что в состоянии множественного выбора<el-select v-model="value">Значение v-model представляет собой массив. В состоянии одиночного выбора это одно значение. Если это множественные выборки, сначала получите копию значения. Необходимо выяснить, что это за значение. На самом деле , значение является опорой этого компонента, который является v- продуктом синтаксического разделения сахара модели, то есть значением в приведенной выше v-модели, которое является элементом данных в данных, переданных пользователем, поэтому изменение этого значения приведет к изменению значения, переданного пользователем. Затем используйте indexOf, чтобы узнать, есть ли параметр option в массиве значений. Если он существует, соединение будет удалено. Если он не существует, вставьте его, чтобы событие ввода родительского компонента было вызвано излучением чтобы изменить значение, и изменение родительского компонента запускается, чтобы уведомить пользователя о моем значении изменилось! Если это состояние с одним выбором, это может быть просто, просто отправьте напрямую.

Срабатывает, когда опция щелкается непосредственно мышью@click.stop="selectOptionClick"selectOptionНажмите

selectOptionClick() {
        if (this.disabled !== true && this.groupDisabled !== true) {
          this.dispatch('ElSelect', 'handleOptionClick', [this, true]);
        }
      },

Этот метод использует общий метод отправки в<el-select>Событие handleOptionClick запускается в , и передается текущий экземпляр параметра. Эта отправка фактически завершает логику передачи событий от дочерних компонентов к компонентам-предкам.<el-select>Должен быть метод on для получения события, как показано ниже.

this.$on('handleOptionClick', this.handleOptionSelect)

Можно видеть, что этот handleOptionSelect и приведенное выше являются методом, поэтому щелчок по параметру и нажатие клавиши ввода в конечном итоге вызовет этот метод для обновления значения.

Таким образом, это полное описание логики процесса.

Щелкните за пределами поля выбора, чтобы свернуть раскрывающееся меню.

Просмотр самого внешнего кода div

<div
    class="el-select"
    :class="[selectSize ? 'el-select--' + selectSize : '']"
    @click.stop="toggleMenu"
    v-clickoutside="handleClose">

здесь@clickПривяжите событие щелчка, чтобы переключить скрытие и отображение меню, следующиеv-clickoutside="handleClose"В этом суть, это директива Vue, логика в handleClose такаяthis.visible = falseУстановите видимость меню на false, чтобы скрыть раскрывающееся меню. Когда диапазон щелчка мыши находится за пределами раскрывающегося компонента, срабатывает этот handleClose. Это очень распространенное требование, но реализация здесь не очень проста.Основная идея состоит в том, чтобы привязать событие mouseup к документу, а затем определить в этом событии, включена ли щелкнутая цель в целевой компонент., Объект, соответствующий этой инструкции, передаетсяimport Clickoutside from 'element-ui/src/utils/clickoutside'Введен, потому что многие компоненты используют этот метод, поэтому они извлекаются отдельно и помещаются в каталог util.кликните сюдаВведите метод привязки этого метода и увидите следующие 2 предложения

!Vue.prototype.$isServer && on(document, 'mousedown', e => (startClick = e));
!Vue.prototype.$isServer && on(document, 'mouseup', e => {
  nodeList.forEach(node => node[ctx].documentHandler(e, startClick));
});

Это привязывает событие нажатия и подъема мыши к документу (рендеринг на стороне сервера недействителен), записывает нажатый элемент DOM при нажатии, проходит все DOM с инструкцией при его поднятии, а затем выполняет documentHandler для оценки , этот метод следующим образом

function createDocumentHandler(el, binding, vnode) {
  return function(mouseup = {}, mousedown = {}) {
    if (!vnode ||
      !vnode.context ||
      !mouseup.target ||
      !mousedown.target ||
      el.contains(mouseup.target) ||
      el.contains(mousedown.target) ||
      el === mouseup.target ||
      (vnode.context.popperElm &&
      (vnode.context.popperElm.contains(mouseup.target) ||
      vnode.context.popperElm.contains(mousedown.target)))) return;

    if (binding.expression &&
      el[ctx].methodName &&
      vnode.context[el[ctx].methodName]) {
      vnode.context[el[ctx].methodName]();
    } else {
      el[ctx].bindingFn && el[ctx].bindingFn();
    }
  };
}

Обратите внимание, что это генерируется createDocumenthandler обработчиком документов в первом IF в нем.el.contains(mouseup.target),el.contains(mousedown.target)Нативный метод contains используется для определения того, содержит ли щелчок элемент el dom, если да, вернуть, если нет, то есть, если щелчок находится за пределами выпадающего меню, выполнитьvnode.context[el[ctx].methodName]()передачаv-clickoutside="handleClose"Метод handleClose скрывает раскрывающееся меню,el[ctx].methodNameОн инициализируется в методе привязки инструкции следующим образом.

bind(el, binding, vnode) {
    nodeList.push(el);
    const id = seed++;
    el[ctx] = {
      id,
      documentHandler: createDocumentHandler(el, binding, vnode),
      methodName: binding.expression,
      bindingFn: binding.value
    };
  },

Назначьте выражение для methodName, что такое ctx? ктх сверхуconst ctx = '@@clickoutsideContext'Я думаю, что это предложение, чтобы добавить атрибут в домен el.Имя этого атрибута начинается с 2 @, что означает, что он очень особенный и не может быть легко перезаписан.Тогда значение этого атрибута является объектом, который хранит много информации.Логика здесь обычно такова: когда инструкция привязывается к элементу dom в первый раз, добавьте атрибуты, такие как метод, который будет выполняться, к элементу dom, затем привяжите событие mouseup к документу, а затем выньте dom из соответствующий элемент, когда пользователь нажимает Принять решение, если оно признано верным, затем вынуть ранее связанный метод в dom для выполнения

Расположение выпадающих меню

Вы можете подумать, что это выпадающее меню абсолютно позиционировано в поле ввода, тогда вы ошибаетесь, на самом деле это выпадающее меню добавлено на document.body

Разве это не удивительно, когда в начальном состоянии не нажимается поле выбора, в раскрывающемся меню отображается: нет, на этот раз абсолютно позиционировано и включено в<el-select>внутри см. ниже

Однако, когда мы нажимаем на компонент, выпадающее меню переходит в тело

Зачем это делать? На официальном сайте указано, что выпадающее меню добавлено в тело по умолчанию, но его можно модифицировать. Это связано с тем, что элемент использует сторонний js:popper.js, это js, используемый для работы с всплывающим окном, более 1000 строк, а затем Element написал vue-popper.vue для дальнейшего управления, этот файл имеет следующий код

 createPopper() {
      ...
      if (!popper || !reference) return;
      if (this.visibleArrow) this.appendArrow(popper);
      
      if (this.appendToBody) document.body.appendChild(this.popperElm);
      
      if (this.popperJS && this.popperJS.destroy) {
        this.popperJS.destroy();
      }
      ...
      this.popperJS = new PopperJS(reference, popper, options);
      this.popperJS.onCreate(_ => {
        this.$emit('created', this);
        this.resetTransformOrigin();
        this.$nextTick(this.updatePopper);
      });
    
    },

creatPopper — это логика при инициализации, котораяif (this.appendToBody) document.body.appendChild(this.popperElm)Это предложение является ключевым. Переместите всплывающее раскрывающееся меню в тело через appendChild. Обратите внимание, что appendChild переместит его, если параметр является существующим элементом. Затем вы обнаружите, что выпадающее меню также будет перемещаться при прокрутке колесика мыши.Обратите внимание, что выпадающее меню находится на теле, поэтому логика движения здесь реализована в popperJS, что немного сложно. , в нем должен быть addEventListener для прослушивания события прокрутки. , проверка действительно имеет

Popper.prototype._setupEventListeners = function() {
        // NOTE: 1 DOM access here
        this.state.updateBound = this.update.bind(this);
        root.addEventListener('resize', this.state.updateBound);
        // if the boundariesElement is window we don't need to listen for the scroll event
        if (this._options.boundariesElement !== 'window') {
            var target = getScrollParent(this._reference);
            // here it could be both `body` or `documentElement` thanks to Firefox, we then check both
            if (target === root.document.body || target === root.document.documentElement) {
                target = root;
            }
            target.addEventListener('scroll', this.state.updateBound);
            this.state.scrollTarget = target;
        }
    };

приведенное выше предложениеtarget.addEventListener('scroll', this.state.updateBound);Это связать прослушиватель событий, продолжить смотреть на updateBound и обнаружить, что он привязан к этому через метод обновления, и обновление выглядит следующим образом.

 /**
     * Updates the position of the popper, computing the new offsets and applying the new style
     * @method
     * @memberof Popper
     */
    Popper.prototype.update = function() {
        var data = { instance: this, styles: {} };

        // store placement inside the data object, modifiers will be able to edit `placement` if needed
        // and refer to _originalPlacement to know the original value
        data.placement = this._options.placement;
        data._originalPlacement = this._options.placement;

        // compute the popper and reference offsets and put them inside data.offsets
        data.offsets = this._getOffsets(this._popper, this._reference, data.placement);

        // get boundaries
        data.boundaries = this._getBoundaries(data, this._options.boundariesPadding, this._options.boundariesElement);

        data = this.runModifiers(data, this._options.modifiers);

        if (typeof this.state.updateCallback === 'function') {
            this.state.updateCallback(data);
        }
    };

Как следует из названия, обновление используется для обновления информации о местоположении всплывающего окна, и существуют различные подметоды для обновления соответствующего местоположения.