preface

This is going to be a very painful time!

Today detailed analysis of Select source code, see the source code fully nine hundred lines, I am ignorant, this is so far read the most of a source, probably after browsing found it there are a lot of knowledge points, light import module has 16, which contains a variety of components, mixed with tools and functions, Therefore, in view of the limited space of this paper, I plan to write it in two parts, as follows: Select (Select) : Select (Select) : Select (Select) : Select (Select) : Select (Select) : Select (Select) : Select (Select)) : Select (Select) : Select (Select)) : Select (Select) : Select (Select))

Usage and features

Understanding a UI component starts with its functionality. Only after understanding the specific functionality of the component will you know why it is encapsulated. How to encapsulate? ElementUI’s official documentation has detailed functions and methods for use, so those unfamiliar with it may want to take a look. Select has the following uses:

  • Basic usage: A basic drop-down box that appears selectively above or below the page, depending on the size of the page
  • Disable options: You can disable an option or the entire drop-down box
  • Empty button: Needless to say, the Input component also has this functionality
  • Multiple selection: Select multiple drop-down options. The selected options can be displayed as a tag or combined into a single paragraph
  • Custom drop-down options: Drop-down options can be custom styles
  • Grouping: You can group drop-down options according to certain categories
  • Search: combined with the server, search lenovo
  • Create entry: You can create a tag to display in the input field

You can see that a Select function has so many, basically take all the functions we need to use in our daily needs into account, since so many functions, then packaging must be particularly troublesome, after all, someone else nine hundred lines of code is not written in vain!!

Clear the function this is very helpful for us to see the source code, we can find the corresponding code implementation according to the function, then directly on the source code:

The basic structure

el-select

It is best not to see the structure directly in the source code, so it is easy to get confused. First let’s take a look at the most basic use of the rendered HTML structure:

First, the outermost div is a class named el-select. If size is declared, we’ll add the el-select-size class based on size. Inside, we’ll wrap the div of el-Select__tags. Let’s take a look at el-input.

This el-Input is rendered as an Input component wrapped in ElementUI. If you haven’t seen it before, take a closer look at the Input source code in the ultra-detailed ElementUI source analysis series.

<el-input>
  <template slot="prefix" v-if="$slots.prefix">
    <slot name="prefix"></slot>
  </template>
  <template slot="suffix">
    <i v-show=! "" showClose" :class="['el-select__caret', 'el-input__icon', 'el-icon-' + iconClass]"></i>
    <i v-if="showClose" class="el-select__caret el-input__icon el-icon-circle-close" @click="handleClearClick"></i>
  </template>
</el-input>
Copy the code

The input component contains two slots, a prefix and a suffix. The prefix slot is used to show the Select component header content (if any), while the suffix is used to show the empty button and small arrows that follow. One detail here is that the small arrow uses V-show and the empty button uses V-if. Here is a brief description of the difference:

  • Both are useful for toggling the show and hide of DOM elements
  • v-showOperating on DOM elementsdisplay: noneProperty that does not change the structure of the DOM tree
  • v-ifIt operates on the DOM tree, directly adding or deleting DOM elements under control
  • v-ifIn the”Does not render when the initial condition is false“Until the first time true
  • v-ifThere are higher”Switching costs“,v-showThere are higher”Initial rendering overhead
  • That is to say,v-showSuitable for frequent DOM manipulation, whilev-ifIs used when you don’t have to manipulate its DOM structure as often

The events it triggers will be analyzed in the next issue, so be sure to read it on time.

Since the following structure uses many components that you haven’t seen before, the components referenced by Select will be examined first.

el-select-menu

Select-dropdown. vue = select-dropdown.vue = select-dropdown.vue The structure of this component is very simple, just a div with a slot in it. The class of this div is “el-select-dropdown el-popper”, and it renders to the page with the following structure:

