Bootstrap

Element-ui源码分析

分析Element-ui封装思想

在平时写业务或者是写玩具的时候为了方便,我们会使用各种各样的组件库。虽然说基本需求看文档就可以了,但是文档中提供的方法和业务需求相比肯定是有一定差距的,这时候就需要自己封装组件了;并且,在写了一些代码后感觉,其实在不同的项目中写过功能差不多相同的代码,那为什么不封装一下方便以后、或者是其他人使用呢?写这篇博客的时候参考了b站up主樱满空视频

文章内容会不断的更新,每一节内容分为

  • props属性分析
  • 样式分析
  • 重新封装

目录结构分析

假设现在你已经在项目中安装了element-ui,此时打开node_modules目录往下翻,可以看到一个名为element-ui的文件夹。
在这里插入图片描述

  • lib文件夹存放element-ui打包后的文件,也就是项目实际依赖了的文件
  • packages文件夹存放组件相关的源代码,也是之后源码分析的主要目标。
  • src文件夹存放了如指令、混入、工具方法等源代码
  • types文件夹存放了ts的类型声明文件,方便引入 typescript 写的项目中,需要在 package.json 中指定 typing 字段的值为 声明的入口文件才能生效。

入口文件

在分析packegs文件夹中的各个组件源码之前,我们先看看src中的入口文件index.js。

// 部分引入
import Pagination from '../packages/pagination/index.js';
import Dialog from '../packages/dialog/index.js';
import Autocomplete from '../packages/autocomplete/index.js';
import Dropdown from '../packages/dropdown/index.js';
import DropdownMenu from '../packages/dropdown-menu/index.js';

// 将引入的文件名放在一个数组中
const components = [
  Pagination,
  Dialog,
  Autocomplete,
  Dropdown, 
  DropdownMenu
]

// Element暴露出去一个install函数,Element本身就是一个插件
const install = function(Vue, opts = {}) {
  locale.use(opts.locale);
  locale.i18n(opts.i18n);
  // 通过对组件使用forEach方法,将所有的组件进行注册
  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;
};

我们在调用Vue.use(ElementUI)注册时,本质上就是调用这个install函数。由于Vue.use接收一个对象,这个对象必须具有install方法,Vue.use函数内部会调用参数的install方法。如果插件没有被注册过,那么注册成功之后会给插件添加一个installed的属性值为true。Vue.use方法内部会检测插件的installed属性,从而避免重复注册插件。

插件的install方法将接收两个参数,第一个是参数是Vue,第二个参数是配置项options(就是这里的opts)对象。

Vue.prototype.$ELEMENT这一句来看,传入的参数可以有sizezIndex属性,size 用于改变组件的默认尺寸,zIndex 设置弹框的初始 z-index(默认值:2000)。我们可以手动向options中传入size和zInde,保存到Vue.prototype.$ELEMENT全局配置中,这样在组件中我们就可以根据size和zIndex进行不同组件尺寸的展示。

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

在入口文件中我们可以通过forEach循环遍历进行大部分组件的注册,小部分如InfiniteScrollLoading在全局注册指令,通过v-infinite-scrollv-loading等指令式来调用;也有如msgboxalert等在全局Vue.prototype添加方法,可以通过函数进行调用。

Layout布局

先看看基础布局里面提供的代码,对于Layout布局的部分,我们需要使用到el-row和el-col的嵌套子组件。

<el-row>
  <el-col :span="24"><div class="grid-content bg-purple-dark"></div></el-col>
</el-row>
<el-row>
  <el-col :span="12"><div class="grid-content bg-purple"></div></el-col>
  <el-col :span="12"><div class="grid-content bg-purple-light"></div></el-col>
</el-row>

el-row

  • 打开路径node_modules -> element-ui -> package -> el-row -> src -> row.js查看组件的逻辑和页面部分

  • 打开路径node_modules -> element-ui -> packages -> theme-chalk -> src -> row.scss查看el-row的样式部分

  • 查阅官方文档查看el-row有那些属性
    在这里插入图片描述
    和传统的vue文件中template模板不同的是,el-row组件是以渲染函数的方式编写的

