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-show
Operating on DOM elementsdisplay: none
Property that does not change the structure of the DOM treev-if
It operates on the DOM tree, directly adding or deleting DOM elements under controlv-if
In the”Does not render when the initial condition is false“Until the first time truev-if
There are higher”Switching costs“,v-show
There are higher”Initial rendering overhead“- That is to say,
v-show
Suitable for frequent DOM manipulation, whilev-if
Is 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 destroypopup
: Is used to open and close the pop-up boxpopup-manager
: used to manage all modal layers in a pagepopper.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
:resize
Event binding and unbindingutils/scrollbar-width.js
: Calculates the width of the scroll bartoObject
: Merges all the objects in the array into one objectBar
: 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
:onscroll
Event handlerupdate
: when the triggerresize
Event, 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 movementsize
: Size of scroll barvertical
: 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
// 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 whole
select
Component consists of an input box and a drop – down box - The input field uses
el-input
Component, the drop-down box usesel-select-menu
component el-select-menu
Is added to the body node throughv-show
Toggle show and hide- The drop-down box contains a built-in scroll component
el-scrollbar
- Custom scroll bars are used in the scroll component
Bar
component - List items are passed
el-option
和el-option-group
renderedul
The labels andli
The label
There are still some unresolved questions for this article:
el-tag
Components are not analyzed in detail- Vue animation
transition
Component 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