As the most popular Vue component library, ElementUI attracts many developers with its rich and diverse components and high degree of configurability. Today we will dig into its source code to see how many of its components are implemented, so that we can write more general components in future development, and also deepen our understanding of the use of its components.

This article was first published in the public number [front-end one read], more exciting content please pay attention to the latest news of the public number.

As always, let’s look at the slogan on the official website:

Element, a Vue 2.0-based desktop component library for developers, designers, and product managers

As you can see, Element is used by most developers; Product managers can use it to reference logical interactions, designers can borrow ICONS and component designs, and developers can use it to lay out pages.

The project structure

Let’s clone the entire project from Github and take a look at the directory files:

Guide. | - lot # for contribution and issue, PR template | - build # deposit packaging tools | configuration file - # examples for example components | - packages # deposit component source code, We also analysis the main directory | - SRC # store entrance files as well as a variety of tools | - directives # roller optimization and avoid repeat click # international functions | | - locale -- mixins # # | - the transition with examples Effect of excessive | - utils # tools | - test # storage unit test file | - | deposit types # typescript declaration documents -- components. Json # complete component listCopy the code

So the Packages and SRC directories are two important directories that we need to focus on; Now that you have an overview of the directory structure, the next thing to look at is the package.json file, which contains project descriptions, project dependencies, script commands, etc. Sometimes when we can’t find the entry file for a project at first glance, we can find it here.

Element-uicommon.js “, which defines the entry file to the NPM package. We require(“element-ui”) in the project to refer to the element-uicommon.js file; Then let’s take a look at the script commands. Here are some important ones:

{
  "scripts": {
    "build:file": "node build/bin/iconInit.js & node build/bin/i18n.js"."build:theme": "gulp build --gulpfile packages/theme-chalk/gulpfile.js"."build:utils": "cross-env BABEL_ENV=utils babel src --out-dir lib --ignore src/index.js"."build:umd": "node build/bin/build-locale.js"."dist": "webpack --config build/webpack.conf.js && webpack --config build/webpack.common.js && webpack --config build/webpack.component.js"}}Copy the code

It can be seen that the previous several build commands are used to build some tools and styles, mainly dist command, which is packaged by webpack. There are also three packages. Let’s take a look at what files are packaged in these three packages respectively. Let’s take a look at the first two configuration files, webpack.conf.js and webpack.common.js.

//build/webpack.conf.js
module.exports = {
  mode: 'production'.entry: {
    app: ['./src/index.js']},output: {
    path: path.resolve(process.cwd(), './lib'),
    publicPath: '/dist/'.filename: 'index.js'.chunkFilename: '[id].js'.libraryTarget: 'umd'.libraryExport: 'default'.library: 'ELEMENT'.umdNamedDefine: true.globalObject: 'typeof self ! == \'undefined\' ? self : this'
  },
  optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          output: {
            comments: false}}})]},}//build/webpack.common.js
module.exports = {
  mode: 'production'.entry: {
    app: ['./src/index.js']},output: {
    path: path.resolve(process.cwd(), './lib'),
    publicPath: '/dist/'.filename: 'element-ui.common.js'.chunkFilename: '[id].js'.libraryExport: 'default'.library: 'ELEMENT'.libraryTarget: 'commonjs2'
  },
  optimization: {
    minimize: false}},Copy the code

Webpack.conf. js is packaged with the UMD specification and is compressed by minimizer. Webpack.common.js is packaged with the CommonJS specification and is not compressed; The main reason for packaging between the two specifications is also the difference in the way Element is installed. The UMD specification mainly aims at the way CDN is introduced, introducing JS and CSS on the page:

<! -- Introducing styles -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<! -- Introducing a component library -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
Copy the code

Element-oriented element-oriented (element-oriented, element-oriented, element-oriented, element-oriented, element-oriented, element-oriented)

import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Copy the code

So last build/webpack.com ponent. Js also is not hard to guess, is to once introduced, only introduced need some components, not the whole package:

const Components = require('.. /components.json');
module.exports = {
  mode: 'production'.entry: Components,
    output: {
    path: path.resolve(process.cwd(), './lib'),
    publicPath: '/dist/'.filename: '[name].js'.chunkFilename: '[id].js'.libraryTarget: 'commonjs2'
  },
  optimization: {
    minimize: false}},Copy the code

Here components. Json is the entry file for each component.

Entrance to the file

We found the entry file/SRC /index.js by looking at the webPack configuration file in the previous section, so let’s look at how Element registers so many components in the entry.

// Some components are captured
import Button from '.. /packages/button/index.js';
import Input from '.. /packages/input/index.js';
import MessageBox from '.. /packages/message-box/index.js';
import Loading from '.. /packages/loading/index.js';
import InfiniteScroll from '.. /packages/infinite-scroll/index.js';
import Notification from '.. /packages/notification/index.js';
import Message from '.. /packages/message/index.js';

const components = [
  Button,
  Input
]
const install = function(Vue, opts = {}) {
  components.forEach(component= > {
    Vue.component(component.name, component);
  });
  Vue.use(InfiniteScroll);
  Vue.use(Loading.directive);
  Vue.prototype.$ELEMENT = {
    size: opts.size || ' '.zIndex: opts.zIndex || 2000
  };
  Vue.prototype.$loading = Loading.service;
  Vue.prototype.$msgbox = MessageBox;
  Vue.prototype.$alert = MessageBox.alert;
  Vue.prototype.$confirm = MessageBox.confirm;
  Vue.prototype.$prompt = MessageBox.prompt;
  Vue.prototype.$notify = Notification;
  Vue.prototype.$message = Message;
};
// The browser environment automatically invokes the registration component
if (typeof window! = ='undefined' && window.Vue) {
  install(window.Vue);
}
export default {
  install,
  Button,
  Input,
  MessageBox,
  Notification
}
Copy the code

Here Element exposes an install function because Element itself is a plug-in, which is essentially what we call install when we call vue.use (ElementUI) registration; So how does vue.use register a plug-in?

  1. Vue.use receives an object that must have the install method. The install method of the parameter is called inside the vue. use function.
  2. If the plug-in has not been registered, an installed attribute is added to the plug-in with a value of true. The vue. use method internally detects the installed property of the plug-in to avoid duplicate plug-in registrations.
  3. The plug-in’s Install method will receive two arguments, the first being Vue and the second being the configuration item options.
  4. Inside the Install method you can add global methods or properties, global directives, mixins, add instance methods, register components with Vue.component(), and more.

In Element’s install function, we found that size and zIndex were extracted from the options parameter and stored in vue.prototype. $Element global configuration so that we could get size and zIndex from the component. Display different component sizes according to size.

In the Element documentation global configuration, it is also indicated that the global configuration can be overridden when Element is introduced:

import Element from 'element-ui';
Vue.use(Element, { size: 'small'.zIndex: 3000 });
Copy the code

component

There are three ways to register Element as an Element plugin. The first way is to loop through an array like Button and Input, register it as a global component in Vue.com Ponent, and reference it directly on the page. InfiniteScroll and Loading are registered globally and invoked by v-infinite-scroll and v-loading instructions. The third, MessageBox, Notification, and Message, adds methods in global vue.prototype that can be called by functions.

Global components

The Container and the Header

Let’s start with a few simple layout container components. Let’s take a quick look at the demo to review how these components work:

<el-container>
  <el-aside width="200px">Aside</el-aside>
  <el-container>
    <el-header>Header</el-header>
    <el-main>Main</el-main>
    <el-footer>Footer</el-footer>
  </el-container>
</el-container>
Copy the code

Let’s take a look at the source code for the el-header. The implementation logic is very simple. (The same goes for el-footer and el-aside, not shown here) :

//packages/header/src/main.vue
<template>
  <header class="el-header" :style="{ height }">
    <slot></slot>
  </header>