export default {
  // 组件名
  name: 'ElRow',
  // 这个选项并非Vue官方提供的API,而是Element团队自定义的属性
  // 在查阅 Vue2 官方文档的时候可以看到,vm.$options的api可以用于当前Vue实例的初始化选项
  // 所有我们写的Vue选项都会放到Vue实例属性$options中
  // 比如之后可以通过this.$options.componentName获取到这里的属性值
  componentName: 'ElRow',
  props: {
    tag: {
      type: String,
      default: 'div'
    },
    gutter: Number,
    type: String,
    justify: {
      type: String,
      default: 'start'
    },
    align: String
  },

  computed: {
    style() {
      const ret = {};
      if (this.gutter) {
        ret.marginLeft = `-${this.gutter / 2}px`;
        ret.marginRight = ret.marginLeft;
      }
      return ret;
    }
  },

  render(h) {
    return h(this.tag, {
      class: [
        'el-row',
        this.justify !== 'start' ? `is-justify-${this.justify}` : '',
        this.align ? `is-align-${this.align}` : '',
        { 'el-row--flex': this.type === 'flex' }
      ],
      style: this.style
    }, this.$slots.default);
  }
};

props属性分析

  • tag:用来自定义元素标签,默认是div。我们可以看到tag属性用在了render函数中,render函数的参数h就是createElement函数的别名,也就是说默认情况下每个渲染出来的el-row是一个div。

  • type、justify、align:这三个属性都与flex布局相关,type属性可选flex布局,后面两个属性用于垂直水平的布局,在render函数中查看class,可以通过垂直水平的属性判断元素所在的位置。顺便介绍下render函数的最后一个参数,this.$slots.default用的是default插槽的内容,也就是在el-row标签中写的内容。

  • gutter:列间距。这个属性用在了computed中计算style,在我们手动传入了gutter的情况下,会给el-row左右两侧各添加一个gutter值除以2的负外边距。这么做是因为 el-col 的左右两侧都会添加一个gutter除以2的内边距。如果不追加这个负外边距的话会导致行的左右两侧也有间距,导致el-col无法和外层元素边缘对齐。

    至于为什么需要这样做,我们可以看看这个例子

<div class="box">
    <div class="son"></div>
    test
    <div class="son"></div>
</div>
<style>
    body{
            background-color: coral;
    }
    .box {
        width: 100%;
        height: 300px;
        background-color: aquamarine;
    }
    .son {
        height: 100px;
        background-color: black;
        
    }
</style>

在这里插入图片描述
现在为son设置一个margin-top:100px看看,可以明显的看到,父元素的box元素也被强制向下移动了100px
在这里插入图片描述
现在为父元素设置margin-top: -100px,整体元素就成功上移了。所以说在el-row中添加负外边距是为了保证子元素设置外边距时,不会影响整体行位置上的改变。
在这里插入图片描述

样式分析

打开row.scss文件

@import "common/var";
@import "mixins/mixins";
@import "mixins/utils";

@include b(row) {
  position: relative;
  box-sizing: border-box;
  @include utils-clearfix;

  @include m(flex) {
    display: flex;
    &:before,
    &:after {
      display: none;
    }

    @include when(justify-center) {
      justify-content: center;
    }
    @include when(justify-end) {
      justify-content: flex-end;
    }
    @include when(justify-space-between) {
      justify-content: space-between;
    }
    @include when(justify-space-around) {
      justify-content: space-around;
    }

    @include when(align-top) {
      align-items: flex-start;
    }

    @include when(align-middle) {
      align-items: center;
    }
    @include when(align-bottom) {
      align-items: flex-end;
    }
  }
}

在第四行中我们可以看到使用了@include指令,这个指令是搭配mixin使用的,现在它混入了一个名为b的mixin,并且传递了一个参数值为row,接下来通过@import "mixins/mixins"点进去查看,这个名为b的mixin做了什么工作。

/* BEM
 -------------------------- */
@mixin b($block) {
  $B: $namespace+'-'+$block !global;

  .#{$B} {
    @content;
  }
}

在这个混入中首先定义了一个$B的变量,值是namespace + ‘-’ + 传入的变量。这个namespace在config.scss中有定义

$namespace: 'el';
$element-separator: '__';
$modifier-separator: '--';
$state-prefix: 'is-';