You can see that this is a drop-down box structure that is added to the body node, positioned above or below the input field by position, and can be adjusted according to the position of the input field. The component itself is not very complicated, but it is mixed with vue-popper. In Vue-Popper, popper-manager is introduced, and vue-popper also introduces the third-party positioning library popper.js, so the relationship is complicated. See the following figure:

Here’s what each module does:

  • vue-popper: Pop-ups for managing components, when to create, where to create, when to destroy and how to destroy
  • popup: Is used to open and close the pop-up box
  • popup-manager: used to manage all modal layers in a page
  • popper.js: third-party library, mainly used to locate pop-ups

An analysis of popper.js is referenced in this CSDN blog post

Because the content of each module is very much, here only Select and Select component analysis, if you want to see the specific, you can go to my Github view.

Let’s go back to the Select component, el-Select-Menu is wrapped with el-ScrollBar for the content of the drop-down box to scroll, so the next content is the analysis of El-ScrollBar.

el-scrollbar

The entry file index.js imports the ScrollBar as SRC /main, which is the component file of el-ScrollBar. The official said that the whole idea of this file refers to the Gemini-Scrollbar. I went to compare it and found that the idea is exactly the same. Even the naming is the same, but others do compatible.

The files imported here are mainly:

  • utils/resize-event.js:resizeEvent binding and unbinding
  • utils/scrollbar-width.js: Calculates the width of the scroll bar
  • toObject: Merges all the objects in the array into one object
  • Bar: custom scroll bar component

main.js/render

For each file source I have been analyzed, first look at main.js inside the source:

// main.js
render(h) {
  // Get the width of the system's built-in scroll bar
  // scrollbarWidth(
  let gutter = scrollbarWidth();
  let style = this.wrapStyle;
  // If the scrollbar exists
  if (gutter) {
    // I think this place should be 'gutterWidth' but it doesn't matter
    const gutterWith = ` -${gutter}px`;
    const gutterStyle = `margin-bottom: ${gutterWith}; margin-right: ${gutterWith}; `;

    if (Array.isArray(this.wrapStyle)) {
      // toObject
      style = toObject(this.wrapStyle);
      style.marginRight = style.marginBottom = gutterWith;
    } else if (typeof this.wrapStyle === 'string') {
      style += gutterStyle;
    } else{ style = gutterStyle; }}// This is the outermost ul
  const view = h(
    this.tag,
    {
      class: ['el-scrollbar__view'.this.viewClass],
      style: this.viewStyle,
      ref: 'resize'
    },
    // Array of subvirtual nodes
    this.$slots.default
  );
  // ul enclosing div
  const wrap = (
    <div
      ref='wrap'
      style={style}
      onScroll={this.handleScroll}
      class={[
        this.wrapClass,
        'el-scrollbar__wrap',
        gutter? ' ': 'el-scrollbar__wrap--hidden-default']} >
      {[view]}
    </div>
  );
  let nodes;
  // Whether to use element scrollbars. Default is false
  // Use custom Bar components
  if (!this.native) {
    nodes = [
      wrap,
      <Bar move={this.moveX} size={this.sizeWidth}></Bar>,
      <Bar vertical move={this.moveY} size={this.sizeHeight}></Bar>
    ];
  } else {
    nodes = [
      <div
        ref='wrap'
        class={[this.wrapClass, 'el-scrollbar__wrap']}
        style={style}
      >
        {[view]}
      </div>
    ];
  }
  return h('div', { class: 'el-scrollbar' }, nodes);
},
Copy the code

As you can see, the main scrolling part of the drop-down box is to use the render function to build a DOM structure, as shown below:

The rendering of the Li tag is done by El-Option, which will be discussed later. The render function binds an onScroll event to the outer wrap, and the listening method is defined in methods:

// onScroll event handler
handleScroll() {
  const wrap = this.wrap;

  // Calculate how far the scrollbar needs to roll (%)
  this.moveY = (wrap.scrollTop * 100) / wrap.clientHeight;
  this.moveX = (wrap.scrollLeft * 100) / wrap.clientWidth;
},
Copy the code