</template>
<script>
  export default {
    name: 'ElHeader',
    componentName: 'ElHeader',
    props: {
      height: {
        type: String,
        default: '60px'
      }
    }
  };
</script>
Copy the code

El-header, el-footer, and el-aside are all similar. They all pass string values of width or height.

The value of the el-container parameter is horizontal/vertical, but its default value is very special.

Vertical if the child element has el-header or el-footer, horizontal otherwise

Knowing its pass-through logic, let’s look at how the source code is implemented:

//packages/container/src/main.vue <template> <section class="el-container" :class="{ 'is-vertical': isVertical }"> <slot></slot> </section> </template> <script> export default { name: "ElContainer", componentName: "ElContainer", props: { direction: String, }, computed: { isVertical() { if (this.direction === "vertical") { return true; } else if (this.direction === "horizontal") { return false; } return this.$slots && this.$slots.default ? this.$slots.default.some((vnode) => { const tag = vnode.componentOptions && vnode.componentOptions.tag; return tag === "el-header" || tag === "el-footer"; }) : false; ,}}}; </script>Copy the code

What’s hard to understand in the code is the logical judgment in isVertical, so let’s look at it paragraph by paragraph; This.$slots is used to get all the slots in the component. This.$refs is like this. This.$slots.default is the slot that gets the default value, which is an array of nodes in the slot. Some is used to determine whether the vnode in the array has an el-header or an el-footer tag. If so, return true and render the is-vertical class name.

The Row and Col

Let’s look at two layout components Row and Col, which are used to create raster layouts. Let’s briefly look at the use of these two components:

<el-row :gutter="20">
  <el-col :span="6">
    <div class="grid-content"></div>
  </el-col>
  <el-col :span="6">
    <div class="grid-content"></div>
  </el-col>
  <el-col :span="6">
    <div class="grid-conten"></div>
  </el-col>
  <el-col :span="6">
    <div class="grid-content"></div>
  </el-col>
</el-row>
Copy the code

These two components also render the page in the way of a slot, but when we look at the source code, we will find that its slot usage is not the same as the above component:

//packages/row/src/row.js
export default {
  name: 'ElRow'.props: {
    tag: {
      type: String.default: 'div'
    },
    gutter: Number.type: String.justify: {
      type: String.default: 'start'
    },
    align: {
      type: String.default: 'top'}},computed: {
    style() {
      const ret = {};
      if (this.gutter) {
        ret.marginLeft = ` -The ${this.gutter / 2}px`;
        ret.marginRight = ret.marginLeft;
      }
      returnret; }},render(h) {
    return h(this.tag, {
      class: [
        'el-row'.this.justify ! = ='start' ? `is-justify-The ${this.justify}` : ' '.this.align ! = ='top' ? `is-align-The ${this.align}` : ' ',
        { 'el-row--flex': this.type === 'flex'}].style: this.style
    }, this.$slots.default); }};Copy the code

We found that el-Row does not have a template rendering, but instead uses the render function to render the page. But why do we need a render function here? In the parameter list, we can see that the parameter has a tag. The tag defines the outermost tag type. Rendering from a template requires multiple if judgments, which can be tedious, but with a rendering function, you can render the tag directly; For details on how to use rendering functions, see the documentation on the official website.

The Col component uses the render function to render a Row, but the Col component gets the value of the parent component’s Row argument.

//packages/col/src/col.js
export default {
  name: "ElCol".computed: {
    gutter() {
      let parent = this.$parent;
      while(parent && parent.$options.componentName ! = ="ElRow") {
        parent = parent.$parent;
      }
      return parent ? parent.gutter : 0; }},render(h) {
    let classList = [];
    let style = {};

    if (this.gutter) {
      style.paddingLeft = this.gutter / 2 + "px";
      style.paddingRight = style.paddingLeft;
    }
    return h(this.tag, {
        class: ["el-col", classList],
        style,
      },
      this.$slots.default ); }};Copy the code