对于这些定义需要了解一下采用BEM的Class命名风格

  • 组件名放在最前面,比如el-row,el-col,el-input
  • 如果说是组件的子元素样式,会用组件名加上两个下划线再加上元素名,比如el-input__inner
  • 修饰符接在最后,用两个中横线与前面的隔开,比如el-button–primary
  • 表示状态的前缀 is-,比如表示禁用状态的样式is-disabled

现在我们回到b的mixin中,#{}是使用变量定义的意思,这里用$B的值定义了一个class,其中使用了@content来将我们使用混入时写在大括号中间的内容放到这个class中。

@include b(row) {
  position: relative;
  box-sizing: border-box;
  ...
}

最终渲染到页面上名为el-row的class中。

在这里插入图片描述
接着row.scss往下看,又include了一个混入 @include utils-clearfix,首部utils-名称表示我们需要到utils.scss中来看。

@mixin utils-clearfix {
  $selector: &;

  @at-root {
    #{$selector}::before,
    #{$selector}::after {
      display: table;
      content: "";
    }
    #{$selector}::after {
      clear: both
    }
  }
}

从display:tabel和clear:both可以明显的看出来该方法用于清除浮动。

回到row.scss,发现存在一个名为m的混入@include m(flex) ,通过mixin.scss中我们可以看到,这个是生成修饰符class用的混入,使用@each遍历我们传入的修饰符,生成class名,然后拼接每个修饰符class,最后把给混入传递的内容放到这些class中。

@mixin m($modifier) {
  $selector: &;
  $currentSelector: "";
  @each $unit in $modifier {
    $currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","};
  }

  @at-root {
    #{$currentSelector} {
      @content;
    }
  }
}

这里的@at-root是让后面的样式跳出目前层级到顶层,由于我们之前在el-row的class下调用m混入,所以默认会把这些样式添加父类选择器el-row。使用@at-root后,这些修饰符选择器就可以和el-row class平级了。
在这里插入图片描述
最后还剩下一堆when的混入,其实就是用来生成is-开头表示状态用的class

@mixin when($state) {
  @at-root {
    &.#{$state-prefix + $state} {
      @content;
    }
  }
}

在这里插入图片描述

el-col

el-col和el-row一样,都是通过渲染函数编写的。
在这里插入图片描述

props属性分析

  • span表示列宽,默认情况下一列的宽度为24占一整行

  • offset、pull、push表示栅格在一行中的便宜位置

  • xs、sm、md、lg、xl用于表示响应式布局,他们可以接收Number或者是Object类型。接收Number类型的时候相当于对应画面大小时的span;接收Object类型时对象的键可以是span、offset、pull、push

在计算属性中有一个gutter属性,通过this.$parend获取到当前el-col的父组件,下面的while循环表示,在el-col的祖先结点中查找到离当前节点距离最近的el-row组件。如果找到了最近的el-col祖先组件,就返回父组件身上所绑定的gutter值,否则为0,Element经常使用这个方法去查找某个组件的最近祖先元素,比如el-from,el-form-item。

 computed: {
    gutter() {
      let parent = this.$parent;
      while (parent && parent.$options.componentName !== 'ElRow') {
        parent = parent.$parent;
      }
      return parent ? parent.gutter : 0;
    }
}

通过props属性以及计算属性,可以在render函数中渲染结点

render(h) {
    let classList = [];
    let style = {};

    if (this.gutter) {
      style.paddingLeft = this.gutter / 2 + 'px';
      style.paddingRight = style.paddingLeft;
    }

    ['span', 'offset', 'pull', 'push'].forEach(prop => {
      if (this[prop] || this[prop] === 0) {
        classList.push(
          prop !== 'span'
            ? `el-col-${prop}-${this[prop]}`
            : `el-col-${this[prop]}`
        );
      }
    });

    ['xs', 'sm', 'md', 'lg', 'xl'].forEach(size => {
      // 如果传递进来进行响应式处理的是具体的数字
      if (typeof this[size] === 'number') {
        classList.push(`el-col-${size}-${this[size]}`);
      } 
      // 如果传递进来的是一个数组
      else if (typeof this[size] === 'object') {
        let props = this[size];
        // 通过Object.keys()拿到键值对,其实感觉用for...in...会更方便
        Object.keys(props).forEach(prop => {
          classList.push(
            prop !== 'span'
              ? `el-col-${size}-${prop}-${props[prop]}`
              : `el-col-${size}-${props[prop]}`
          );
        });
      }
    });

    return h(this.tag, {
      class: ['el-col', classList],
      style
    }, this.$slots.default);
}