When the internal list is scrolled, calculate how far the scrollbar needs to scroll, here is the percentage used, and use it in the Bar component. This Bar is an official custom scroll Bar component, also analyzed below. We notice that the component receives a native property that indicates whether or not to use the browser’s own scroll Bar. The default is false, that is, to use the Bar component instead, and then put the entire structure into H for Vue to parse.

main.js/methods

Methods defines two methods:

  • handleScroll:onscrollEvent handler
  • update: when the triggerresizeEvent, change the size of the scroll bar
update() {
  // Width and height percentage
  let heightPercentage, widthPercentage;
  const wrap = this.wrap;
  if(! wrap)return;

  // Find the percentage of the total size of the content that is visible. This is the percentage of the scrollbar relative to the content
  heightPercentage = (wrap.clientHeight * 100) / wrap.scrollHeight;
  widthPercentage = (wrap.clientWidth * 100) / wrap.scrollWidth;

  // The size of the scroll bar
  // If the viewable area is smaller than the total size of the content, prove that you need to scroll, assign the percentage to sizeXXX
  // If you don't need to scroll clientHeight = scrollHeight
  this.sizeHeight = heightPercentage < 100 ? heightPercentage + The '%' : ' ';
  this.sizeWidth = widthPercentage < 100 ? widthPercentage + The '%' : ' ';
}
Copy the code

When the container is mounted, call the update method. The noreSize property in the prop property of the component disables resizing. If the container size does not change, it is best to set it to optimize performance. As you can see later, the update method is called before the component is mounted and destroyed, and frequent calls to update can consume performance, so we do not want to adjust the frame size and declare the noreSize property as much as possible.

mounted() {
  if (this.native) return;
  // Update needs to use the updated DOM, so put it in $nextTick
  this.$nextTick(this.update);
  // If the frame can be resized, add a resize listening event to the element
  !this.noresize && addResizeListener(this.$refs.resize, this.update);
},

beforeDestroy() {
  if (this.native) return;
  // Remove the element's resize listening event
  !this.noresize && removeResizeListener(this.$refs.resize, this.update);
}
Copy the code

As for the addResizeListener method, it officially borrowed the third-party package resize- Observer-Polyfill to handle resize events. ResizeObserver is a new API with very good performance. Go to MDN for details.

Bar

Let’s look at the Bar component call:

<Bar move={this.moveX} size={this.sizeWidth}></Bar>,
<Bar vertical move={this.moveY} size={this.sizeHeight}></Bar>
Copy the code

Two or three arguments are passed:

  • move: Distance of horizontal or vertical movement
  • size: Size of scroll bar
  • vertical: Whether the scroll bar is vertical or horizontal

dom.js/on

The bar. Js file is the bar component, which imports the objects of two utility classes. As for the analysis of utility classes, I plan to write a special column in the later stage, here is a brief look at the related methods.

@param {HTMLElement} Element to bind to * @param {String} event to bind to * @param {Function} Handler The function */ that is executed when the event is triggered
export const on = (function() {
  if(! isServer &&document.addEventListener) {
    return function(element, event, handler) {
      if (element && event && handler) {
        // false indicates the bubbling phase
        // true indicates execution in the capture phase
        element.addEventListener(event, handler, false); }}; }else {
    // Add event listener using attachEvent in IE
    return function(element, event, handler) {
      if (element && event && handler) {
        element.attachEvent('on'+ event, handler); }}; }}) ();Copy the code

The on method is used to bind events to the specified element. A close look at the source shows that it uses an immediate function (IIEF) and then exports the result of its execution, which is still a function. Two questions arise:

  • Why use immediate execution functions?
  • Why return a function?

