Element-ui ScrollBar component source code in-depth analysis

The scrollbar component root directory contains the index.js file and the SRC folder. The index.js file is used to register the Vue plug-in. The SRC directory is the core code of scrollbar. The entry file is main.js.

Before we start analyzing the source code, let’s talk about the principle of custom scroll bar, so that we can better understand.

When the wrap overflows, it creates a native scroll bar for each browser. To customize the scroll bar, we must eliminate the native scroll bar. Suppose we wrap a div around the wrap, and set it to overflow: Hidden, and set the wrap’s marginRight,marginBottom to a negative value exactly equal to the width of the original scroll bar, At this point, the parent container’s overflow: Hidden property allows the original scroll bar to be hidden. We then place the custom scroll bar absolutely to the right and bottom of the Wrap container and add the scroll logic, such as scroll and drag events, to implement the custom scroll bar.

Let’s take a closer look at how Element implements this logic, starting with the main.js entry.

The main. Js file directly exports an object that uses the render function to render the Scrollbar component. The exposed interface of the component is as follows:

props: {
  native: Boolean.// Whether to use native scroll (i.e. just hide the native scroll bar, but do not use custom scroll bar)
  wrapStyle: {},  // Define the style of the wrap container inline
  wrapClass: {},  // Define the style of the wrap container with the class name
  viewClass: {},  // Inline custom view container style
  viewStyle: {},  // Define the style of the view container with the class name
  noresize: Boolean.// If the Container size does not change, it is best to set it to optimize performance
  tag: {  				// The view container is rendered with that tag, which defaults to div
    type: String.default: 'div'}}Copy the code

As you can see, this is the exposed interface of the entire ScrollBar component, including the custom Wrap, View-style interface, and the NoreSize interface for optimizing performance.

Then let’s analyze the render function:

render(){
	let gutter = scrollbarWidth();  // Use the scrollbarWidth() method to get the width of the browser's native scrollbar
  let style = this.wrapStyle;

  if (gutter) {
    const gutterWith = ` -${gutter}px`;
    
    // Define the marginBottom and marginRight to be applied to the wrap container, with values negative of the browser scrollbar width calculated above
    const gutterStyle = `margin-bottom: ${gutterWith}; margin-right: ${gutterWith}; `;

    // This part mainly deals with the style according to the data type of the style passed in by the interface wrapStyle. The resulting style may be an object or a string
    if (Array.isArray(this.wrapStyle)) {
      style = toObject(this.wrapStyle);
      style.marginRight = style.marginBottom = gutterWith;
    } else if (typeof this.wrapStyle === 'string') {
      style += gutterStyle;
    } else{ style = gutterStyle; }}... }Copy the code

The most important piece of code in this section is the way to get the width of the browser’s native scroll bar, ScrllbarWidth is defined for element. This method is imported from the outside: import scrollbarWidth from ‘element-ui/ SRC /utils/scrollbar-width’; Let’s take a look at this function:

import Vue from 'vue';

let scrollBarWidth;

export default function() {
  if (Vue.prototype.$isServer) return 0;
  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);

  const widthNoScroll = outer.offsetWidth;
  outer.style.overflow = 'scroll';

  const inner = document.createElement('div');
  inner.style.width = '100%';
  outer.appendChild(inner);

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

  return scrollBarWidth;
};
Copy the code

Create a body element outer with a fixed width of 100px and set overflow to scroll to create a scroll bar. Then create a body element inner with a width of 100%. Since outer has a scrollbar, the width of inner cannot be equal to outer’s width, so subtract the width of outer from the width of inner to get the browser scrollbar width. Finally, destroy the outer element from the body and create it dynamically.

Back to the Render function, after dynamically generating the style variable style based on the browser ScrollBar width and wrapStyle, it’s time to generate the HTML for the ScrollBar component in the Render function.

// Generate the View node and insert the default slots content under the View node
const view = h(this.tag, {
  class: ['el-scrollbar__view'.this.viewClass],
  style: this.viewStyle,
  ref: 'resize'
}, this.$slots.default);