样式分析

在theme-chalk文件夹下找到col.scss文件,在文件的起始部分为每一个el-col开头的元素设置了左浮动和border-box属性

[class*="el-col-"] {
  float: left;
  box-sizing: border-box;
}

接下来通过一个循环从0-24设置span、offset、pull、push的样式

// span为0的样式会额外设置一个display:none
.el-col-0 {
  display: none;
}

@for $i from 0 through 24 {
  .el-col-#{$i} {
    width: (1 / 24 * $i * 100) * 1%;
  }

  .el-col-offset-#{$i} {
    margin-left: (1 / 24 * $i * 100) * 1%;
  }

  .el-col-pull-#{$i} {
    position: relative;
    right: (1 / 24 * $i * 100) * 1%;
  }

  .el-col-push-#{$i} {
    position: relative;
    left: (1 / 24 * $i * 100) * 1%;
  }
}

紧接着的会通过名为res的混入生成各个size的响应式布局样式

@include res(xs) {
  .el-col-xs-0 {
    display: none;
  }
  @for $i from 0 through 24 {
    .el-col-xs-#{$i} {
      width: (1 / 24 * $i * 100) * 1%;
    }
    ...
  }
}

我们来看一下这个名为res的混入,这个混入接收两个参数,$key表示传入响应式的key值(xs、sm等)

/* Break-points
 -------------------------- */
@mixin res($key, $map: $--breakpoints) {
  // 循环断点Map,如果存在则返回
  @if map-has-key($map, $key) {
    @media only screen and #{inspect(map-get($map, $key))} {
      @content;
    }
  } @else {
    @warn "Undefeined points: `#{$map}`";
  }
}

如果没有传入第二个参数 m a p , 会 使 用 默 认 值 map,会使用默认值 map使–breakpoints

/* Break-point
--------------------------*/
$--sm: 768px !default;
$--md: 992px !default;
$--lg: 1200px !default;
$--xl: 1920px !default;

$--breakpoints: (
  'xs' : (max-width: $--sm - 1),
  'sm' : (min-width: $--sm),
  'md' : (min-width: $--md),
  'lg' : (min-width: $--lg),
  'xl' : (min-width: $--xl)
);

回到res混入中,通过@if和 scss内置的函数方法map-has-key判断key值是否在 m a p 中 , 如 果 k e y 值 在 map中,如果key值在 mapkeymap中存在,就会生成一个媒体查询。这里的inspect也是scss的内置函数,用来将变量值转换为字符串形式;map-get函数用来获取map中key所对应的value值。

举个栗子,当我们使用

<el-col :xs="8" :sm="6" :md="4" :lg="3" :xl="1"><div class="grid-content bg-purple-light"></div></el-col>

就会分别生成max-width: 765px,min-width: 766px、992px、1200px、1920px的媒体查询,根据画面的不同展示不同的样式,他们对应每一列的宽度也会不一样。

重新封装

官方文档给定el-row的最大宽度有24列,现在如果将其扩展为48列应该如何操作呢?

在前面分析样式文件中,每一列的宽度都是在一个从1至24的循环中设置,最小列宽为 (1 / 24 * 1 * 100) * 1% = (1 / 24)%,如果想要扩展列数,我们只需要将循环的上限扩大为48,以及每一列改为 (1 / 48 * i * 100)* 1%即可。

更改

这样做似乎不行,在页面上显示的时候仍按照24的宽度,期待大佬分享正确改法。

思路的确是这样的,还记得最开始分析目录结构的时候介绍的lib文件夹吗,通过element-ui->lib->theme-chalk->col.css可以看到打包完成的element col的样式。
在这里插入图片描述可以明显的看到列的最大值为24,具体每一列的宽度也计算好了放在文件的后面。而我之前一直是在scss文件中去修改它的循环条件,计算结果最终却没有重新打包,即引用的是未经过打包的、没有修改的css文件,所以最终导致无法显示。也许你会想,那直接修改打包后的文件可以吗?答案是不行,vue项目中的node-module->element-ui文件夹中没有build文件夹。所以无法直接修改项目中的element-ui。