The second is that returning a function is obviously using a “closure”, which has the advantage of “access to the outer scope”. For example, isServer is a variable defined in dom.js. But using closures can cause a “memory leak” that would overwhelm our memory if not destroyed, which is why we use “execute functions now” to eliminate the side effects of closures.

bar.js/render

Back to the Bar component, we still use the render function to render the component:

render(h) {
  const { size, move, bar } = this;
  return (
    <div
      class={ ['el-scrollbar__bar', 'is-'+bar.key]}onMousedown={ this.clickTrackHandler } >
      <div
        ref="thumb"
        class="el-scrollbar__thumb"
        onMousedown={ this.clickThumbHandler }
        style={ renderThumbStyle({ size.move.bar }) }>
      </div>
    </div>
  );
},
Copy the code

The rendered structure can be seen in the previous image, which is two nested divs with onMouseDown events bound to handle mouse press events. In style, there is also a renderThumbStyle function.

export function renderThumbStyle({ move, size, bar }) {
  const style = {};
  // By how much
  const translate = `translate${bar.axis}(${ move }%) `;

  // Set the width/height of the scroll bar
  style[bar.size] = size;
  style.transform = translate;
  style.msTransform = translate;
  style.webkitTransform = translate;

  return style;
};
Copy the code

This function controls the movement of the scroll bar as it moves around the list. Take a look at the style changes before and after scrolling:

TranslateY has changed, that is, it simulates scrolling by panting, and the exact value is passed by the parent component (in this case, el-Scrollbar).

bar.js/methods

Let’s take a look at some of its methods, all related to event binding:

// Handle the event when the mouse button is pressed on the scroll bar
clickThumbHandler(e) {
  // prevent click event of right button
  // The ctrlKey event property returns a Boolean value indicating whether the Ctrl key is pressed and held when an event occurs
  // e.button = 2 indicates the right mouse button
  if (e.ctrlKey || e.button === 2) {
    return;
  }
  this.startDrag(e);
  this[this.bar.axis]
    = (e.currentTarget[this.bar.offset] 
    - (e[this.bar.client]
    - e.currentTarget.getBoundingClientRect()[this.bar.direction]));
},
Copy the code

If Ctrl is pressed or the right mouse button is pressed to stop the execution of the event, the startDrag method is executed when pressed

// Click and drag the scroll bar
startDrag(e) {
  // The rest of the listener functions for the current element will not be executed
  e.stopImmediatePropagation();
  this.cursorDown = true;

  // Bind the mouse movement event and mouse button lift event to document
  on(document.'mousemove'.this.mouseMoveDocumentHandler);
  on(document.'mouseup'.this.mouseUpDocumentHandler);
  // Disallows text to be selected
  / / reference https://www.jianshu.com/p/701cc19d2c5a
  document.onselectstart = (a)= > false;
}
Copy the code

Explain e.s topImmediatePropagation () method, we use less at ordinary times. When many events of the same type are bound to an element, the callbacks are executed in the order in which they were bound, but when we declare this method in the event handler, the rest of the listeners for the current element are not executed.

// Event handler when the mouse button is pressed in the area where the scroll bar is located
// When the mouse clicks on the blank area above the scrollbar, the scrollbar scrolls up
// When the mouse clicks on the space below the scrollbar, the scrollbar scrolls down
clickTrackHandler(e) {
  // Gets the distance between the click position and the top distance of the element
  // That is the offsetX/offsetY property of IE
  const offset
  	= Math.abs(e.target.getBoundingClientRect()[this.bar.direction] 
    - e[this.bar.client]);
  // Half the width/height of the scroll bar
  const thumbHalf = (this.$refs.thumb[this.bar.offset] / 2);
  const thumbPositionPercentage
 	  = ((offset - thumbHalf) * 100 / this.$el[this.bar.offset]);

  // For example
  // wrap.scrollTop = -10(false data) * wrap.scrollheight / 100
  this.wrap[this.bar.scroll]
    = (thumbPositionPercentage * this.wrap[this.bar.scrollSize] / 100);
}
Copy the code