Since the Row component passes gutter to the grid interval, the Rol component also needs a bit of padding, but how do you get parameters from the parent component? Col uses a while loop to continuously fetch the parent component and determine the component name.

The Form and Form – the Item

The top layer of the Form component is the Form and form-item component. We can check the Input field or other component values by passing rules to the Form. First, we will look at the source code of the Form.

//packages/form/src/form.vue <template> <form class="el-form" :class="[ labelPosition ? 'el-form--label-' + labelPosition : '', { 'el-form--inline': inline } ]"> <slot></slot> </form> </template> <script> export default { name: 'ElForm', provide() { return { elForm: this }; }, watch: { rules() { // remove then add event listeners on form-item after form rules change this.fields.forEach(field => { field.removeValidateEvents(); field.addValidateEvents(); }); if (this.validateOnRuleChange) { this.validate(() => {}); } } }, data() { return { fields: [], potentialLabelWidthArr: [] // use this array to calculate auto width }; }, created() { this.$on('el.form.addField', (field) => { if (field) { this.fields.push(field); }}); /* istanbul ignore next */ this.$on('el.form.removeField', (field) => { if (field.prop) { this.fields.splice(this.fields.indexOf(field), 1); }}); }, methods: { validate(callback) { let promise; if (typeof callback ! == 'function' && window.Promise) { promise = new window.Promise((resolve, reject) => { callback = function(valid) { valid ? resolve(valid) : reject(valid); }; }); } let valid = true; let count = 0; let invalidFields = {}; this.fields.forEach(field => { field.validate('', (message, field) => { if (message) { valid = false; } invalidFields = objectAssign({}, invalidFields, field); if (typeof callback === 'function' && ++count === this.fields.length) { callback(valid, invalidFields); }}); }); if (promise) { return promise; }}}}; </script>Copy the code

We can see that the Form page structure is very simple. There is only one Form tag, and props only uses labelPosition and inline. Other attributes are used in the form-item. The provide function is also used in the Form. In Vue component communication, we introduced provide/ Inject, which is mainly used to communicate across multiple layers of components. We will use it later in the component introduction.

Let’s look at how the common form validation function validate is implemented. In Created, we see that two events are registered: addField and removeField, which are called when all the child form-items are initialized, to create a collection store into the Fields array. Why not use $children? Because the page structure is uncertain, the sub-component of the Form may not be the form-item, if the loop is time-consuming and laborious, and the sub-component management operation is more frequent, so through the event; After collecting all the form-Items, we can iterate and validate each Form element.

Form-item = form-item = form-item = form-item