// Generate a wrap node and bind the scroll event to wrap
const wrap = (
  <div
  	ref="wrap"
  	style={ style }
		onScroll={ this.handleScroll }
		class={ [this.wrapClass, 'el-scrollbar__wrap', gutter? ' ': 'el-scrollbar__wrap--hidden-default'] }>
  		{ [view] }
	</div>
);
Copy the code

Then the wrap is assembled from native, and the view generates the entire HTML node tree.

let nodes;

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

You can see that if native is false, the custom scrollbar is used; if true, the custom scrollbar is not used. Simplify the render function above to generate the following HTML:

<div class="el-scrollbar">
  <div class="el-scrollbar__wrap">
    <div class="el-scrollbar__view">
    	this.$slots.default
    </div>
  </div>
  <Bar vertical move={ this.moveY } size={ this.sizeHeight} / >
  <Bar move={ this.moveX } size={ this.sizeWidth} / >
</div>
Copy the code

The outermost el-scrollbar sets overflow:hidden to hide the browser’s native scrollbar generated in the wrap. When a ScrollBar is built, the content written to the ScrollBar component is slotted into the view. In addition, move, size and vertical interfaces are used to call the Bar component, which is Track and Thumb on the schematic diagram. Let’s look at the Bar component:

props: {
  vertical: Boolean.// Whether the current Bar component is a vertical scroll Bar
  size: String.// Percentage, the percentage of current Bar component's thumb length/track length
  move: Number   // Scroll down/right takes the value of transform: translate
},
Copy the code

The behavior of the Bar component is controlled by these three interfaces. In the previous analysis, we saw that these three props were passed in when the Bar component was called in the Scrollbar. How does the parent component initialize and update the values of these three parameters to update the Bar component? Mounted hook (); update ();

update() {
  let heightPercentage, widthPercentage;
  const wrap = this.wrap;
  if(! wrap)return;

  heightPercentage = (wrap.clientHeight * 100 / wrap.scrollHeight);
  widthPercentage = (wrap.clientWidth * 100 / wrap.scrollWidth);

  this.sizeHeight = (heightPercentage < 100)? (heightPercentage +The '%') : ' ';
  this.sizeWidth = (widthPercentage < 100)? (widthPercentage +The '%') : ' ';
}
Copy the code

Here you can see, the core content is to calculate the length of the thumb heightPercentage/widthPercentage. Here we use wrap.clientheight/wrap.scrollheight to get the percentage of the thumb length. Why is that

By analyzing the schematic diagram of scrollbar we drew earlier, thumb scrolls up and down in track, scrollable region view scrolls up and down in visible region wrap, the relative relation between Thumb and track can be regarded as a miniature model of the relative relation between wrap and View (miniature reaction). The meaning of the scroll bar is to reflect the relative motion of the view and wrap. On the other hand, we can think of a view scrolling in a wrap as a wrap scrolling up and down in a view, just like a larger scroll bar.

ClientHeight/wrap.scrollHeight = thumb. ClientHeight/track.clientHeight. ClientHeight = thumb. ClientHeight = track.clientHeight = thumb.

One other thing to note is that when the ratio is greater than or equal to 100% (wrap.clientheight is greater than or equal to wrap.scrollHeight), the scrollbar is no longer needed, so size is set to an empty string.

Now let’s look at move, which is the update of the scroll position of the scroll bar.

handleScroll() {
  const wrap = this.wrap;

  this.moveY = ((wrap.scrollTop * 100) / wrap.clientHeight);
  this.moveX = ((wrap.scrollLeft * 100) / wrap.clientWidth);
}
Copy the code

MoveX /moveY is used to control the scrolling position of the scroll Bar. When this value is passed to the Bar component, renderThumbStyle is called in the Render function of the Bar component to convert it to trumb’s style transform: TranslateX (${moveX}%)/transform: translateY(${moveY}%) According to the similarity of the previous analysis, when wrap.scrollTop is exactly equal to wrap.clientHeight, thumb should scroll down its own length distance, i.e. Transform: translateY(100%). So when the wrap scrolls, the thumb should scroll down just as far as transform: translateY(wrap.scrollTop/wrap.clientheight). This is where the logic in the wrap scroll function handleScroll comes in.