This method is used to handle events that are clicked on the outer layer of the scroll bar. StartDrag uses a similar method for mousemove and Mouseup events, which I won’t cover here.

Look at the mouse event binding, we should pay attention to the event binding must have cancel listening, especially when the mouse movement of the event, in the source code is used to off method, specific and on similar.

Move on to the scrollbarWidth and toObject holes we dug in main.js/render

scrollbar-width.js

This file is very simple, is to calculate the width of the scrollbar system, I looked, the Internet is basically this method.

export default function() {
  if (Vue.prototype.$isServer) return 0;
  // Return scrollBarWidth if it exists
  if(scrollBarWidth ! = =undefined) return scrollBarWidth;

  const outer = document.createElement('div');
  outer.className = 'el-scrollbar__wrap';
  outer.style.visibility = 'hidden';
  outer.style.width = '100px';
  outer.style.position = 'absolute';
  outer.style.top = '-9999px';
  document.body.appendChild(outer);

  // Width without scroll bar = element offsetWidth
  const widthNoScroll = outer.offsetWidth;
  // Make the outer layer scrollable and the scrollbar appear
  outer.style.overflow = 'scroll';

  const inner = document.createElement('div');
  // When width is set to 100%, the child element's content width is forced to equal the parent element's content width
  // A scroll bar appears when the child element content width is greater than the parent element content width
  inner.style.width = '100%';
  outer.appendChild(inner);

  const widthWithScroll = inner.offsetWidth;
  outer.parentNode.removeChild(outer);
  scrollBarWidth = widthNoScroll - widthWithScroll;

  return scrollBarWidth;
};
Copy the code

Inner should not use offsetWidth but clientWidth, because offsetWidth contains the scroll bar, so it is impossible to calculate. I don’t know if they wrote it wrong or I understood it wrong. This method always gets 0 anyway, because I ran it on Chrome on Mac and couldn’t get the width of the scrollbar, and I couldn’t set overflow to keep the scrollbar there. I don’t know how it works on Windows. Please run through this method and let me know in the comments section. Thank you.

util.js/toObject

function extend(to, _from) {
  // _from will not loop if it is a basic data type
  for (let key in _from) {
    to[key] = _from[key];
  }
  return to;
}

// Convert all the objects in the array into one object
export function toObject(arr) {
  var res = {};
  for (let i = 0; i < arr.length; i++) {
    if(arr[i]) { extend(res, arr[i]); }}return res;
}
Copy the code

The method here is very simple, which is to iterate over all the properties of the objects in the array to a new object, if the array has a basic data type will be skipped.

At this point, our Scrollbar has been fully analyzed, but not to the end of the scattering, then there is the el-Option component.

el-option