//packages/form/src/form-item.vue <template> <div class="el-form-item" :class="[{ 'el-form-item--feedback': elForm && elForm.statusIcon, 'is-error': validateState === 'error', 'is-validating': validateState === 'validating', 'is-success': validateState === 'success', 'is-required': isRequired || required, 'is-no-asterisk': elForm && elForm.hideRequiredAsterisk }, sizeClass ? 'el-form-item--' + sizeClass : '' ]"> <label-wrap :is-auto-width="labelStyle && labelStyle.width === 'auto'" :update-all="form.labelWidth === 'auto'"> <label :for="labelFor" class="el-form-item__label" :style="labelStyle" v-if="label || $slots.label"> <slot name="label">{{label + form.labelSuffix}}</slot> </label> </label-wrap> <div class="el-form-item__content" :style="contentStyle"> <slot></slot> <transition name="el-zoom-in-top"> <slot v-if="validateState === 'error' && showMessage && form.showMessage" name="error" :error="validateMessage"> <div class="el-form-item__error" :class="{ 'el-form-item__error--inline': typeof inlineMessage === 'boolean' ? inlineMessage : (elForm && elForm.inlineMessage || false) }" > {{validateMessage}} </div> </slot> </transition> </div> </div> </template> <script> import AsyncValidator from 'async-validator'; import emitter from 'element-ui/src/mixins/emitter'; import objectAssign from 'element-ui/src/utils/merge'; import { noop, getPropByPath } from 'element-ui/src/utils/util'; export default { name: 'ElFormItem', componentName: 'ElFormItem', mixins: [emitter], provide() { return { elFormItem: this }; }, inject: ['elForm'], computed: { fieldValue() { const model = this.form.model; if (! model || ! this.prop) { return; } let path = this.prop; if (path.indexOf(':') ! == -1) { path = path.replace(/:/, '.'); } return getPropByPath(model, path, true).v; }, _formSize() { return this.elForm.size; }, elFormItemSize() { return this.size || this._formSize; }, sizeClass() { return this.elFormItemSize || (this.$ELEMENT || {}).size; } }, data() { return { validateState: '', validateMessage: '', validateDisabled: false, validator: {}, isNested: false, computedLabelWidth: '' }; }, methods: { validate(trigger, callback = noop) { this.validateDisabled = false; const rules = this.getFilteredRule(trigger); if ((! rules || rules.length === 0) && this.required === undefined) { callback(); return true; } this.validateState = 'validating'; const descriptor = {}; if (rules && rules.length > 0) { rules.forEach(rule => { delete rule.trigger; }); } descriptor[this.prop] = rules; const validator = new AsyncValidator(descriptor); const model = {}; model[this.prop] = this.fieldValue; validator.validate(model, { firstFields: true }, (errors, invalidFields) => { this.validateState = ! errors ? 'success' : 'error'; this.validateMessage = errors ? errors[0].message : ''; callback(this.validateMessage, invalidFields); this.elForm && this.elForm.$emit('validate', this.prop, ! errors, this.validateMessage || null); }); }, getRules() { let formRules = this.form.rules; const selfRules = this.rules; const requiredRule = this.required ! == undefined ? { required: !! this.required } : []; const prop = getPropByPath(formRules, this.prop || ''); formRules = formRules ? (prop.o[this.prop || ''] || prop.v) : []; return [].concat(selfRules || formRules || []).concat(requiredRule); }, onFieldBlur() { this.validate('blur'); }, onFieldChange() { if (this.validateDisabled) { this.validateDisabled = false; return; } this.validate('change'); }, addValidateEvents() { const rules = this.getRules(); if (rules.length || this.required ! == undefined) { this.$on('el.form.blur', this.onFieldBlur); this.$on('el.form.change', this.onFieldChange); } }, }, mounted() { if (this.prop) { this.dispatch('ElForm', 'el.form.addField', [this]); this.addValidateEvents(); } }, beforeDestroy() { this.dispatch('ElForm', 'el.form.removeField', [this]); }}; </script>Copy the code

We see here that provide/inject is used to handle cross-component data communication, the Form is introduced, the class name is rendered using the props of the Form, and the inject itself is passed down. SizeClass: The size of the form-item itself, then the size of the Form, and finally the size of the $ELEMENT we mount globally. We look at other components such as Input, Button, and Radio that render size in this way.

We can also see in the form-item lifecycle functions that Form collection is done by triggering the addField and removeField of the Form, but via a dispatch function that is neither in vue’s official website nor defined in methods. So how does it get triggered? When we look closely at the code, we can see an array of mixins:[Emitter]. Form-items are used to extract common functions from mixins.

function broadcast(componentName, eventName, params) {
  this.$children.forEach(child= > {
    var name = child.$options.componentName;
    if (name === componentName) {
      child.$emit.apply(child, [eventName].concat(params));
    } else{ broadcast.apply(child, [componentName, eventName].concat([params])); }}); }export default {
  methods: {
    dispatch(componentName, eventName, params) {
      var parent = this.$parent || this.$root;
      var name = parent.$options.componentName;
      while(parent && (! name || name ! == componentName)) { parent = parent.$parent;if(parent) { name = parent.$options.componentName; }}if(parent) { parent.$emit.apply(parent, [eventName].concat(params)); }},broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params); }}};Copy the code