Now that we’ve fully figured out all the logic in the ScrollBar component, let’s look at what the Bar component does when it receives props.

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 Render function gets the size passed by the parent. After move, renderThumbStyle is used to generate thumb and the onMousedown event is bound to track and Thumb.

clickThumbHandler(e) {
  this.startDrag(e);
  // Record this.y, this.y = the distance from the mouse-down point to the bottom of the thumb
  // Record this.x, this.x = the distance from the mouse-down point to the left of the thumb
  this[this.bar.axis] = (e.currentTarget[this.bar.offset] - (e[this.bar.client] - e.currentTarget.getBoundingClientRect()[this.bar.direction]));
},
 
// Start dragging function
startDrag(e) {
  e.stopImmediatePropagation();
  // Identifies the bit that indicates the current start of drag
  this.cursorDown = true;

  // Bind mousemove and Mouseup events
  on(document.'mousemove'.this.mouseMoveDocumentHandler);
  on(document.'mouseup'.this.mouseUpDocumentHandler);
  
  // Resolve the page content selection bug during dragging
  document.onselectstart = (a)= > false;
},
  
mouseMoveDocumentHandler(e) {
  // Check whether during the drag process,
  if (this.cursorDown === false) return;
  // The value of this.y(this.x) just recorded
  const prevPage = this[this.bar.axis];

  if(! prevPage)return;

  // The offset of mouse-down position in track, i.e. the distance from the mouse-down point to the top (left) of track
  const offset = ((this.$el.getBoundingClientRect()[this.bar.direction] - e[this.bar.client]) * - 1);
  // The distance from the mouse down point to the top of the thumb (left)
  const thumbClickPosition = (this.$refs.thumb[this.bar.offset] - prevPage);
  // The distance from the top of the current thumb (left) to the top of the track (left), i.e. the percentage of the height (width) of the track that the thumb is offset downward (right)
  const thumbPositionPercentage = ((offset - thumbClickPosition) * 100 / this.$el[this.bar.offset]);
	// wrap.scrollHeight / wrap.scrollLeft * thumbPositionPercentage得到wrap.scrollTop / wrap.scrollLeft
  // When wrap.scrollTop(wrap.scrollLeft) changes, the onScroll event bound to the parent component wrap will be triggered.
  // Recalculate moveX/moveY so that the thumb scroll position is re-rendered
  this.wrap[this.bar.scroll] = (thumbPositionPercentage * this.wrap[this.bar.scrollSize] / 100);
},

mouseUpDocumentHandler(e) {
  // When dragging ends, set the identifier bit to false
  this.cursorDown = false;
  // Clear the value of this.y(this.x) from the last drag record
  this[this.bar.axis] = 0;
  // Unbind the mousemove event from the page
  off(document.'mousemove'.this.mouseMoveDocumentHandler);
  // Empty the function bound to the onselectStart event
  document.onselectstart = null;
}
Copy the code

The whole idea is to dynamically calculate the percentage of the distance from the top of thumb (left) to the top of track (left) in the height (width) of track itself during the process of dragging thumb. This percentage is then used to dynamically change the value of wrap.scrollTop, which triggers the page scroll and the recalculation of the scroll bar position to achieve the scrolling effect.

The previous diagram is convenient for everyone to understand ( ̄▽ ̄)”

Track’s onMousedown and Trumb’s logic is similar, with two caveats:

  1. The onMousedown event callback of track does not bind the mousemove and Mouseup events to the page, because track is equivalent to the Click event
  2. In the onmousedown event of track, we calculate the top of the thumb to the top of the track by subtracting half the height of the thumb from the mouse click point to the top of the track. This is because after clicking the track, the midpoint of the thumb is exactly where the mouse click point was.

At this point, the whole scrollbar source code analysis is over, looking back, in fact, the implementation of scrollbar is not difficult, mainly to clarify a variety of scrolling relations, the length of thumb and how to determine the scrolling position through the wrap, view relations. This part may be a bit tricky, but if you don’t understand it, I suggest you study hand animation drawing. As long as you understand the rolling principle, it is very simple to implement.