The el-Option section contains the component itself and an El-Option-group component. Option is the component that actually renders the drop-down list, which is rendered to the page as the

  • tag. This option template structure has a default slot for displaying the text content of the list item. Because option is a lot of and select component methods, so I’m going to put in the next article to analyze, first look at some simple:
  • // Check whether the two parameters are equal
    isEqual(a, b) {
      if (!this.isObject) {
        return a === b;
      } else {
        // Get valueKey for the select component instance
        // valueKey is the key name that uniquely identifies a value. This parameter is mandatory when the binding value is an object type
        const valueKey = this.select.valueKey;
        returngetValueByPath(a, valueKey) === getValueByPath(b, valueKey); }}Copy the code

    GetValueByPath is imported from util. This method is used to access properties specified by the object:

    /** * Deep access Object properties * @param {Object} Object target Object * @param {string} property name xxx.xxx.xxx
    export const getValueByPath = function(object, prop) {
      prop = prop || ' ';
      // paths => [xxx, xxx, xxx]
      // object: {
      // xxx: {
      // xxx: {
      // xxx: 'xxx'
      / /}
      / /}
      // }
      const paths = prop.split('. ');
      // Save the object to avoid changing the original object
      let current = object;
      let result = null;
      for (let i = 0, j = paths.length; i < j; i++) {
        const path = paths[i];
        if(! current)break;
    
        When the specified attribute name is reached, its attribute value is returned
        if (i === j - 1) {
          result = current[path];
          break;
        }
        // If not, continue to traverse
        current = current[path];
      }
      return result;
    };
    Copy the code

    But in my opinion, the official implementation could be simpler, since since the property names are stored in an array, wouldn’t it be better to use the array method, and then use a while loop to implement a few lines of code:

    function getValByPath(obj, path) {
      const paths = path.split('. ')
      let res = obj
      let prop
      while ((prop = paths.shift())) {
        res = res[prop]
      }
      return res
    }
    Copy the code

    Here’s another method:

    // The event listening method that is triggered when the mouse moves
    hoverItem() {
      // If the current item is not disabled, set the select component's 'hoverIndex'
      // Its value is the index of the current list item in the Options array
      if (!this.disabled && !this.groupDisabled) {
        this.select.hoverIndex = this.select.options.indexOf(this); }}Copy the code

    Hover status is displayed when the mouse moves over a list item, which is implemented in select. The rest of them need to be analyzed later. The liver is not moving… As for the option-group, the contents are very simple, not much different from the option-group. You just need to have a quick glance, so I won’t write here.

    Summary and reflection

    So at this point, we’ve finally finished analyzing the template part of the Select component, and notice that this is the template part, and the real bulk of it is missing, and in select methods are the majority, about 400 lines, and the rest is just a bunch of properties and lifecycle hooks, and methods in my next article, To summarize the SELECT template:

    • First of all, the wholeselectComponent consists of an input box and a drop – down box
    • The input field usesel-inputComponent, the drop-down box usesel-select-menucomponent
    • el-select-menuIs added to the body node throughv-showToggle show and hide
    • The drop-down box contains a built-in scroll componentel-scrollbar
    • Custom scroll bars are used in the scroll componentBarcomponent
    • List items are passedel-optionel-option-grouprenderedulThe labels andliThe label

    There are still some unresolved questions for this article:

    • el-tagComponents are not analyzed in detail
    • Vue animationtransitionComponent not analyzed

    To summarise my feeling see the select components, see the source code and write the article it took me a week’s time, only to see a small part, have to say that it involves the knowledge too much too much, for I such a front-end small white is too difficult, it often have a week to see not go down, Some knowledge I have never seen, by constantly see documents, constantly check my blog, gradually into a state of a kind of enjoyment, you take part of the code to run browser, a breakpoint suddenly understand principle (I really don’t know how to put the whole project running, too, I tried many ways to all have no success). During this period, I also read a lot of Vue tutorials and APIS, and gradually updated my cognition of Vue. I believe THAT I will be more proficient in using Vue in future development, because it will be easier to understand concepts with actual projects. Gradually, when you accumulate enough knowledge, you can form a complete knowledge loop. “Asking a few whys” while looking at the source code can really help you understand it. Another is the design thought of component, it is a very abstract thing, you see one or two components alone is no way to understand, you need a lot of reading component source code, and know what it has solved the problem, have what function, why such design, when you see a component can quickly understand the three questions so component of thoughts you have. This is something that you have to do a lot of reading and practice. You can’t do it just by reading a few blogs and looking at two components.

    If you like this article, please click a “like” to let more people see it. If there is something wrong in the article, please point out, you can also add my wechat [Liu472362746] to discuss, and I will also push the detailed code to Github warehouse.

    portal

    [2020.3.15] Ultra detailed ElementUI source code analysis — Input

    [2020.3.16] Ultra detailed ElementUI source code analysis — Layout

    [2020.3.18] Analysis of ElementUI source code