We see that Dispatch is used to send events to the parent component, also iterating through the loop through while, while broadcast is used to broadcast events to the child component.

The Button and the Button – Group

Button is a commonly used component, let’s look at its source:

//packages/button/src/button.vue <template> <button class="el-button" @click="handleClick" :disabled="buttonDisabled || loading" :autofocus="autofocus" :type="nativeType" :class="[ type ? 'el-button--' + type : '', buttonSize ? 'el-button--' + buttonSize : '', { 'is-disabled': buttonDisabled, 'is-loading': loading, 'is-plain': plain, 'is-round': round, 'is-circle': circle } ]" > <i class="el-icon-loading" v-if="loading"></i> <i :class="icon" v-if="icon && ! loading"></i> <span v-if="$slots.default"><slot></slot></span> </button> </template> <script> export default { name: 'ElButton', inject: { elForm: { default: '' }, elFormItem: { default: '' } }, props: { type: { type: String, default: 'default' }, size: String, icon: { type: String, default: '' }, nativeType: { type: String, default: 'button' }, loading: Boolean, disabled: Boolean, plain: Boolean, autofocus: Boolean, round: Boolean, circle: Boolean }, computed: { _elFormItemSize() { return (this.elFormItem || {}).elFormItemSize; }, buttonSize() { return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size; }, buttonDisabled() { return this.disabled || (this.elForm || {}).disabled; } }, methods: { handleClick(evt) { this.$emit('click', evt); }}}; </script>Copy the code

We see that the Button logic is simple compared to the above component, rendering the class name using buttonSize and buttonDisabled computed, and relying on the injected elForm and elFormItem data. Trigger the click event when clicked; We can also use button-group to nest multiple buttons.

//packages/button/src/button-group.vue
<template>
  <div class="el-button-group">
    <slot></slot>
  </div>
</template>
<script>
  export default {
    name: 'ElButtonGroup'
  };
</script>
Copy the code

The code is simpler, using only one slot to nest all the buttons.

Instruction module

Directive components are registered with vue.directive (name, opt). Name is the name of the directive we want to register, and opt is an object containing five hook functions. We can write only a few of them as needed:

Vue.directive("demo", {
  // Only called once, when the directive is first bound to the element
  bind:function(el,binding,vnode){},// called when the bound element is inserted into the parent node
  inserted:function(el,binding,vnode){},// Called when the VNode of the component is updated
  update:function(el,binding,vnode){},// call after the component's VNode and its child VNodes are all updated
  componentUpdated:function(el,binding,vnode){},// Only called once, when the instruction is unbound from the element.
  unbind:function(el,binding,vnode){},})Copy the code

Each hook function takes three callback arguments. El represents the element bound by the instruction and can be used for direct DOM manipulation. Binding is our binding information, which is an object containing the following properties:

  • Name: indicates the command namev-Prefix.
  • Value: The binding value of the instruction, such as V-demo =”num”, where the binding value is num value, 2.
  • Expression: Indicates the expression of an instruction in the form of a string, for example, V-demo =”num”. The expression is num.
  • Modifiers: an object that contains modifiers, such as v-demo.foo.bar which are {foo: true, bar: true}
  • RawName: Complete instruction modification

InfiniteScroll

The InfiniteScroll infinite scroll component is also very simple to use, we want to scroll on the list to load v-infinitescroll, assign a custom load function, when the list to the bottom will automatically trigger the function.

<template>
  <ul class="infinite-list" v-infinite-scroll="load" style="overflow:auto">
    <li v-for="i in count" :key="i" class="infinite-list-item">{{ i }}</li>
  </ul>
</template>
<script>
export default {
  data() {
    return {
      count: 0,
    }
  },
  methods: {
    load() {
      this.count += 2;
    },
  }
}
</script>
Copy the code