解决方法

首先将ElementUI的源码clone下来并安装依赖

git clone https://github.com/ElemeFE/element.git
cd element
npm install

然后在packages文件夹中去修改目标文件的源代码结构以及theme-chalk下的样式,修改完毕后执行npm run dist进行打包

使用dist打包原因来自官方文档:https://github.com/ElemeFE/element/blob/master/.github/CONTRIBUTING.zh-CN.md

打包结束会生成一个lib文件夹,将他替换掉项目中node_modules->element-ui下的lib文件夹即可(我之前使用的是element 12+,打包后lib文件夹中只有一个index.js,将版本回退到2.4.5的时候打包结果和项目中文件结构相同)

打包的过程中如果你的 node版本 ≥ 12.0 并且 gulp版本 < 4.0,会遇到下面的报错
在这里插入图片描述
面对版本冲突错误,我选择升级gulp的版本,方法可以看gulp官方文档:https://gulpjs.com/docs/en/getting-started/quick-start/

在stackoverflow上面看到了另外一种解决办法:https://stackoverflow.com/questions/55921442/how-to-fix-referenceerror-primordials-is-not-defined-in-node-js,通过在package.json文件里修改配置实现兼容

解决版本冲突后重新打包,将打包后的lib文件夹,替换掉项目中lib文件夹后重启项目,就可以正常使用了。

Container 布局容器

用于布局的容器组件,方便快速搭建页面的基本结构

el-container

打开pakages文件找到contianer组件所在的位置,可以看到这个组件是通过模板编写的

<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>

props属性分析

  • direction:用于定义主题的排列顺序,可以直接传入horizontal或vertical;也可以使用之前介绍过的方法,每个组件都有一个特殊的componentName属性,默认情况下通过插槽获取到子元素的数组,利用该属性的值进行判断。
 // 如果存在插槽 && 存在默认插槽
 return this.$slots && this.$slots.default
 ?   this.$slots.default.some(vnode => {
 	 // 1 && function, 会执行后面的函数
 	 // 0 && function, 不会执行
 	 const tag = vnode.componentOptions && vnode.componentOptions.tag;
 	 // 对结点进行判断是否是el-header || el-footer
	 return tag === 'el-header' || tag === 'el-footer';
 })
 : false;

在这里插入图片描述

样式分析

@import "mixins/mixins";

@include b(container) {
  display: flex;
  flex-direction: row;
  flex: 1;
  flex-basis: auto;
  box-sizing: border-box;
  min-width: 0;

  @include when(vertical) {
    flex-direction: column;
  }
}

首先看到的还是首先将contianer传入混入b,最终生成el-container的类名以及属性。整体来看属性值就是border-box盒子模型以及flex布局,需要多介绍的是min-width属性,使用了这个属性可以让外部盒子元素缩短到比元素中的内容还短。

之后是when混入,负责生成is-vertical的状态判断属性,这里是将元素设置为列排序。

el-main

在container布局里面剩下的几个组件(el-header/aside/footer/main)内容相仿,所以单独挑出main进行举例介绍。

props属性分析

el-header、aside、footer都会接收height或者是width的prop用来设置内联样式。如果不想设置这些属性,你也可以显示的将这个属性设置为null,这样每一个组件的大小就可以由子内容撑开。

样式分析

@import "mixins/mixins";
@import "common/var";

@include b(main) {
  // IE11 supports the <main> element partially https://caniuse.com/#search=main
  display: block;
  flex: 1;
  flex-basis: auto;
  overflow: auto;
  box-sizing: border-box;
  padding: $--main-padding;
}

el-aside、main将overflow设置为auto,使内容超出容器的情况下显示滚动条而不是溢出。

Button按钮

Button是常用的操作按钮,我们经常会对Button组件进行封装

props属性分析在这里插入图片描述

在进行props属性分析之前我们先看看 script 部分。

这里复习一下inject和provide的使用方法。首先看看官网API中的定义
在这里插入图片描述
单纯看概念可能还是会有些生疏,所以借助代码实例来理解:

// 父级组件提供 'foo'
// 父组件通过provide提供一个对象,这个对象的key是foo,value是'bar'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// 子组件注入 'foo'
// inject注入字符串数组,数组项中存放的就是provide提供的key,之后再子组件中通过this.key可以拿到父组件中的value
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}