When ul scrolls to the bottom, the load function is triggered to load more data; The function is called when the container is scrolled to the bottom. Let’s see how it works:

import throttle from 'throttle-debounce/debounce';
import {
  getScrollContainer
} from 'element-ui/src/utils/dom';

const attributes = {
  delay: {
    type: Number.default: 200
  },
  distance: {
    type: Number.default: 0
  },
  disabled: {
    type: Boolean.default: false
  },
  immediate: {
    type: Boolean.default: true}};const getScrollOptions = (el, vm) = > {
  if(! isHtmlElement(el))return {};
  return entries(attributes).reduce((map, [key, option]) = > {
    const { type, default: defaultValue } = option;
    let value = el.getAttribute(`infinite-scroll-${key}`);
    value = isUndefined(vm[value]) ? value : vm[value];
    switch (type) {
      case Number:
        value = Number(value);
        value = Number.isNaN(value) ? defaultValue : value;
        break;
      case Boolean:
        value = isDefined(value) ? value === 'false' ? false : Boolean(value) : defaultValue;
        break;
      default:
        value = type(value);
    }
    map[key] = value;
    return map;
  }, {});
};

const handleScroll = function(cb) {
  const { el, vm, container, observer } = this[scope];
  const { distance, disabled } = getScrollOptions(el, vm);
  if (disabled) return;
  const containerInfo = container.getBoundingClientRect();
  if(! containerInfo.width && ! containerInfo.height)return;
  let shouldTrigger = false;
  if (container === el) {
    const scrollBottom = container.scrollTop + getClientHeight(container);
    shouldTrigger = container.scrollHeight - scrollBottom <= distance;
  } else {
    const heightBelowTop = getOffsetHeight(el) + getElementTop(el) - getElementTop(container);
    const offsetHeight = getOffsetHeight(container);
    const borderBottom = Number.parseFloat(getStyleComputedProperty(container, 'borderBottomWidth'));
    shouldTrigger = heightBelowTop - offsetHeight + borderBottom <= distance;
  }
  if (shouldTrigger && isFunction(cb)) {
    cb.call(vm);
  } else if (observer) {
    observer.disconnect();
    this[scope].observer = null; }};export default {
  name: 'InfiniteScroll'.inserted(el, binding, vnode) {
    const cb = binding.value;

    const container = getScrollContainer(el, true);
    const { delay, immediate } = getScrollOptions(el, vm);
    const onScroll = throttle(delay, handleScroll.bind(el, cb));

    if (container) {
      container.addEventListener('scroll', onScroll);

      if (immediate) {
        const observer = el[scope].observer = new MutationObserver(onScroll);
        observer.observe(container, { childList: true.subtree: true}); onScroll(); }}},unbind(el) {
    const { container, onScroll } = el[scope];
    if (container) {
      container.removeEventListener('scroll', onScroll); }}};Copy the code

The logic in the INSERTED function is also simple: first cb is our custom callback to fire; Check whether our EL is a scroll container by using getScrollContainer, and then encapsulate the handleScroll function handling the scroll logic into onScroll with throttle function, and bind it to the scroll event of the container.

We also see that there are four parameters, all starting with infinite-scroll-, to control the time when the load function is triggered; As seen in the source code, getScrollOptions is used to obtain the parameters, and defines an Object to store the names, types, and default values of the four parameters. Keys are used as an array, and reduce is used as a Map Object.

Function calling component

Vue.com Ponent will not be able to register global components through Vue.com Ponent. Instead, we create a subclass constructor with vue.extend that takes an object containing component options, instantiates it and mounts it to the page element via $mount.

var MyMessage = Vue.extend({
  template: '<div>number:{{number}}</div>'.data() {
    return {
      number: 1}}})let instance = new MyMessage()
instance.$mount('#components')
Copy the code

Or get the DOM structure from $mount and mount it to the body:

let instance = new MyMessage()
instance.$mount()
document.body.appendChild(instance.$el)
Copy the code

Message