// inject注入一个对象,本地的key名是foo,value的默认值是待注入的 'foo'
// 需要注意的是通过inject注入属性的时候,default的value值也需要加上单引号来表示,引用的是某个provide提供的key
const Child2 = {
  inject: {
    foo: { default: 'foo' }
  }
  // ...
} 

// 如果在不同的组件中提供了相同名字的 provide,
// 在子组件注册的时候可以使用 from 来表示其源 property
const Child3 = {
  inject: {
    foo: {
      from: 'bar',
      default: 'foo'
    }
  }
  // ...
}

所以在这里,最开始的时候通过inject注入了el-form和el-form-item的组件实例本身(因为他们provide都是自身的this),如果之后需要使用他们身上属性的话,直接通过 this.elForm.xxx | this.elFormItem.xxx就可以直接使用了,仿佛是在button组件中直接操作form-item中的数据,也的确如此!

element的表单控件组件基本上都会使用这种方式来访问form或者from-item身上的实例属性或者是方法。

// button.vue
inject: {
	elForm: {
		default: ''
	},
	elFormItem: {
		default: ''
	}
}
// el-form-item.vue
provide() {
	return {
		elFormItem: this
    };
}

props里面接收到的参数:

  • size:用于定义按钮的大小,对应template中的buttonSize,之后会在computed中介绍
  • type:用于定义按钮的类型,在class的三目表达式中添加上对应的el-button-${type}属性
  • plain、round、circle:用于定义按钮的显示样式,在class中通过传入的true/false来添加对应的'is-'属性
    • 复习一下动态绑定class的方法,:class="[ 普通的css | 三目表达式的css , { 通过bool值判断是否添加的css }]"
    • 举个例子,:class="[‘home-container’, isShow ? ‘show’ : ’ ', { ‘is-active’ : isActive}]"
  • loading、disabled:disabled对应template中的buttonDisabled,之后会在computed中介绍;通过loading判断按钮的加载状态,需要注意的是,如果在加载的状态中,按钮的disabled由disabled和loading是否为真来决定
  • native-type:负责决定原生的button事件
<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>

接下来分析computed中的计算属性

computed: {
      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },
      buttonSize() {
        return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
      },
      buttonDisabled() {
        return this.disabled || (this.elForm || {}).disabled;
      }
},
  • _elFormItemSize:这个计算属性使用的是从el-form-item里面注入的elFormItemSize属性,这个属性在el-form-item.vue中也是一个计算属性,值是在item中传入size时返回的,如果在item里面没有传入size则使用el-form实例身上的size。
 // el-form-item.vue 里的 computed
 _formSize() {
	 return this.elForm.size;
 },
 elFormItemSize() {
 	return this.size || this._formSize;
 },
  • buttonSize:

    • 这个属性首先会应用当前组件中传入的size值
    • 没传入的情况下会使用之前计算的_elFormItemSize
    • 如果仍未false值,则会使用 E l e m e n t 身 上 的 值 。 Element身上的值。 ElementElement在介绍入口文件时已经分析过,我们在注册Element插件的时候可以手动的传入一个数组(元素有size和zIndex)Vue.use(Element, { size: 'small', zIndex: 3000 }),这也是为什么最后写的是(this.$ELEMENT || {}).size
    • 如果都没设定的话结果就为空值
  • buttonDisabled:现在我们看到的是2.4.5版本的Element-ui,该属性会首先获取button->props身上的disabled属性,如果为false,向上查找注入的elForm身上的disabled属性。

    • 这样会导致一个问题:当我们给按钮明确传入false的时候,按钮按照想法来说应该不会被禁用,可是当他发现this.disabled为空时就找elForm身上的disabled,也就是说如果同时为elForm身上设置disabled:true,离el-Form最近的那个按钮就会导致失效。
    • 官方认为这是一个bug,所以在2.15.7版本时修复为:
    buttonDisabled() {
    	return this.$options.propsData.hasOwnProperty('disabled') ? 
    	this.disabled : 
    	(this.elForm || {}).disabled
    }
    

    这样修改的话会首先判断实例button身上的$options.propsData是否有自有属性disabled,来判断组件是否被传入了disabled属性,如果传入了就使用elForm身上的disabled属性。

样式分析

;