The Message component is mounted to the global variable Message in the entry file, and is called via ‘ ‘this.message, then’ ‘this.message, then’ ‘this.message(), This.$message.error() can also be called by this.$message.error(). Let’s take a look at the Message source (part) :

//packages/message/src/main.js
import Main from './main.vue';
import { isVNode } from 'element-ui/src/utils/vdom';
let MessageConstructor = Vue.extend(Main);

let instance;
let instances = [];
let seed = 1;

const Message = function(options) {
  options = options || {};
  if (typeof options === 'string') {
    options = {
      message: options
    };
  }
  let id = 'message_' + seed++;
  instance = new MessageConstructor({
    data: options
  });
  instance.id = id;
  if (isVNode(instance.message)) {
    instance.$slots.default = [instance.message];
    instance.message = null;
  }
  instance.$mount();
  document.body.appendChild(instance.$el);
  let verticalOffset = options.offset || 20;
  instances.forEach(item= > {
    verticalOffset += item.$el.offsetHeight + 16;
  });
  instance.verticalOffset = verticalOffset;
  instance.visible = true;
  instances.push(instance);
  return instance;
};

['success'.'warning'.'info'.'error'].forEach(type= > {
  Message[type] = options= > {
    if (typeof options === 'string') {
      options = {
        message: options
      };
    }
    options.type = type;
    return Message(options);
  };
});
export default Message;
Copy the code

In the constructor of Message, we can pass in either a string or an object. Therefore, we can unify the string options into an object. We then instantiate an instance with the vue. extend constructor, pass all parameters to the instance for rendering, and insert instance.$el into the page for rendering. In order to manage all instances, such as rendering the height of the last instance and closing the instance object according to the number of all instances, an array instances is maintained for management, and each instance is given an autoincrement ID to facilitate searching.

Vue, extend, main. Vue

//packages/message/src/main.vue <template> <transition name="el-message-fade" @after-leave="handleAfterLeave"> <div :class="[ 'el-message', type && !iconClass ? `el-message--${ type }` : '', center ? 'is-center' : '', showClose ? 'is-closable' : "', customClass ]" :style="positionStyle" v-show="visible" @mouseenter="clearTimer" @mouseleave="startTimer" role="alert"> <i :class="iconClass" v-if="iconClass"></i> <i :class="typeClass" v-else></i> <slot> <p v-if="! dangerouslyUseHTMLString" class="el-message__content">{{ message }}</p> <p v-else v-html="message" class="el-message__content"></p> </slot> <i v-if="showClose" class="el-message__closeBtn el-icon-close" @click="close"></i> </div> </transition> </template> <script type="text/babel"> export default { data() { return { visible: false, message: '', duration: 3000, type: 'info', iconClass: '', customClass: '', onClose: null, showClose: false, closed: false, verticalOffset: 20, timer: null, dangerouslyUseHTMLString: false, center: false }; }, methods: { handleAfterLeave() { this.$destroy(true); this.$el.parentNode.removeChild(this.$el); ,}}}; </script>Copy the code

We can see that the data parameter is the same as the parameter given in the document. In the above constructor, options is passed in to override the data parameter. This closes the component by listening for closed to be true, and then assigning visible to false; So why don’t we just assign a visible value here? My guess is to trigger the callback onClose while closing; Parentnode. removeChild is also called after the component animation to remove the component from the body,

conclusion

We start from webpack configuration file, find the entry file for component registration configuration and export, the numerous components are classified into three categories; Due to the limited space of the article, here only shows the source of each component in part of the component, like many commonly used components, such as Input, Radio and Checkbox and many other components are similar, we can continue to learn the source code; While looking at the source code, it is suggested that you can view the parameters and parameter descriptions in the official documents, so that you can better understand the logic of the source code and not be confused.

For more front-end information, please pay attention to the public number [front-end reading].

If you think it’s good, check out my Nuggets page. Please visit Xie xiaofei’s blog for more articles