结合Vue3.0学习elementUi 源码

结合Vue3.0学习elementUi 源码

前言

用vue3.0把elementui 的组件源码重写一遍,是个不错的学习vue3.0的方式,同时可以深入理解elementui中组件的实现方式
在重写之前当然要先创建一个vue3的项目,再把elementui的源码拿到分析一下要用到的文件有哪些,我这里就直接把组件的文件夹packages下的各组件,packages包里面有一个
theme-chalk的文件夹是存放各组件的样式的文件,这个我直接复制到项目中,在各个组件引入即可用了。

一、element ui 之 container

(一)对container的简单解析

首先看element ui 官网中的第二个组件container,这个组件由header、container、footer、aside各组件组成。

  1. header组件
    header组件的文件结构
    在这里插入图片描述
    index.js文件, 文件内容用于注册文件,当vue实例调用use方法时会自动调用install方法
import Header from './src/main';

/* istanbul ignore next */
Header.install = function(Vue) {
  Vue.component(Header.name, Header);
};

export default Header;

main.vue文件

标签<slot></slot>插槽,用于插入其他内容,props接收组件传值,通过height属性来改变内容高度

<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>
  1. container组件

与header组件类似,的文件结构,index.js文件用于注册组件,main.js文件为组件内容

index.js

import Container from './src/main';

/* istanbul ignore next */
Container.install = function(Vue) {
  Vue.component(Container.name, Container);
};

export default Container;

main.vue

direction属性是用于设置组件中的内容布局方式,这个属性会控制样式中flex布局是row还是colum,当为vertical时垂直排列horizontal时水平排列,当出现标签为el-headerel-footer标签时,设置为垂直排列,其他情况为默认的水平排列

<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>
  1. footer组件
    组件结构与上面header组件一样,index.js注册组件,main.vue组件内容,index.js文件不展示代码,直接看main.vue文件,height属性控制高度
    main.vue
<template>
  <footer class="el-footer" :style="{ height }">
    <slot></slot>
  </footer>
</template>

<script>
  export default {
    name: 'ElFooter',

    componentName: 'ElFooter',

    props: {
      height: {
        type: String,
        default: '60px'
      }
    }
  };
</script>
  1. aside组件
    width属性设置aside组件的宽度
<template>
  <aside class="el-aside" :style="{ width }">
    <slot></slot>
  </aside>
</template>

<script>
  export default {
    name: 'ElAside',

    componentName: 'ElAside',

    props: {
      width: {
        type: String,
        default: '300px'
      }
    }
  };
</script>
  1. header组件, container组件,footer组件,aside组件
    vue3中注册组件

(二)、用Vue3.0的方式重写container

import Header from './src/index.vue';
export default {
    install: (app) => {
        app.component('el-header', Header);
    }
}

header、footer、aside组件内容没有变,contaier组件内容有点变化,vue3中setup()会被立即执行,state相当于vue2中的data,vue3中通过reactive()来实现响应式数据,或者ref()来实现响应式,
setup()返回state,就可以用于数据渲染了通过state.属性的方式渲染

<template>
    <section class="el-container" :class="{ 'is-vertical': state.isVertical }"></section>
    <slot></slot>
</template>
<script>
import '../../plaginStyle/src/container.scss'; //引入组件样式
import {ref, reactive, computed} from 'vue'; // 引入接口
export default {
    name: 'ElContainer',

    componentName: 'ElContainer',

    props: {
        direction: String
    },

    setup(props, context) {
        const state = reactive({
            isVertical: computed(() => {
                if (props.direction === 'vertical') {
                    return true;
                } else {
                    return false;
                }
   
                return context.slots && context.slots.default
                    ? this.slots.default.some(vnode => {
                        const tag = vnode.componentOptions && vnode.componentOptions.tag;
                        return tag === 'el-header' || tag === 'el-footer';
                    })
                    : false;
            })
        });

        return {
            state
        }
    }
}
</script>

(三)、container用vue3.0写的差异部分

与vue2不同的是
1、vue3通过apireactive()或者ref()来实现数据的响应,
关于这两个接口的详细解释composition Api
2、书写方式不一样,因为container部分,大部分都是通过样式控制的,与vue2除了写法上有不一样,其他实现都是一样的,vue3.0的具体书写方式的描写可以通过官网更详细的学习

二、element ui 之 layout

(一)、layout 简单解析

layout部分,是由row, col各组件组成。

  1. row组件
    组件的文件结构和上面的container各组件是类似的,注册方式也是一样的,只是引用的组件和名称是各自的。这里直接看组件的内容
export default {
  name: 'ElRow', // 组件名称

  componentName: 'ElRow', // 组件名称

  props: {  // 接收的父组件传递的各个参数
    tag: {  // 自定义元素标签
      type: String,
      default: 'div'
    },
    gutter: Number, // 栅格间隔,每行中各小部分间隔
    type: String,  //布局模式,可选 flex,现代浏览器下有效
    justify: { // felx布局下水平排列方式
      type: String,
      default: 'start'
    },
    align: { // felx布局下垂直布局方式
      type: String,
      default: 'top'
    }
  },

  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 !== 'top' ? `is-align-${this.align}` : '',
        { 'el-row--flex': this.type === 'flex' }
      ],
      style: this.style
    }, this.$slots.default);
  }
};

与container部分不同的是它的内容是一个js文件,通过渲染函数动态渲染。h()方法的三个参数: 第一个,渲染时要创建的标签;第二个,该标签的所有要用到的属性;第三个,子元素可以是插槽对象

  1. col组件
    组件的文件结构和上面的container各组件是类似的,注册方式也是一样的,只是引用的组件和名称是各自的。这里直接看组件的内容
export default {
  name: 'ElCol', //组件名称

  props: { // 接收父租价传参
    span: { //栅格占据的列数 默认24为最大
      type: Number,
      default: 24
    },
    tag: { //自定义元素标签
      type: String,
      default: 'div'
    },
    offset: Number, // 栅格左侧的间隔格数
    pull: Number, // 栅格向右移动格数
    push: Number, //栅格向左移动格数
    xs: [Number, Object],  //<768px 响应式栅格数或者栅格属性对象
    sm: [Number, Object], // ≥768px 响应式栅格数或者栅格属性对象
    md: [Number, Object], // ≥992px 响应式栅格数或者栅格属性对象
    lg: [Number, Object], // ≥1200px 响应式栅格数或者栅格属性对象
    xl: [Number, Object] //  ≥1920px 响应式栅格数或者栅格属性对象	
  },

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

	// 遍历参数,将存在的参数对应的类添加到类数组中
    ['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(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);
  }
};

(二)、用vue3.0重写Layout

  1. row组件
    直接看组件内容,
import '../../../plaginStyle/src/row.scss'; // 组件样式
import {ref, reactive, computed, h} from 'vue'; // api
export default {
    name: 'ElRow',

    componentName: 'ElRow',

    props: { // 接收父组件的传参
        tag: { // 自定义标签
            type: String,
            default: 'div'
        },
        gutter: Number, // 栅格间隔(左右)
        type: String, // 样式布局类型
        justify: {    // flex布局下 子元素的水平排列方式
            type: String,
            default: 'start'
        },
        align: {  // flex布局下 子元素的垂直排列方式
            type: String,
            default: 'top'
        }
    },
    setup(props, context) { 
        const state = reactive({
            style: computed(() => {
                const ret = {};

                if (props.gutter) {
                    ret.marginLeft = `-${this.gutter / 2}px`;
                    ret.marginRight = ret.marginRight;
                }

                return ret;
            })
        })
        return () => h(props.tag, {
            class: [
                'el-row',
                props.justify !== 'start' ? `is-justify-${props.justify}` : '',
                props.align !== 'top' ? `is-align-${props.align}` : '',
                { 'el-row--flex': props.type === 'flex' }
            ],
            style: state.style
        }, context.slots);
    }
};

代码逻辑是一样的,这里需要注意的是,没有render()函数了,而是通过setup()直接返回h()渲染。获取插槽的内容是通过setup()的参数context获取。computed计算属性是在reactive()里定义

  1. col组件
    直接看内容

import '../../../plaginStyle/src/col.scss';
import {ref, reactive, computed, h} from 'vue';

export default {
    name: 'ElCol',

    props: {
        span: {
            type: Number,
            default: 24
        },
        tag: {
            type: String,
            default: 'div'
        },
        offset: Number,
        pull: Number,
        push: Number,
        xs: [Number, Object],
        sm: [Number, Object],
        md: [Number, Object],
        lg: [Number, Object],
        xl: [Number, Object]
    },

    setup(props, context) {
        const state = reactive({
            gutter: computed(() => {
                let parent = context.parent;
                while (parent && parent.$options.componentName !== 'ElRow') {
                    parent = parent.$parent;
                }
                return parent ? parent.gutter : 0;
            })
        })

        return () => {
            let classList = [];
            let style = {};

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

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

            ['xs', 'sm', 'md', 'lg', 'xl'].forEach(size => {
                if (typeof props[size] === 'number') {
                    classList.push(`el-col-${size}-${props[size]}`);
                } else if (typeof props[size] === 'object') {
                    let pop = props[size];
                    Object.keys(pop).forEach(prop => {
                        classList.push(
                            prop !== 'span'
                                ? `el-col-${size}-${prop}-${pop[prop]}`
                                : `el-col-${size}-${pop[prop]}`
                        );
                    });
                }
            });

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

col组件和row组件一样,逻辑实现没有改变,差异与row组件中的差异一样

(三)、layout用vue3写的差异部分

1、vue3中的render直接返回一个函数,函数的返回值就是h(),vue3的渲染具体解析可看
2、vue3中,$slots对象是通过context参数中获取context.slots, setup()函数参数的具体解析
3、vue3中,computed属性是在reactive()函数中定义的,方式:属性值:computed(() => { return value}) vue3 computed计算属性
4、vue3中用到的api需要从vue中引入

三、element ui 之 button

(一)、button组件的简单解析

button部分是由 button组件和button-group组件共同组成,
该部分的文件结构如下:
在这里插入图片描述
index文件是用于注册各组件的,方式和上面了解的一样,内容文件是.vue文件,通过名称可以知道对应的组件内容,直接看内容

  1. button组件
<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 属性
        type: String,
        default: 'button'
      },
      loading: Boolean, // 是否加载状态
      disabled: Boolean, //是否禁用状态
      plain: Boolean, // 是否朴素按钮
      autofocus: Boolean, // 是否聚焦
      round: Boolean, //是否圆角
      circle: Boolean // 是否圆形
    },

    computed: {
      _elFormItemSize() { // 获取form表单对象的属性
        return (this.elFormItem || {}).elFormItemSize;
      },
      buttonSize() { // 按钮的大小
        return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
      },
      buttonDisabled() { // 按钮是否禁用
        return this.disabled || (this.elForm || {}).disabled;
      }
    },

    methods: {
      handleClick(evt) { // 向父级传值,方法名称click
        this.$emit('click', evt);
      }
    }
  };
</script>

首先是template部分,这里面是一个button标签,button标签包裹两个i标签和一个span标签,第一i标签是指定loading图标,第二个是自定义图标,span标签是用于存放插槽内容的,也就是button的文字描述,提交或重置之类的。
其次是button中用到的属性,都在上面代码有注释

  1. button-group组件
<template>
  <div class="el-button-group">
    <slot></slot>
  </div>
</template>
<script>
  export default {
    name: 'ElButtonGroup'
  };
</script>

这个组件没有什么内容就是向该组件内部插入内容,可以有多个按钮

(二)、vue3重写button

文件结构和上面的一样,直接看内容
button组件

<template>
    <button 
        class="el-button"
        @click="handleClick"
        :disabled="state.buttonDisabled || loading"
        :autofocus="autofocus"
        :type="nativeType"
        :class="[
        type ? 'el-button--' + type : '',
        state.buttonSize ? 'el-button--' + state.buttonSize : '',
        {
            'is-disabled': state.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="context.slots"><slot></slot></span>
    </button>
</template>
<script>
import '../../plaginStyle/src/button.scss'
import {ref, reactive, computed, watchEffect, onMounted} from 'vue';
export default {
    name: 'ElButton',

    props: {
        type: { //按钮类型 primary / success / warning / danger / info / text
            type: String,
            default: 'default'
        },
        size: { // 按钮尺寸
            type: String,
            default: 'small'
        },
        icon: { // 按钮自定义的icon icon为class名称
            type: String,
            default: ''
        },
        nativeType: { // 按钮原生type属性 button / submit / reset
            type: String,
            default: 'button'
        },
        loading: Boolean, // loading的icon图标是否展示
        disabled: Boolean, // 是否禁用按钮
        plain: Boolean, // 是否朴素按钮
        autofocus: Boolean, // 是否默认聚焦
        round: Boolean, // 是否圆角按钮
        circle: Boolean // 是否圆形按钮
    },
    setup(props, context) {
        const state = reactive({
            buttonSize: computed(() => {
                return props.size
            }),
            buttonDisabled: computed(() => {
                return props.disabled || false
            }), 
        })
        function handleClick(evt) {
            context.emit('click', evt);
        }
        return {
            handleClick,
            state,
            context
        }
    }
}
</script>
<style lang="scss" scoped>
</style>

button组件中与vue2的差异上面提到的都在这个组件体现了,需要注意的一点是,vue2中的$emit对象,在vue3中是通过context参数获取的context.emit,使用方式不变

button-group组件没有变化,这里不重复

(三)、vue3重新button组件差异

加上上面提到的差异,加上一条:子组件向父组件传值时,通过context来获取emit对象,详细解释上面有相关链接

四、element ui 之 link

(一)、组件的简单解析

直接看组件内容

<template>
  <a
    :class="[
      'el-link',
      type ? `el-link--${type}` : '',
      disabled && 'is-disabled',
      underline && !disabled && 'is-underline'
    ]"
    :href="disabled ? null : href"
    v-bind="$attrs"
    @click="handleClick"
  >

    <i :class="icon" v-if="icon"></i>

    <span v-if="$slots.default" class="el-link--inner">
      <slot></slot>
    </span>

    <template v-if="$slots.icon"><slot v-if="$slots.icon" name="icon"></slot></template>
  </a>
</template>

<script>

export default {
  name: 'ElLink',

  props: {
    type: { // 链接类型 primary / success / warning / danger / info
      type: String,
      default: 'default'
    },
    underline: { // 是否带下滑线
      type: Boolean,
      default: true
    },
    disabled: Boolean, // 是否禁用
    href: String, // 原生属性
    icon: String // 图标类名
  },

  methods: {
    handleClick(event) {
      if (!this.disabled) {
        if (!this.href) {
          this.$emit('click', event);
        }
      }
    }
  }
};
</script>

link组件内容简单,在a标签中v-bind=“ a t t r s ” 他 的 作 用 是 包 含 了 父 作 用 域 中 不 作 为 组 件 p r o p s 或 自 定 义 事 件 。 当 一 个 组 件 没 有 声 明 任 何 p r o p 时 , 这 里 会 包 含 所 有 父 作 用 域 的 绑 定 , 并 且 可 以 通 过 v − b i n d = " attrs” 他的作用是 包含了父作用域中不作为组件 props 或自定义事件。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定,并且可以通过 v-bind=" attrspropspropvbind="attrs" 传入内部组件——在创建高阶的组件时非常有用。

(二)、用vue3重写link

直接看内容

<template>
    <a 
        :class="[
            'el-link',
            type ? `el-link--${type}` : '',
            disabled && 'is-disabled',
            underline && !disabled && 'is-underline'
        ]"
        :href="disabled ? null : href"
        v-bind="context.attrs"
        @click="handleClick"
    >
    
    <i :class="icon" v-if="icon"></i>

    <span v-if="context.slots.default" class="el-link--inner">
        <slot></slot>
    </span>

    <template v-if="context.slots.icon"><slot v-if="context.slots.icon" name="icon"></slot></template>
    </a>
</template>

<script>
import '../../../plaginStyle/src/link.scss';
import {ref, reactive, computed} from 'vue';
export default {
    name: 'ElLink',

    props: {
        type: {
            type: String,
            default: 'default'
        },
        underline: {
            type: Boolean,
            default: true
        },
        disabled: Boolean,
        herf: String,
        icon: String
    },

    setup(props, context) {
        function  handleClick(event) {
            if (!props.disabled) {
                if (!props.herf) {
                    context.emit('click', event);
                }
            }
        };
        return {
            context,
            handleClick
        };
    }
}
</script>

实现逻辑是一样的,v-bind=“ a t t r s ” 的 实 现 方 式 是 一 样 的 , 只 要 把 " attrs”的实现方式是一样的,只要把" attrs"attrs"对象的获取方式改成context.attrs。

(三)、link组件用vue3写的差异

大部分差异的部分在上面已经有说明过,加上一点,vue2中的$attrs,在vue3
中是通过context参数来获取的,方式:context.attrs

五、element ui 之 radio组件

(一)、组件简单解析

radio组件是由,radio组件,radio-button组件、radio-group组件共同组成。
组件文件结构
在这里插入图片描述
下面看组件内容
radio组件

<template>
  <label
    class="el-radio"
    :class="[
      border && radioSize ? 'el-radio--' + radioSize : '',
      { 'is-disabled': isDisabled },
      { 'is-focus': focus },
      { 'is-bordered': border },
      { 'is-checked': model === label }
    ]"
    role="radio"
    :aria-checked="model === label"
    :aria-disabled="isDisabled"
    :tabindex="tabIndex"
    @keydown.space.stop.prevent="model = isDisabled ? model : label"
  >
    <span class="el-radio__input"
      :class="{
        'is-disabled': isDisabled,
        'is-checked': model === label
      }"
    >
      <span class="el-radio__inner"></span>
      <input
        ref="radio"
        class="el-radio__original"
        :value="label"
        type="radio"
        aria-hidden="true"
        v-model="model"
        @focus="focus = true"
        @blur="focus = false"
        @change="handleChange"
        :name="name"
        :disabled="isDisabled"
        tabindex="-1"
      >
    </span>
    <span class="el-radio__label" @keydown.stop>
      <slot></slot>
      <template v-if="!$slots.default">{{label}}</template>
    </span>
  </label>
</template>
<script>
  import Emitter from 'element-ui/src/mixins/emitter';

  export default {
    name: 'ElRadio',

    mixins: [Emitter],

    inject: {
      elForm: {
        default: ''
      },

      elFormItem: {
        default: ''
      }
    },

    componentName: 'ElRadio',

    props: {
      value: {}, // 涉及到了v-model里面语法糖的原理,接收的是radio值
      label: {}, // 当前radio的值
      disabled: Boolean, // 是否禁用
      name: String, // 原生属性
      border: Boolean, // 是否加边框
      size: String // 尺寸大小
    },

    data() {
      return {
        focus: false 
      };
    },
    computed: {
      isGroup() {   // 判断radio组件的父组件,逐级向上直到找到为radio-group组件时,赋值_radioGroup为radio-group组件
        let parent = this.$parent;
        while (parent) {
          if (parent.$options.componentName !== 'ElRadioGroup') {
            parent = parent.$parent;
          } else {
            this._radioGroup = parent;
            return true;
          }
        }
        return false;
      },
      model: { // v-model绑定的值,用于获取选中radio的值和设置raadio的值,实现双向的绑定
        get() {
          return this.isGroup ? this._radioGroup.value : this.value;
        },
        set(val) {
          if (this.isGroup) {
            this.dispatch('ElRadioGroup', 'input', [val]);
          } else {
            this.$emit('input', val);
          }
          this.$refs.radio && (this.$refs.radio.checked = this.model === this.label);
        }
      },
      _elFormItemSize() { // 获取表单项的尺寸
        return (this.elFormItem || {}).elFormItemSize;
      },
      radioSize() { // radio尺寸, 逐级向上取值
        const temRadioSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
        return this.isGroup
          ? this._radioGroup.radioGroupSize || temRadioSize
          : temRadioSize;
      },
      isDisabled() { // 是否禁用
        return this.isGroup
          ? this._radioGroup.disabled || this.disabled || (this.elForm || {}).disabled
          : this.disabled || (this.elForm || {}).disabled;
      },
      tabIndex() { // 这个和键盘上下键控制有关,不是很董
        return (this.isDisabled || (this.isGroup && this.model !== this.label)) ? -1 : 0;
      }
    },

    methods: {
      handleChange() {
        this.$nextTick(() => {  
        	// 向父级传值,在radio值改变时触发
          this.$emit('change', this.model);
          this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);
        });
      }
    }
  };
</script>

radio组件的标签结构组成:label标签下是一个span标签,到这里还看不到组件具体长啥样,span标签的里面包裹了第一个span标签(组件的圆点),第二个input(真正的原生radio,通过样式隐藏起来了),第三个span就是圆点后面关联的文字了,这里radio的样子才完全出来了,
radio组件的逻辑实现:
isGroup方法就是逐级向上判断是否有radio-group组件,并且赋值该实例对象;
model属性是与input中v-model绑定的,这里涉及到了vue ,v-model语法糖的原理,放上链接:https://juejin.cn/post/6844903662284718088
上面链接有详细的解释v-model的原理,这里简单说一下,v-model之所以会实时改变,是它本身的实现方式是,在input的触发input或change事件时执行改变当前input标签的值。
其他属性从名称上可以知道,具体的作用,
dispatch方法是一个工具类方法,直接引入,这个方法的作用是向指定的组件派发方法传值,

radio-group组件

<template>
  <component
    :is="_elTag"
    class="el-radio-group"
    role="radiogroup"
    @keydown="handleKeydown"
  >
    <slot></slot>
  </component>
</template>
<script>
  import Emitter from 'element-ui/src/mixins/emitter';

  const keyCode = Object.freeze({
    LEFT: 37,
    UP: 38,
    RIGHT: 39,
    DOWN: 40
  });
  export default {
    name: 'ElRadioGroup',

    componentName: 'ElRadioGroup',

    inject: {
      elFormItem: {
        default: ''
      }
    },

    mixins: [Emitter],

    props: {
      value: {},
      size: String,
      fill: String,
      textColor: String,
      disabled: Boolean
    },

    computed: {
      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },
      _elTag() {
        return (this.$vnode.data || {}).tag || 'div';
      },
      radioGroupSize() {
        return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
      }
    },

    created() {
      this.$on('handleChange', value => {
        this.$emit('change', value);
      });
    },
    mounted() {
      // 当radioGroup没有默认选项时,第一个可以选中Tab导航
      const radios = this.$el.querySelectorAll('[type=radio]');
      const firstLabel = this.$el.querySelectorAll('[role=radio]')[0];
      if (![].some.call(radios, radio => radio.checked) && firstLabel) {
        firstLabel.tabIndex = 0;
      }
    },
    methods: {
      handleKeydown(e) { // 左右上下按键 可以在radio组内切换不同选项
        const target = e.target;
        const className = target.nodeName === 'INPUT' ? '[type=radio]' : '[role=radio]';
        const radios = this.$el.querySelectorAll(className);
        const length = radios.length;
        const index = [].indexOf.call(radios, target);
        const roleRadios = this.$el.querySelectorAll('[role=radio]');
        switch (e.keyCode) {
          case keyCode.LEFT:
          case keyCode.UP:
            e.stopPropagation();
            e.preventDefault();
            if (index === 0) {
              roleRadios[length - 1].click();
              roleRadios[length - 1].focus();
            } else {
              roleRadios[index - 1].click();
              roleRadios[index - 1].focus();
            }
            break;
          case keyCode.RIGHT:
          case keyCode.DOWN:
            if (index === (length - 1)) {
              e.stopPropagation();
              e.preventDefault();
              roleRadios[0].click();
              roleRadios[0].focus();
            } else {
              roleRadios[index + 1].click();
              roleRadios[index + 1].focus();
            }
            break;
          default:
            break;
        }
      }
    },
    watch: {
      value(value) {
        this.dispatch('ElFormItem', 'el.form.change', [this.value]);
      }
    }
  };
</script>


radio-group组件的标签结构就是接收插入的内容,radio-group组件是在created方法中监听了子组件radio派发的handleChange方法后再向父组件派发chang方法,从而改变radio-group组件中v-model绑定的值,并实现了单项选择,

radio-button组件

<template>
  <label
    class="el-radio-button"
    :class="[
      size ? 'el-radio-button--' + size : '',
      { 'is-active': value === label },
      { 'is-disabled': isDisabled },
      { 'is-focus': focus }
    ]"
    role="radio"
    :aria-checked="value === label"
    :aria-disabled="isDisabled"
    :tabindex="tabIndex"
    @keydown.space.stop.prevent="value = isDisabled ? value : label"
  >
    <input
      class="el-radio-button__orig-radio"
      :value="label"
      type="radio"
      v-model="value"
      :name="name"
      @change="handleChange"
      :disabled="isDisabled"
      tabindex="-1"
      @focus="focus = true"
      @blur="focus = false"
    >
    <span
      class="el-radio-button__inner"
      :style="value === label ? activeStyle : null"
      @keydown.stop>
      <slot></slot>
      <template v-if="!$slots.default">{{label}}</template>
    </span>
  </label>
</template>
<script>
  import Emitter from 'element-ui/src/mixins/emitter';

  export default {
    name: 'ElRadioButton',

    mixins: [Emitter],

    inject: {
      elForm: {
        default: ''
      },
      elFormItem: {
        default: ''
      }
    },

    props: {
      label: {},
      disabled: Boolean,
      name: String
    },
    data() {
      return {
        focus: false
      };
    },
    computed: {
      value: {
        get() {
          return this._radioGroup.value;
        },
        set(value) {
          this._radioGroup.$emit('input', value);
        }
      },
      _radioGroup() {
        let parent = this.$parent;
        while (parent) {
          if (parent.$options.componentName !== 'ElRadioGroup') {
            parent = parent.$parent;
          } else {
            return parent;
          }
        }
        return false;
      },
      activeStyle() { 
        return {
          backgroundColor: this._radioGroup.fill || '',
          borderColor: this._radioGroup.fill || '',
          boxShadow: this._radioGroup.fill ? `-1px 0 0 0 ${this._radioGroup.fill}` : '',
          color: this._radioGroup.textColor || ''
        };
      },
      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },
      size() {
        return this._radioGroup.radioGroupSize || this._elFormItemSize || (this.$ELEMENT || {}).size;
      },
      isDisabled() {
        return this.disabled || this._radioGroup.disabled || (this.elForm || {}).disabled;
      },
      tabIndex() {
        return (this.isDisabled || (this._radioGroup && this.value !== this.label)) ? -1 : 0;
      }
    },

    methods: {
      handleChange() {
        this.$nextTick(() => {
          this.dispatch('ElRadioGroup', 'handleChange', this.value);
        });
      }
    }
  };
</script>

radio-button组件的标签结构很像,唯一不同的是没有了圆点,
实现逻辑上基本一致,唯一不同的是,它只能和radio-group组合使用,它不能单独向父级传值,必须通过radio-group这一层

(二)、vue3 radio组件重写

radio组件

<template>
    <label 
        class="el-radio"
        :class="[
            border && state.radioSize ? 'el-radio--' + state.radioSize : '',
            { 'is-disabled': state.isDisabled },
            { 'is-focus': state.focus },
            { 'is-bordered': border },
            { 'is-checked': state.model === label }
        ]"
        role="radio"
        :aria-checked="state.model === label"
        :aria-disabled="state.isDisabled"
        :tabindex="state.tabIndex"
        @keydown.space.stop.prevent="state.model = state.isDisabled ? state.model : label"
    >
        <span class="el-radio__input"
            :class="{
                'is-disabled': state.isDisabled,
                'is-checked': state.model === label
            }"
        >
            <span class="el-radio__inner"></span>
            <input
                ref="radio"
                class="el-radio__original"
                :value="label"
                type="radio"
                aria-hidden="true"
                v-model="state.model"
                @focus="state.focus = true"
                @blur="state.focus = false"
                @change.stop.prevent="handleChange"
                :name="name"
                :disabled="state.isDisabled"
                tabindex="-1"
            >
        </span>
        <span class="el-radio__label" @keydown.stop>
            <slot></slot>
            <template v-if="!context.slot">{{label}}</template>
        </span>
    </label>
</template>
<script>
import '../../../plaginStyle/src/radio.scss'
import {ref, reactive, computed, onMounted, getCurrentInstance} from 'vue';
import Emitter from '../../../plaginSrc/mixins/emitter.js';
import stepVue from '../../../../../element-dev/packages/steps/src/step.vue';

export default {
    name: 'ElRadio',

    componentName: 'ElRadio',

    props: {
        modelValue: {},
        label: {},
        disabled: Boolean,
        name: String,
        border: Boolean,
        size: String
    },

    setup(props, context) {
        const currentInstance = getCurrentInstance();
        const {dispatch, broadcast} = Emitter(currentInstance);
        const radio = ref(null);
        const state = reactive({
            focus: false,
            _radioGroup: null,
            isGroup: computed(() => {
                let parent = currentInstance.parent;
                while (parent) {
                    if (parent.type.componentName !== 'ElRadioGroup') {
                        parent = parent.parent;
                    } else {
                        state._radioGroup = parent;
                        return true;
                    }
                }
                return false;
            }),
            model: computed({
                get: () => {
                    return state.isGroup ? state._radioGroup.props.modelValue : props.modelValue
                },
                set: (val) => {
                    if (state.isGroup) {
                        dispatch('ElRadioGroup', 'update:modelValue', [val]);
                    } else {
                        context.emit('update:modelValue', val);
                    }
                    
                    radio.value && (radio.value.checked = state.model === props.label)
                }
            }),
            _elFormItemSize: computed(() => {
                return '';
            }),
            radioSize: computed(() => {
                const temRadioSize = props.size;
                return state.isGroup
                    ? state._radioGroup.props.radioGroupSize || temRadioSize
                    : temRadioSize;
            }),
            isDisabled: computed(() => {
                return state.isGroup
                    ? state._radioGroup.props.disabled || props.disabled 
                    : props.disabled;
            }),
            tabIndex: computed(() => {
                return (state.isDisabled  || (state.isGroup && state.model !== props.label)) ? -1 : 0;
            })
        });
        onMounted(() => {
            
        })
        function handleChange() {
            context.emit('change', state.model);
            state.isGroup && dispatch('ElRadioGroup', 'change', state.model);
        }

        return {
            state,
            handleChange,
            context,
            radio
        }
    }
}
</script>

标签结构和vue2是一样的,主要是里面的渲染数据写法不一样
逻辑实现是一样的,区别在于里面有些对象,获取的方式不同,
1、isGroup是通过currentInstance获取父级对象的,currentInstance对象是再setup方法里面被定义,它是从vue中引入的getCurrentInstance()方法获取,currentInstance相当于vue2中的this,
2、model这个和vue2,中v-model的原理是一样的,区别在vue2中获取选中值得value要用modelValue名,向父级传值时得方法change要改成update:modelValue 参考链接:https://v3.cn.vuejs.org/guide/component-basics.html#%E5%9C%A8%E7%BB%84%E4%BB%B6%E4%B8%8A%E4%BD%BF%E7%94%A8-v-model

radio-group组件

<template>
    <component
        :is="state._elTag"
        class="el-radio-group"
        role="radiogroup"
        @keydown="handleKeydown"
    >
        <slot></slot>
    </component>
</template>
<script>
import '../../../plaginStyle/src/radio-group.scss';
import Emitter from '../../../plaginSrc/mixins/emitter.js';
import {
    ref, 
    reactive, 
    computed, 
    getCurrentInstance, 
    onMounted,
    watch,
    inject
} from 'vue';

const keyCode = Object.freeze({
    LEFT: 37,
    UP: 38,
    RIGHT: 39,
    DOWN: 40
})
export default {
    name: 'ElRadioGroup',

    componentName: 'ElRadioGroup',

    props: {
        modelValue: {},
        size: String,
        fill: String,
        textColor: String,
        disabled: Boolean
    },

    setup(props, context) {
        const elFormItem = inject(elFormItem, ref(''));
        const currentInstance = getCurrentInstance();
        const {ctx} = getCurrentInstance();
        const {dispatch} = Emitter(currentInstance);
        const state = reactive({
        _elFormItemSize: computed(() => {
            return (elFormItem || {}).elFormItemSize;
        }),
        _elTag: computed(() => {
            return (currentInstance.vnode.data || {}).tag || 'div';
        }),
        radioGroupSize: computed(() => {
            return props.size || state._elFormItemSize
        })
        });
        onMounted(() => {
            const radios = currentInstance.vnode.el.querySelectorAll('[type=radio]');
            const firstLabel = currentInstance.vnode.el.querySelectorAll('[type=radio]')[0];
            if (![].some.call(radios, radio => radio.checked) && firstLabel) {
                firstLabel.tabIndex = 0;
            }
        });

        watch(() => props.modelValue, (modelValue, preModelValue) => {
            dispatch('ElFormItem', 'el.form.change', [props.modelValue]);
        })

        function handleKeydown(e) {
            const target = e.target;
            const className = target.nodeName === 'INPUT' ? '[type=radio]' : '[role=radio]';
            const radios = currentInstance.vnode.el.querySelectorAll(className);
            const length = radios.length;
            const index = [].indexOf.call(radios, target);
            const roleRadios = currentInstance.vnode.el.querySelectorAll('[role=radio]');
            switch (e.keyCode) {
                case keyCode.LEFT:
                case keyCode.UP:
                    e.stopPropagation();
                    e.preventDefault();
                    if (index === 0) {
                        roleRadios[length - 1].click();
                        roleRadios[length - 1].focus();
                    } else {
                        roleRadios[index - 1].click();
                        roleRadios[index - 1].focus();
                    }
                    break;
                case keyCode.RIGHT:
                case keyCode.DOWN:
                    if (index === (length - 1)) {
                        e.stopPropagation();
                        e.preventDefault();
                        roleRadios[0].click();
                        roleRadios[0].focus();
                    } else {
                        roleRadios[index + 1].click();
                        roleRadios[index + 1].focus();
                    }
                    break;
                default:
                    break;
            }
        };


        return {
            state,
            handleKeydown
        }
    }
}
</script>

radio-button组件

<template>
    <label
        class="el-radio-button"
        :class="[
            state.size ? 'el-radio-button--' + state.size : '',
            { 'is-active': state.modelValue === label },
            { 'is-disabled': state.isDisabled },
            { 'is-focus': state.focus }
        ]"
        role="radio"
        :aria-checked="state.modelValue === label"
        :aria-disabled="state.isDisabled"
        :tabindex="state.tabIndex"
        @keydown.space.stop.prevent="state.modelValue = state.isDisabled ? state.modelValue : label"
    >
        <input 
            class="el-radio-button__orig-radio"
            :value="label"
            type="radio"
            v-model="state.modelValue"
            :name="name"
            @change.stop="handleChange"
            :disabled="state.isDisabled"
            tabindex="-1"
            @focus="state.focus = true"
            @blur="state.focus = false"
        >
        <span
            class="el-radio-button__inner"
            :style="state.modelValue === label ? state.activeStyle : null"
            @keydown.stop>
            <slot></slot>
            <template v-if="!context.slots.default">{{label}}</template>
        </span>
    </label>
</template>
<script>
import '../../../plaginStyle/src/radio-button.scss';
import Emitter from '../../../plaginSrc/mixins/emitter.js';
import {
    ref, 
    reactive, 
    computed, 
    getCurrentInstance,
    inject,
    onMounted
} from 'vue';


export default {
  name: 'ElRadioButton',

  props: {
      label: {},
      disabled: Boolean,
      name: String
  },
  
  setup(props, context) {
      const elForm = inject(elForm, ref(''));
      const elFormItem = inject(elFormItem, ref(''));
      const currentInstance = getCurrentInstance();
      const {dispatch} = Emitter(currentInstance);
      const state = reactive({
          focus: false,
          modelValue: computed({
              get: () => {
                  return state._radioGroup.props.modelValue;
              },
              set: (value) => {
                  state._radioGroup.emit('update:modelValue', value);
              }
          }),
          _radioGroup: computed(() => {
              let parent = currentInstance.parent;
              while (parent) {
                  if (parent.type.componentName !== 'ElRadioGroup') {
                      parent = parent.parent;
                  } else {
                      return parent;
                  }
              }
              return false;
          }),
          activeStyle: computed(() => {
              return {
                  backgroundColor: state._radioGroup.ctx.fill || '',
                  borderColor: state._radioGroup.ctx.fill || '',
                  boxShadow: state._radioGroup.ctx.fill ? `-1px 0 0 0 ${state._radioGroup.ctx.fill}` : '',
                  color: state._radioGroup.ctx.textColor || '' 
              };
          }),
          _elFormItemSize: computed(() => {
              return (elFormItem || {}).elFormItemSize;
          }),
          size: computed(() => {
              return state._radioGroup.ctx.radioGroupSize || state._elFormItemSize
          }),
          isDisabled: computed(() => {
              return props.disabled || state._radioGroup.ctx.disabled || (state.elForm || {}).disabled;
          }),
          tabIndex: computed(() => {
              return (state.isDisabled || (state._radioGroup && state.modelValue !== props.label)) ? -1 : 0;
          })
      });
        function  handleChange() {
            dispatch('ElRadioGroup', 'change', state.modelValue);
        }
    return {
        state,
        context,
        handleChange
    }
  }
    
}
</script>

(三)、差异

1、vue3获取当前组件实例对象,不再是this,而是通过api getCurrentInstance方法获取,得到的对象和this会有区别,
2、在组件上使用v-model时,其语法上有区别,上面链接有详细解释

六、element ui 之 checkbox

(一)、checkbox简单解析

checkbox部分由checkbox、checkbox-button、checkbox-group组件构成
文件结构,和button组件的结构一样,
在这里插入图片描述
再来看各组件的具体内容
checkbox组件

<template>
  <label
    class="el-checkbox"
    :class="[
      border && checkboxSize ? 'el-checkbox--' + checkboxSize : '',
      { 'is-disabled': isDisabled },
      { 'is-bordered': border },
      { 'is-checked': isChecked }
    ]"
    :id="id"
  >
    <span class="el-checkbox__input"
      :class="{
        'is-disabled': isDisabled,
        'is-checked': isChecked,
        'is-indeterminate': indeterminate,
        'is-focus': focus
      }"
      :tabindex="indeterminate ? 0 : false"
      :role="indeterminate ? 'checkbox' : false"
      :aria-checked="indeterminate ? 'mixed' : false"
    >
      <span class="el-checkbox__inner"></span>
      <input
        v-if="trueLabel || falseLabel"
        class="el-checkbox__original"
        type="checkbox"
        :aria-hidden="indeterminate ? 'true' : 'false'"
        :name="name"
        :disabled="isDisabled"
        :true-value="trueLabel"
        :false-value="falseLabel"
        v-model="model"
        @change="handleChange"
        @focus="focus = true"
        @blur="focus = false">
      <input
        v-else
        class="el-checkbox__original"
        type="checkbox"
        :aria-hidden="indeterminate ? 'true' : 'false'"
        :disabled="isDisabled"
        :value="label"
        :name="name"
        v-model="model"
        @change="handleChange"
        @focus="focus = true"
        @blur="focus = false">
    </span>
    <span class="el-checkbox__label" v-if="$slots.default || label">
      <slot></slot>
      <template v-if="!$slots.default">{{label}}</template>
    </span>
  </label>
</template>
<script>
  import Emitter from 'element-ui/src/mixins/emitter';

  export default {
    name: 'ElCheckbox',

    mixins: [Emitter],

    inject: {
      elForm: {
        default: ''
      },
      elFormItem: {
        default: ''
      }
    },

    componentName: 'ElCheckbox',

    data() {
      return {
        selfModel: false,
        focus: false,
        isLimitExceeded: false
      };
    },

    computed: {
      model: {
        get() {
          return this.isGroup
            ? this.store : this.value !== undefined
              ? this.value : this.selfModel;
        },

        set(val) {
          if (this.isGroup) {
            this.isLimitExceeded = false;
            (this._checkboxGroup.min !== undefined &&
              val.length < this._checkboxGroup.min &&
              (this.isLimitExceeded = true));

            (this._checkboxGroup.max !== undefined &&
              val.length > this._checkboxGroup.max &&
              (this.isLimitExceeded = true));

            this.isLimitExceeded === false &&
            this.dispatch('ElCheckboxGroup', 'input', [val]);
          } else {
            this.$emit('input', val);
            this.selfModel = val;
          }
        }
      },

      isChecked() {
        if ({}.toString.call(this.model) === '[object Boolean]') {
          return this.model;
        } else if (Array.isArray(this.model)) {
          return this.model.indexOf(this.label) > -1;
        } else if (this.model !== null && this.model !== undefined) {
          return this.model === this.trueLabel;
        }
      },

      isGroup() {
        let parent = this.$parent;
        while (parent) {
          if (parent.$options.componentName !== 'ElCheckboxGroup') {
            parent = parent.$parent;
          } else {
            this._checkboxGroup = parent;
            return true;
          }
        }
        return false;
      },

      store() {
        return this._checkboxGroup ? this._checkboxGroup.value : this.value;
      },

      /* used to make the isDisabled judgment under max/min props */
      isLimitDisabled() {
        const { max, min } = this._checkboxGroup;
        return !!(max || min) &&
          (this.model.length >= max && !this.isChecked) ||
          (this.model.length <= min && this.isChecked);
      },

      isDisabled() {
        return this.isGroup
          ? this._checkboxGroup.disabled || this.disabled || (this.elForm || {}).disabled || this.isLimitDisabled
          : this.disabled || (this.elForm || {}).disabled;
      },

      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },

      checkboxSize() {
        const temCheckboxSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
        return this.isGroup
          ? this._checkboxGroup.checkboxGroupSize || temCheckboxSize
          : temCheckboxSize;
      }
    },

    props: {
      value: {},
      label: {},
      indeterminate: Boolean,
      disabled: Boolean,
      checked: Boolean,
      name: String,
      trueLabel: [String, Number],
      falseLabel: [String, Number],
      id: String, /* 当indeterminate为真时,为controls提供相关连的checkbox的id,表明元素间的控制关系*/
      controls: String, /* 当indeterminate为真时,为controls提供相关连的checkbox的id,表明元素间的控制关系*/
      border: Boolean,
      size: String
    },

    methods: {
      addToStore() {
        if (
          Array.isArray(this.model) &&
          this.model.indexOf(this.label) === -1
        ) {
          this.model.push(this.label);
        } else {
          this.model = this.trueLabel || true;
        }
      },
      handleChange(ev) {
        if (this.isLimitExceeded) return;
        let value;
        if (ev.target.checked) {
          value = this.trueLabel === undefined ? true : this.trueLabel;
        } else {
          value = this.falseLabel === undefined ? false : this.falseLabel;
        }
        this.$emit('change', value, ev);
        this.$nextTick(() => {
          if (this.isGroup) {
            this.dispatch('ElCheckboxGroup', 'change', [this._checkboxGroup.value]);
          }
        });
      }
    },

    created() {
      this.checked && this.addToStore();
    },
    mounted() { // 为indeterminate元素 添加aria-controls 属性
      if (this.indeterminate) {
        this.$el.setAttribute('aria-controls', this.controls);
      }
    },

    watch: {
      value(value) {
        this.dispatch('ElFormItem', 'el.form.change', value);
      }
    }
  };
</script>

他和button组件的实现方式很像
checkbox-group组件

<script>
  import Emitter from 'element-ui/src/mixins/emitter';

  export default {
    name: 'ElCheckboxGroup',

    componentName: 'ElCheckboxGroup',

    mixins: [Emitter],

    inject: {
      elFormItem: {
        default: ''
      }
    },

    props: {
      value: {},
      disabled: Boolean,
      min: Number,
      max: Number,
      size: String,
      fill: String,
      textColor: String
    },

    computed: {
      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },
      checkboxGroupSize() {
        return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
      }
    },

    watch: {
      value(value) {
        this.dispatch('ElFormItem', 'el.form.change', [value]);
      }
    }
  };
</script>

<template>
  <div class="el-checkbox-group" role="group" aria-label="checkbox-group">
    <slot></slot>
  </div>
</template>

checkbox-button组件

<template>
  <label
    class="el-checkbox-button"
      :class="[
        size ? 'el-checkbox-button--' + size : '',
        { 'is-disabled': isDisabled },
        { 'is-checked': isChecked },
        { 'is-focus': focus },
      ]"
    role="checkbox"
    :aria-checked="isChecked"
    :aria-disabled="isDisabled"
    >
    <input
      v-if="trueLabel || falseLabel"
      class="el-checkbox-button__original"
      type="checkbox"
      :name="name"
      :disabled="isDisabled"
      :true-value="trueLabel"
      :false-value="falseLabel"
      v-model="model"
      @change="handleChange"
      @focus="focus = true"
      @blur="focus = false">
    <input
      v-else
      class="el-checkbox-button__original"
      type="checkbox"
      :name="name"
      :disabled="isDisabled"
      :value="label"
      v-model="model"
      @change="handleChange"
      @focus="focus = true"
      @blur="focus = false">

    <span class="el-checkbox-button__inner"
      v-if="$slots.default || label"
      :style="isChecked ? activeStyle : null">
      <slot>{{label}}</slot>
    </span>

  </label>
</template>
<script>
  import Emitter from 'element-ui/src/mixins/emitter';

  export default {
    name: 'ElCheckboxButton',

    mixins: [Emitter],

    inject: {
      elForm: {
        default: ''
      },
      elFormItem: {
        default: ''
      }
    },

    data() {
      return {
        selfModel: false,
        focus: false,
        isLimitExceeded: false
      };
    },

    props: {
      value: {},
      label: {},
      disabled: Boolean,
      checked: Boolean,
      name: String,
      trueLabel: [String, Number],
      falseLabel: [String, Number]
    },
    computed: {
      model: {
        get() {
          return this._checkboxGroup
            ? this.store : this.value !== undefined
              ? this.value : this.selfModel;
        },

        set(val) {
          if (this._checkboxGroup) {
            this.isLimitExceeded = false;
            (this._checkboxGroup.min !== undefined &&
              val.length < this._checkboxGroup.min &&
              (this.isLimitExceeded = true));

            (this._checkboxGroup.max !== undefined &&
              val.length > this._checkboxGroup.max &&
              (this.isLimitExceeded = true));

            this.isLimitExceeded === false &&
            this.dispatch('ElCheckboxGroup', 'input', [val]);
          } else if (this.value !== undefined) {
            this.$emit('input', val);
          } else {
            this.selfModel = val;
          }
        }
      },

      isChecked() {
        if ({}.toString.call(this.model) === '[object Boolean]') {
          return this.model;
        } else if (Array.isArray(this.model)) {
          return this.model.indexOf(this.label) > -1;
        } else if (this.model !== null && this.model !== undefined) {
          return this.model === this.trueLabel;
        }
      },

      _checkboxGroup() {
        let parent = this.$parent;
        while (parent) {
          if (parent.$options.componentName !== 'ElCheckboxGroup') {
            parent = parent.$parent;
          } else {
            return parent;
          }
        }
        return false;
      },

      store() {
        return this._checkboxGroup ? this._checkboxGroup.value : this.value;
      },

      activeStyle() {
        return {
          backgroundColor: this._checkboxGroup.fill || '',
          borderColor: this._checkboxGroup.fill || '',
          color: this._checkboxGroup.textColor || '',
          'box-shadow': '-1px 0 0 0 ' + this._checkboxGroup.fill

        };
      },

      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },

      size() {
        return this._checkboxGroup.checkboxGroupSize || this._elFormItemSize || (this.$ELEMENT || {}).size;
      },

      /* used to make the isDisabled judgment under max/min props */
      isLimitDisabled() {
        const { max, min } = this._checkboxGroup;
        return !!(max || min) &&
          (this.model.length >= max && !this.isChecked) ||
          (this.model.length <= min && this.isChecked);
      },

      isDisabled() {
        return this._checkboxGroup
          ? this._checkboxGroup.disabled || this.disabled || (this.elForm || {}).disabled || this.isLimitDisabled
          : this.disabled || (this.elForm || {}).disabled;
      }
    },
    methods: {
      addToStore() {
        if (
          Array.isArray(this.model) &&
          this.model.indexOf(this.label) === -1
        ) {
          this.model.push(this.label);
        } else {
          this.model = this.trueLabel || true;
        }
      },
      handleChange(ev) {
        if (this.isLimitExceeded) return;
        let value;
        if (ev.target.checked) {
          value = this.trueLabel === undefined ? true : this.trueLabel;
        } else {
          value = this.falseLabel === undefined ? false : this.falseLabel;
        }
        this.$emit('change', value, ev);
        this.$nextTick(() => {
          if (this._checkboxGroup) {
            this.dispatch('ElCheckboxGroup', 'change', [this._checkboxGroup.value]);
          }
        });
      }
    },

    created() {
      this.checked && this.addToStore();
    }
  };
</script>

(二)、vue3重写checkbox

checkbox组件

<template>
    <label 
        class="el-checkbox"
        :class="[
            border && state.checkboxSize ? 'el-checkbox--' + state.checkboxSize : '',
            {'is-disabled': state.isDisabled},
            {'is-bordered': border},
            {'is-checked' : state.isChecked}
            ]"
            :id="id"
        >
            <span class="el-checkbox__input"
                :class="{
                    'is-disabled': state.isDisabled,
                    'is-checked': state.isChecked,
                    'is-indeterminate': indeterminate,
                    'is-focus': state.focus
                }"
                :tabindex="state.indeterminate ? 0 : false"
                :role="state.indeterminate ? 'checkbox' : false"
                :aria-checked="state.indeterminate ? 'mixed' : false"
            >
                <span class="el-checkbox__inner"></span>
                <input 
                    v-if="trueLabel || falseLabel"
                    class="el-checkbox__original"
                    type="checkbox"
                    :aria-hidden="state.indeterminate ? 'true' : 'false'"
                    :name="name"
                    :disabled="state.isDisabled"
                    :true-value="trueLabel"
                    :false-value="falseLabel"
                    v-model="state.model"
                    @change.stop="handleChange"
                    @focus="state.focus = true"
                    @blur="state.focus = false">
                <input
                    v-else
                    class="el-checkbox__original"
                    type="checkbox"
                    :aria-hidden="state.indeterminate ? 'true' : 'false'"
                    :disabled="state.isDisabled"
                    :value="label"
                    :name="name"
                    v-model="state.model"
                    @change.stop="handleChange"
                    @focus="state.focus = true"
                    @blur="state.focus = false">
            </span>
            <span class="el-checkbox__label" v-if="context.slots.default || label">
                <slot></slot>
                <template v-if="!context.slots.default">{{label}}</template>
            </span>
        </label>
</template>
<script>
import '../../../plaginStyle/src/checkbox.scss';
import {ref, reactive, computed, onMounted, getCurrentInstance, watch} from 'vue';
import Emitter from '../../../plaginSrc/mixins/emitter.js';
// import tree from '../../../../../element-dev/packages/table/src/store/tree.js';

export default {
    name: 'ElCheckbox',
    componentName: 'ElCheckbox',

    props: {
        modelValue: {},
        label: {},
        indeterminate: Boolean,
        disabled: Boolean,
        checked: Boolean,
        name: String,
        trueLabel: [String, Number],
        falseLabel: [String, Number],
        id: String,
        controls: String,
        border: Boolean,
        size: String
    },
    setup(props, context) {
        const currentInstance = getCurrentInstance(); // 当前组件对象
        const {dispatch, broadcast} = Emitter(currentInstance); // 派发方法
        const state = reactive({
            selfModel: false,
            focus: false,
            isLimitExceeded: false,
            model: computed({ // v-model绑定值
                get() {
                    return state.isGroup
                        ? state.store : props.modelValue !== undefined
                            ? props.modelValue : state.selfModel;
                },  
                set(val) {
                    if (state.isGroup) {
                        state.isLimitExceeded = false;
                        (state.isGroup.ctx.min !== undefined && 
                        val.length < state.isGroup.ctx.min && 
                        (state.isLimitExceeded = true));

                        (state.isGroup.ctx.max !== undefined &&
                        val.length > state.isGroup.ctx.max && 
                        (state.isLimitExceeded = true));

                        state.isLimitExceeded === false &&
                        dispatch('ElCheckboxGroup', 'update:modelValue', [val]);
                    } else {
                        context.emit('update:modelValue', val);
                        state.selfModel = val;
                    }
                }
            }),
            // 判断是否选中
            isChecked: computed(() => {
                if ({}.toString.call(state.model) === '[object Boolean]') {
                    return state.model;
                } else if (Array.isArray(state.model)) {
                    return state.model.indexOf(props.label) > -1;
                } else if (state.model !== null && state.model !== undefined) {
                    return state.model === props.trueLabel;
                }
             }),
            
            isGroup: computed(() => {
                let parent = currentInstance.parent;
                while (parent) {
                    if (parent.type.componentName !== 'ElCheckboxGroup') {
                        parent = parent.parent;
                    } else {
                        // state._checkboxGroup = parent;
                        return parent;
                    }
                }
                return false;
            }),

            store: computed(() => {
                return state.isGroup ? state.isGroup.ctx.modelValue : props.modelValue;
            }),

            isLimitDisabled: computed(() => {
                const { max, min } = state.isGroup;
                return !!(max || min) &&
                    (state.model.length >= max && !state.isChecked) ||
                    (state.model.length <= min && state.isChecked);
            }),

            isDisabled: computed(() => {
                return state.isGroup
                    ? state.isGroup.ctx.disabled || props.disabled || state.isLimitDisabled
                    : props.disabled;
            }),

            _elFormItemSize: computed(() => {
                return (state.elFormItem || {}).elFormItemSize;
            }),

            checkboxSize: computed(() => {
                const temCheckboxSize = props.size || state._elFormItemSize
                return state.isGroup
                    ? state.isGroup.ctx.checkboxGroupSize || temCheckboxSize
                    : temCheckboxSize;
            }),
        });

        function addToStore() {
            if (
                Array.isArray(state.model) && 
                state.model.indexOf(props.label) === -1
            ) {
                state.model.push(props.label);
            } else {
                state.model = props.trueLabel || true;
            }
        }
        function handleChange(ev) {
            if (state.isLimitExceeded) return;
            let value;
            if (ev.target.checked) {
                value = props.trueLabel === undefined ? true : props.trueLabel;
            } else {
                value = props.falseLabel === undefined ? false : props.falseLabel;
            }
            context.emit('change', value, ev);
            if (state.isGroup) {
                dispatch('ElCheckboxGroup', 'change', [state.isGroup.ctx.modelValue]);
            }
        }

        // 首次执行相当于created
        props.checked && addToStore();

        // mounted
        onMounted(() => {
            if (state.indeterminate) {
                currentInstance.vnode.setAttribute('aria-controls', this.controls); 
            }
        });

        watch(() => props.modelValue, (modelValue, preModelValue) => {
            dispatch('ElFormItem', 'el.form.change', modelValue);
        })
        return {
            state,
            context,
            addToStore,
            handleChange
        }
    }
}
</script>

checkbox-group组件

<script>
import '../../../plaginStyle/src/checkbox-group.scss';
import Emitter from '../../../plaginSrc/mixins/emitter.js';
import {ref, reactive, computed, onMounted, watch, inject, getCurrentInstance} from 'vue';
export default {
    name: 'ElCheckboxGroup',

    componentName: 'ElCheckboxGroup',

    props: {
        modelValue: {},
        disabled: Boolean,
        min: Number,
        max: Number,
        size: String,
        fill: String,
        textColor: String
    },

    setup(props, context) {
        const elFormItem = inject(elFormItem, ref(''));
        const currentInstance = getCurrentInstance();
        const {dispatch, broadcast} = Emitter(currentInstance);
        const state = reactive({
            _elFormItemSize: computed(() => {
                return (elFormItem || {}).elFormItemSize;
            }),
            checkboxGroupSize: computed(() => {
                return props.size || state._elFormItemSize
            })
        })
        watch(() => props.modelValue, (modelValue, perModelValue) => {
            dispatch('ElFormItem', 'el.form.change', [modelValue]);
        })
    }
}
</script>

<template>
    <div class="el-checkbox-group" role="group" aria-label="checkbox-group">
        <slot></slot>
    </div>
</template>

checkbox-button组件

<template>
    <label
        class="el-checkbox-button"
        :class="[
            state.size ? 'el-checkbox-button--' + state.size : '',
            { 'is-disabled': state.isDisabled },
            { 'is-checked': state.isChecked },
            { 'is-focus': state.focus },
        ]"
        role="checkbox"
        :aria-checked="state.isChecked"
        :aria-disabled="state.isDisabled"
        >
        <input
            v-if="trueLabel || falseLabel"
            class="el-checkbox-button__original"
            type="checkbox"
            :name="name"
            :disabled="state.isDisabled"
            :true-value="trueLabel"
            :false-value="falseLabel"
            v-model="state.model"
            @change.stop="handleChange"
            @focus="state.focus = true"
            @blur="state.focus = false">
        <input
            v-else
            class="el-checkbox-button__original"
            type="checkbox"
            :name="name"
            :disabled="state.isDisabled"
            :value="label"
            v-model="state.model"
            @change.stop="handleChange"
            @focus="state.focus = true"
            @blur="state.focus = false">
        
        <span class="el-checkbox-button__inner"
            v-if="context.slots.default || label"
            :style="state.isChecked ? state.activeStyle : null">
            <slot>{{label}}</slot>
        </span>
    </label>
</template>
<script>
import '../../../plaginStyle/src/checkbox-button.scss';
import {ref, reactive, inject, computed, onMounted, watch, getCurrentInstance} from 'vue';
import Vue from 'vue';
import Emitter from '../../../plaginSrc/mixins/emitter.js';
import { props } from '../../../../../vue源码/vue-dev/test/weex/cases/recycle-list/components/counter.vue';

export default {
    name: 'ElCheckboxButton',

    props: {
        modelValue: {},
        label: {},
        disabled: Boolean,
        checked: Boolean,
        name: String,
        trueLabel: [String, Number],
        falseLabel: [String, Number]
    },

    setup(props, context) {
        
        const currentInstance = getCurrentInstance();
        const {dispatch, broadcast} = Emitter(currentInstance);
        const elForm = inject(elForm, ref(''));
        const elFormItem = inject(elFormItem, ref(''));
        const state = reactive({
            selfModel: false,
            focus: false,
            isLimitExceeded: false,
            model: computed({
                get() {
                    return state._checkboxGroup
                    ? state.store : props.modelValue !== undefined
                    ? props.modelValue : state.selfModel;
                },

                set(val) {
                    if (state._checkboxGroup) {
                        state.isLimitExceeded = false;
                        (state._checkboxGroup.ctx.min !== undefined &&
                        val.length < state._checkboxGroup.ctx.min && 
                        (state.isLimitExceeded = true));

                        (state._checkboxGroup.ctx.max !== undefined &&
                            val.length > state._checkboxGroup.ctx.max &&
                            (state.isLimitExceeded = true));

                        state.isLimitExceeded === false &&
                        dispatch('ElCheckboxGroup', 'update:modelValue', [val]);
                    } else if (state.modelValue !== undefined) {
                        context.emit('update:modelValue', val);
                    } else {
                        state.selfModel = val;
                    }
                }
            }),

            isChecked: computed(() => {
                if ({}.toString.call(state.model) === '[object Boolean]') {
                    return state.model;
                } else if (Array.isArray(state.model)) {
                    return state.model.indexOf(props.label) > -1;
                } else if (state.model !== null && state.model !== undefined) {
                    return state.model = props.trueLabel;
                }
            }),

            _checkboxGroup: computed(() => {
                let parent = currentInstance.parent;
                while (parent) {
                    if (parent.type.componentName !== 'ElCheckboxGroup') {
                        parent = parent.parent;
                    } else {
                        return parent;
                    }
                }
                return false;
            }),

            store: computed(() => {
                return state._checkboxGroup ? state._checkboxGroup.ctx.modelValue : props.modelValue;
            }),

            activeStyle: computed(() => {
                return {
                    backgroundColor: state._checkboxGroup.ctx.fill || '',
                    borderColor:  state._checkboxGroup.ctx.fill || '',
                    color: state._checkboxGroup.ctx.textColor || '',
                    'box-shadow': '-1px 0 0 0' + state._checkboxGroup.ctx.fill
                };
            }),

            _elFormItemSize: computed(() => {
                return (elFormItem || {}).elFormItemSize;
            }),

            size: computed(() => {
                return state._checkboxGroup.ctx.checkboxGroupSize || state._elFormItemSize;
            }),

            isLimitDisabled: computed(() => {
                const { max, min } = state._checkboxGroup;
                return !!(max || min) && 
                (state.model.length >= max && !state.isChecked) || 
                (state.model.length <= min && state.isChecked);
            }),

            isDisabled: computed(() => {
                return state._checkboxGroup
                ? state._checkboxGroup.ctx.disabled || props.disabled || (elForm || {}).disabled || state.isLimitDisabled
                : props.disabled || (elForm || {}).disabled;
            }),
        });

        function addToStore() {
            if (
                Array.isArray(state.model) && 
                state.model.indexOf(props.label) === -1
            ) {
                state.model.push(props.label);
            } else {
                state.model = props.trueLabel || true;
            }
        }

        function handleChange(ev) {
            if (state.isLimitExceeded) return;
            let value;
            if (ev.target.checked) {
                value = props.trueLabel === undefined ? true : props.trueLabel;
            } else {
                value = props.falseLabel === undefined ? false : props.falseLabel;
            }
            currentInstance.emit('change', value, ev);
            if (state._checkboxGroup) {
                dispatch('ElCheckboxGroup', 'change', [state._checkboxGroup.ctx.modelValue]);
            }
        }
        props.checked && addToStore();
        return {
            state,
            context,
            currentInstance,
            handleChange
        }
    }
}
</script>

七、element ui 之 input

(一)、源码

在这里插入图片描述
源码

<template>
  <div :class="[
    type === 'textarea' ? 'el-textarea' : 'el-input',
    inputSize ? 'el-input--' + inputSize : '',
    {
      'is-disabled': inputDisabled,
      'is-exceed': inputExceed,
      'el-input-group': $slots.prepend || $slots.append,
      'el-input-group--append': $slots.append,
      'el-input-group--prepend': $slots.prepend,
      'el-input--prefix': $slots.prefix || prefixIcon,
      'el-input--suffix': $slots.suffix || suffixIcon || clearable || showPassword
    }
    ]"
    @mouseenter="hovering = true"
    @mouseleave="hovering = false"
  >
    <template v-if="type !== 'textarea'">
      <!-- 前置元素 -->
      <div class="el-input-group__prepend" v-if="$slots.prepend">
        <slot name="prepend"></slot>
      </div>
      <input
        :tabindex="tabindex"
        v-if="type !== 'textarea'"
        class="el-input__inner"
        v-bind="$attrs"
        :type="showPassword ? (passwordVisible ? 'text': 'password') : type"
        :disabled="inputDisabled"
        :readonly="readonly"
        :autocomplete="autoComplete || autocomplete"
        ref="input"
        @compositionstart="handleCompositionStart"
        @compositionupdate="handleCompositionUpdate"
        @compositionend="handleCompositionEnd"
        @input="handleInput"
        @focus="handleFocus"
        @blur="handleBlur"
        @change="handleChange"
        :aria-label="label"
      >
      <!-- 前置内容 -->
      <span class="el-input__prefix" v-if="$slots.prefix || prefixIcon">
        <slot name="prefix"></slot>
        <i class="el-input__icon"
           v-if="prefixIcon"
           :class="prefixIcon">
        </i>
      </span>
      <!-- 后置内容 -->
      <span
        class="el-input__suffix"
        v-if="getSuffixVisible()">
        <span class="el-input__suffix-inner">
          <template v-if="!showClear || !showPwdVisible || !isWordLimitVisible">
            <slot name="suffix"></slot>
            <i class="el-input__icon"
              v-if="suffixIcon"
              :class="suffixIcon">
            </i>
          </template>
          <i v-if="showClear"
            class="el-input__icon el-icon-circle-close el-input__clear"
            @mousedown.prevent
            @click="clear"
          ></i>
          <i v-if="showPwdVisible"
            class="el-input__icon el-icon-view el-input__clear"
            @click="handlePasswordVisible"
          ></i>
          <span v-if="isWordLimitVisible" class="el-input__count">
            <span class="el-input__count-inner">
              {{ textLength }}/{{ upperLimit }}
            </span>
          </span>
        </span>
        <i class="el-input__icon"
          v-if="validateState"
          :class="['el-input__validateIcon', validateIcon]">
        </i>
      </span>
      <!-- 后置元素 -->
      <div class="el-input-group__append" v-if="$slots.append">
        <slot name="append"></slot>
      </div>
    </template>
    <textarea
      v-else
      :tabindex="tabindex"
      class="el-textarea__inner"
      @compositionstart="handleCompositionStart"
      @compositionupdate="handleCompositionUpdate"
      @compositionend="handleCompositionEnd"
      @input="handleInput"
      ref="textarea"
      v-bind="$attrs"
      :disabled="inputDisabled"
      :readonly="readonly"
      :autocomplete="autoComplete || autocomplete"
      :style="textareaStyle"
      @focus="handleFocus"
      @blur="handleBlur"
      @change.stop="handleChange"
      :aria-label="label"
    >
    </textarea>
    <span v-if="isWordLimitVisible && type === 'textarea'" class="el-input__count">{{ textLength }}/{{ upperLimit }}</span>
  </div>
</template>
<script>
  import emitter from 'element-ui/src/mixins/emitter';
  import Migrating from 'element-ui/src/mixins/migrating';
  import calcTextareaHeight from './calcTextareaHeight';
  import merge from 'element-ui/src/utils/merge';
  import {isKorean} from 'element-ui/src/utils/shared';

  export default {
    name: 'ElInput',

    componentName: 'ElInput',

    mixins: [emitter, Migrating],

    inheritAttrs: false,

    inject: {
      elForm: {
        default: ''
      },
      elFormItem: {
        default: ''
      }
    },

    data() {
      return {
        textareaCalcStyle: {},
        hovering: false,
        focused: false,
        isComposing: false,
        passwordVisible: false
      };
    },

    props: {
      value: [String, Number],
      size: String,
      resize: String,
      form: String,
      disabled: Boolean,
      readonly: Boolean,
      type: {
        type: String,
        default: 'text'
      },
      autosize: {
        type: [Boolean, Object],
        default: false
      },
      autocomplete: {
        type: String,
        default: 'off'
      },
      /** @Deprecated in next major version */
      autoComplete: {
        type: String,
        validator(val) {
          process.env.NODE_ENV !== 'production' &&
            console.warn('[Element Warn][Input]\'auto-complete\' property will be deprecated in next major version. please use \'autocomplete\' instead.');
          return true;
        }
      },
      validateEvent: {
        type: Boolean,
        default: true
      },
      suffixIcon: String,
      prefixIcon: String,
      label: String,
      clearable: {
        type: Boolean,
        default: false
      },
      showPassword: {
        type: Boolean,
        default: false
      },
      showWordLimit: {
        type: Boolean,
        default: false
      },
      tabindex: String
    },

    computed: {
      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },
      validateState() {
        return this.elFormItem ? this.elFormItem.validateState : '';
      },
      needStatusIcon() {
        return this.elForm ? this.elForm.statusIcon : false;
      },
      validateIcon() {
        return {
          validating: 'el-icon-loading',
          success: 'el-icon-circle-check',
          error: 'el-icon-circle-close'
        }[this.validateState];
      },
      textareaStyle() {
        return merge({}, this.textareaCalcStyle, { resize: this.resize });
      },
      inputSize() {
        return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
      },
      inputDisabled() {
        return this.disabled || (this.elForm || {}).disabled;
      },
      nativeInputValue() {
        return this.value === null || this.value === undefined ? '' : String(this.value);
      },
      showClear() {
        return this.clearable &&
          !this.inputDisabled &&
          !this.readonly &&
          this.nativeInputValue &&
          (this.focused || this.hovering);
      },
      showPwdVisible() {
        return this.showPassword &&
          !this.inputDisabled &&
          !this.readonly &&
          (!!this.nativeInputValue || this.focused);
      },
      isWordLimitVisible() {
        return this.showWordLimit &&
          this.$attrs.maxlength &&
          (this.type === 'text' || this.type === 'textarea') &&
          !this.inputDisabled &&
          !this.readonly &&
          !this.showPassword;
      },
      upperLimit() {
        return this.$attrs.maxlength;
      },
      textLength() {
        if (typeof this.value === 'number') {
          return String(this.value).length;
        }

        return (this.value || '').length;
      },
      inputExceed() {
        // show exceed style if length of initial value greater then maxlength
        return this.isWordLimitVisible &&
          (this.textLength > this.upperLimit);
      }
    },

    watch: {
      value(val) {
        this.$nextTick(this.resizeTextarea);
        if (this.validateEvent) {
          this.dispatch('ElFormItem', 'el.form.change', [val]);
        }
      },
      // native input value is set explicitly
      // do not use v-model / :value in template
      // see: https://github.com/ElemeFE/element/issues/14521
      nativeInputValue() {
        this.setNativeInputValue();
      },
      // when change between <input> and <textarea>,
      // update DOM dependent value and styles
      // https://github.com/ElemeFE/element/issues/14857
      type() {
        this.$nextTick(() => {
          this.setNativeInputValue();
          this.resizeTextarea();
          this.updateIconOffset();
        });
      }
    },

    methods: {
      focus() {
        this.getInput().focus();
      },
      blur() {
        this.getInput().blur();
      },
      getMigratingConfig() {
        return {
          props: {
            'icon': 'icon is removed, use suffix-icon / prefix-icon instead.',
            'on-icon-click': 'on-icon-click is removed.'
          },
          events: {
            'click': 'click is removed.'
          }
        };
      },
      handleBlur(event) {
        this.focused = false;
        this.$emit('blur', event);
        if (this.validateEvent) {
          this.dispatch('ElFormItem', 'el.form.blur', [this.value]);
        }
      },
      select() {
        this.getInput().select();
      },
      resizeTextarea() {
        if (this.$isServer) return;
        const { autosize, type } = this;
        if (type !== 'textarea') return;
        if (!autosize) {
          this.textareaCalcStyle = {
            minHeight: calcTextareaHeight(this.$refs.textarea).minHeight
          };
          return;
        }
        const minRows = autosize.minRows;
        const maxRows = autosize.maxRows;

        this.textareaCalcStyle = calcTextareaHeight(this.$refs.textarea, minRows, maxRows);
      },
      setNativeInputValue() {
        const input = this.getInput();
        console.log(input, 'input');
        if (!input) return;
        if (input.value === this.nativeInputValue) return;
        input.value = this.nativeInputValue;
      },
      handleFocus(event) {
        this.focused = true;
        this.$emit('focus', event);
      },
      handleCompositionStart() {
        this.isComposing = true;
      },
      handleCompositionUpdate(event) {
        const text = event.target.value;
        const lastCharacter = text[text.length - 1] || '';
        this.isComposing = !isKorean(lastCharacter);
      },
      handleCompositionEnd(event) {
        if (this.isComposing) {
          this.isComposing = false;
          this.handleInput(event);
        }
      },
      handleInput(event) {
        // should not emit input during composition
        // see: https://github.com/ElemeFE/element/issues/10516
        if (this.isComposing) return;

        // hack for https://github.com/ElemeFE/element/issues/8548
        // should remove the following line when we don't support IE
        if (event.target.value === this.nativeInputValue) return;

        this.$emit('input', event.target.value);

        // ensure native input value is controlled
        // see: https://github.com/ElemeFE/element/issues/12850
        this.$nextTick(this.setNativeInputValue);
      },
      handleChange(event) {
        this.$emit('change', event.target.value);
      },
      calcIconOffset(place) {
        let elList = [].slice.call(this.$el.querySelectorAll(`.el-input__${place}`) || []);
        if (!elList.length) return;
        let el = null;
        for (let i = 0; i < elList.length; i++) {
          if (elList[i].parentNode === this.$el) {
            el = elList[i];
            break;
          }
        }
        if (!el) return;
        const pendantMap = {
          suffix: 'append',
          prefix: 'prepend'
        };

        const pendant = pendantMap[place];
        if (this.$slots[pendant]) {
          el.style.transform = `translateX(${place === 'suffix' ? '-' : ''}${this.$el.querySelector(`.el-input-group__${pendant}`).offsetWidth}px)`;
        } else {
          el.removeAttribute('style');
        }
      },
      updateIconOffset() {
        this.calcIconOffset('prefix');
        this.calcIconOffset('suffix');
      },
      clear() {
        this.$emit('input', '');
        this.$emit('change', '');
        this.$emit('clear');
      },
      handlePasswordVisible() {
        this.passwordVisible = !this.passwordVisible;
        this.focus();
      },
      getInput() {
        return this.$refs.input || this.$refs.textarea;
      },
      getSuffixVisible() {
        return this.$slots.suffix ||
          this.suffixIcon ||
          this.showClear ||
          this.showPassword ||
          this.isWordLimitVisible ||
          (this.validateState && this.needStatusIcon);
      }
    },

    created() {
      this.$on('inputSelect', this.select);
    },

    mounted() {
      this.setNativeInputValue();
      this.resizeTextarea();
      this.updateIconOffset();
    },

    updated() {
      this.$nextTick(this.updateIconOffset);
    }
  };
</script>

(二)、vue3重写input

<template>
    <div :class="[
        type === 'textarea' ? 'el-textarea' : 'el-input',
        state.inputSize ? 'el-input--' + state.inputSize : '',
        {
            'is-disabled': state.inputDisabled,
            'is-exceed': state.inputExceed,
            'el-input-group': context.slots.prepend || context.slots.append,
            'el-input-group--append': context.slots.append,
            'el-input-group--prepend': context.slots.prepend,
            'el-input--prefix': context.slots.prefix || prefixIcon,
            'el-input--suffix': context.slots.suffix || suffixIcon || clearable || showPassword
        }
        ]"
        @mouseenter="state.hovering = true"
        @mouseleave="state.hovering = false"
    >
        <template v-if="type !== 'textarea'"> 
            
            <!-- 前置元素 -->
            <div class="el-input-group__prepend" v-if="context.slots.prepend">
                <slot name="prepend"></slot>
            </div>
            <input 
                :tabindex="tabindex"
                v-if="type !== 'textarea'"
                class="el-input__inner"
                v-bind="context.attrs"
                :type="showPassword ? (state.passwordVisible ? 'text' : 'password') : type"
                :disabled="state.inputDisabled"
                :readonly="readonly"
                :autocomplete="autoComplete || autocomplete"
                ref="input"
                @compositionstart="handleCompositionStart"
                @compositionupdate="handleCompositionUpdate"
                @compositionend="handleCompositionEnd"
                @input.stop="handleInput"
                @focus="handleFocus"
                @blur="handleBlur"
                @change.stop="handleChange"
                :aria-label="label"
            >
            <!-- 前置内容 -->
            <span class="el-input__prefix" v-if="context.slots.prefix || prefixIcon">
                <slot name="prefix"></slot>
                <i class="el-input__icon"
                    v-if="prefixIcon"
                    :class="prefixIcon">
                </i>
            </span>
            <!-- 后置内容 -->
            <span
                class="el-input__suffix"
                v-if="getSuffixVisible()">
                <span class="el-input__suffix-inner">
                    <template v-if="!state.showClear || !state.showPwdVisible || !state.isWordLimitVisible">
                        <slot name="suffix"></slot>
                        <i class="el-input__icon"
                            v-if="suffixIcon"
                            :class="suffixIcon"
                        ></i>
                    </template>
                    <i v-if="state.showClear"
                        class="el-input__icon el-icon-circle-close el-input__clear"
                        @mousedown.prevent
                        @click="clear"
                    ></i>
                    <i v-if="state.showPwdVisible"
                        class="el-input__icon el-icon-view el-input__clear"
                        @click="handlePasswordVisible"
                    ></i>
                    <span v-if="state.isWordLimitVisible" class="el-input__count">
                        <span class="el-input__count-inner">
                            {{ state.textLength }} / {{ state.upperLimit }}
                        </span>
                    </span>
                </span>
                <i class="el-input__icon"
                    v-if="state.validateState"
                    :class="['el-input__validateIcon', state.validateIcon]">
                </i>
            </span>
            <!-- 后置元素 -->
            <div class="el-input-group__append" v-if="context.slots.append">
                <slot name="append"></slot>
            </div>
        </template>
        <textarea
            v-else
            :tabindex="tabindex"
            class="el-textarea__inner"
            @compositionstart="handleCompositionStart"
            @compositionupdate="handleCompositionUpdate"
            @compositionend="handleCompositionEnd"
            @input.stop="handleInput"
            ref="textarea"
            v-bind="context.attrs"
            :disabled="state.inputDisabled"
            :readonly="readonly"
            :autocomplete="autoComplete || autocomplete"
            :style="state.textareaStyle"
            @focus="handleFocus"
            @blur="handleBlur"
            @change.stop="handleChange"
            :aria-label="label"
        >
        </textarea>
        <span v-if="state.isWordLimitVisible && type === 'textarea'" class="el-input__count">{{ state.textLength }}/{{ state.upperLimit }}</span>
    </div>
</template>
<script>
import '../../../plaginStyle/src/input.scss';
import emitter from '../../../plaginSrc/mixins/emitter.js';
import Migrating from '../../../plaginSrc/mixins/migrating';
import calcTextareaHeight from './calcTextareaHeight';
import merge from '../../../plaginUtils/utils/merge.js';
import {isKorean} from '../../../plaginUtils/utils/shared';
import {ref, reactive, computed, onMounted, watch, inject, getCurrentInstance, onUpdated} from 'vue';
export default {
    name: 'ElInput',

    componentName: 'ElInput',

    inheritAttrs: false,

    props: {
        modelValue: [String, Number],
        size: String,
        resize: String,
        form: String,
        disabled: Boolean,
        readonly: Boolean,
        type: {
            type: String,
            default: 'text'
        },
        autosize: {
            type: [Boolean, Object],
            default: false
        },
        autocomplete: {
            type: String,
            default: 'off'
        },
        autoComplete: {
            type: String,
            validator(val) {
                process.env.NODE_ENV !== 'production' &&
                    console.warn('[Element Warn][Input]\'auto-complete\'property will be deprecated in next major version. please use \'autocomplete\' instead.');
                return true;
            }
        },
        validateEvent: {
            type: Boolean,
            default: true
        },
        suffixIcon: String,
        prefixIcon: String,
        label: String,
        clearable: {
            type: Boolean,
            default: false
        },
        showPassword: {
            type: Boolean,
            default: false
        },
        showWordLimit: {
            type: Boolean,
            default: false
        },
        tabindex: String
    },
    setup(props, context) {
        const elForm = inject(elForm, ref(''));
        const elFormItem = inject(elFormItem, ref(''));
        const currentInstance = getCurrentInstance();
        const { dispatch } = emitter(currentInstance);
        const textarea = ref(null);
        const input = ref(null);
        const state = reactive({
            textareaCalcStyle: {},
            hovering: false,
            focused: false,
            isComposing: false,
            passwordVisible: false,
            _elFormItemSize: computed(() => {
                return (elFormItem || {})._elFormItemSize;
            }),
            validateState: computed(() => {
                return elFormItem ? elFormItem.validateState : '';
            }),
            needStatusIcon: computed(() => {
                return elForm ? elForm.statusIcon: false;
            }),
            validateIcon: computed(() => {
                return {
                    validating: 'el-icon-loading',
                    success: 'el-icon-circle-check',
                    error: 'el-icon-circle-close'
                }[state.validateState];
            }),
            textareaStyle: computed(() => {
                return merge({}, state.textareaCalcStyle, { resize:  props.resize});
            }),
            inputSize: computed(() => {
                return props.size || state._elFormItemSize;
            }),
            inputDisabled: computed(() => {
                return props.disabled || (elForm || {}).disabled;
            }),
            nativeInputValue: computed(() => {
                return props.modelValue === null || props.modelValue === undefined ? '' : String(props.modelValue);
            }),
            showClear: computed(() => {
                return props.clearable &&
                    !state.inputDisabled &&
                    !props.readonly &&
                    state.nativeInputValue &&
                    (state.focused || state.hovering);
                    
            }),
            showPwdVisible: computed(() => {
                return props.showPwdVisible && 
                    !state.inputDisabled &&
                    !props.readonly &&
                    (!!state.nativeInputValue || state.focused);
            }),
            isWordLimitVisible: computed(() => {
                return props.showWordLimit &&
                    context.attrs.maxlength &&
                    (props.type === 'text' || props.type === 'textarea') &&
                    !state.inputDisabled &&
                    !props.readonly &&
                    !props.showPassword;
            }),
            upperLimit: computed(() => {
                return context.attrs.maxlength;
            }),
            textLength: computed(() => {
                if (typeof props.modelValue === 'number') {
                    return String(props.modelValue).length;
                }

                return (props.modelValue || '').length;
            }),
            inputExceed: computed(() => {
                return state.isWordLimitVisible &&
                (state.textLength > state.upperLimit);
            })
        })

        watch(() => props.modelValue, (value, preValue) => {
            setTimeout(() => {
                resizeTextarea;
            }, 2000);
            if (props.validateEvent) {
                dispatch('ElFormItem', 'el.form.change', [value]);
            }
        })
        watch(() => state.nativeInputValue, (value, preValue) => {
            setNativeInputValue();
        })
        watch(() => props.type, (value, preValue) => {
            setTimeout(() => {
                setNativeInputValue();
                resizeTextarea();
                updateIconOffset();
            }, 300);
        })
        function focus() {
            getInput().value.focus();
        }
        function blur() {
            getInput().value.blur();
        }
        function getMigratingConfig() {
            return {
                props: {
                    'icon': 'icon is removed, use suffix-icon / prefix-icon instead.',
                    'on-icon-click': 'on-icon-click is removed.'
                },
                events: {
                    'click': 'click is removed.'
                }
            };
        }
        function handleBlur(event) {
            state.focused = false;
            context.emit('blur', event);
            if (props.validateEvent) {
                dispatch('ElFormItem', 'el.form.blur', [props.modelValue]);
            }
        }
        function select() {
            getInput().value.select();
        }
        function resizeTextarea() {
            
            const {autosize, type} = currentInstance.ctx;
            if (type !== 'textarea') return;
            if (!autosize) {
                state.textareaCalcStyle = {
                    minHeight: calcTextareaHeight(textarea.value || currentInstance.refs.textarea).minHeight
                };
                return;
            }
            const minRows = autosize.minRows;
            const maxRows = autosize.maxRows;
            state.textareaCalcStyle = calcTextareaHeight(currentInstance.refs.textarea, minRows, maxRows);
            
        }
        function setNativeInputValue() {
            const input = getInput();
            if (!input) return;
            if (!input.value) {
                    currentInstance.refs.textarea &&
                    !(currentInstance.refs.textarea.value === state.nativeInputValue) && 
                    (currentInstance.refs.textarea.value = state.nativeInputValue);

                    currentInstance.refs.input &&
                    !(currentInstance.refs.input.value === state.nativeInputValue) &&
                    (currentInstance.refs.input.value = state.nativeInputValue);
            } else {
                if (input.value.value === state.nativeInputValue) return;
                input.value.value = state.nativeInputValue;
            }
            
            
        }
        function handleFocus(event) {
            state.focused = true;
            context.emit('focus', event);
        }
        function handleCompositionStart() {
            state.isComposing = true;
        }
        function handleCompositionUpdate(event) {
            const text = event.target.value;
            const lastCharacter = text[text.length - 1] || '';
            state.isComposing = !isKorean(lastCharacter);
        }
        function handleCompositionEnd(event) {
            if (state.isComposing) {
                state.isComposing = false;
                handleInput(event);
            }
        }
        function handleInput(event) {
            if (state.isComposing) return;
            if (event.target.value === state.nativeInputValue) return;
            context.emit('update:modelValue', event.target.value);
            setTimeout(() => {
                setNativeInputValue();
            }, 300);
        }
        function handleChange(event) {
            // event.stopPropagation();
            context.emit('change', event.target.value);
        }
        function calcIconOffset(place) {
            let elList = [].slice.call(currentInstance.vnode.el.querySelectorAll(`.el-input__${place}`) || []);
            if (!elList.length) return;
            let el = null;
            for (let i = 0; i < elList.length; i++) {
                if (elList[i].parentNode === currentInstance.vnode.el) {
                    el = elList[i];
                    break;
                }
            }
            if (!el) return;
            const pendantMap = {
                suffix: 'append',
                prefix: 'prepend'
            };

            const pendant = pendantMap[place];
            if (context.slots[pendant]) {
                el.style.transform = `translateX(${place === 'suffix' ? '-' : ''}${currentInstance.vnode.el.querySelectorAll(`.el-input-group__${pendant}`).offsetWidth}px)`;
            } else {
                el.removeAttribute('style');
            }
        }
        function updateIconOffset() {
            calcIconOffset('prefix');
            calcIconOffset('suffix');
        }
        function clear() {
            context.emit('input', '');
            context.emit('change', '')
            context.emit('clear');
        }
        function handlePasswordVisible() {
            state.passwordVisible = !state.passwordVisible;
            focus();
        }
        function getInput() {
            return input || textarea;
        }
        function getSuffixVisible() {
            return context.slots.suffix ||
                props.suffixIcon ||
                state.showClear ||
                state.showPassword ||
                state.isWordLimitVisible ||
                (state.validateState && state.needStatusIcon);
        }

        // 初始执行
        // context.on('inputSelect', state.select);
        onMounted(() => {
            setNativeInputValue();
            resizeTextarea();
            updateIconOffset();
        })
        onUpdated(() => {
            setTimeout(() => {
                updateIconOffset();
            }, 300);
        }) 
        return {
            state,
            context,
            textarea,
            input,
            focus,
            blur,
            getMigratingConfig,
            handleBlur,
            select,
            resizeTextarea,
            setNativeInputValue,
            handleFocus,
            handleCompositionStart,
            handleCompositionUpdate,
            handleCompositionEnd,
            handleInput,
            handleChange,
            calcIconOffset,
            updateIconOffset,
            clear,
            handlePasswordVisible,
            getInput,
            getSuffixVisible
        }
    }

}
</script>

八、element ui 之 form

(一)、源码

在这里插入图片描述
form组件:

<template>
  <form class="el-form" :class="[
    labelPosition ? 'el-form--label-' + labelPosition : '',
    { 'el-form--inline': inline }
  ]">
    <slot></slot>
  </form>
</template>
<script>
  import objectAssign from 'element-ui/src/utils/merge';

  export default {
    name: 'ElForm',

    componentName: 'ElForm',

    provide() {
      return {
        elForm: this
      };
    },

    props: {
      model: Object,
      rules: Object,
      labelPosition: String,
      labelWidth: String,
      labelSuffix: {
        type: String,
        default: ''
      },
      inline: Boolean,
      inlineMessage: Boolean,
      statusIcon: Boolean,
      showMessage: {
        type: Boolean,
        default: true
      },
      size: String,
      disabled: Boolean,
      validateOnRuleChange: {
        type: Boolean,
        default: true
      },
      hideRequiredAsterisk: {
        type: Boolean,
        default: false
      }
    },
    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(() => {});
        }
      }
    },
    computed: {
      autoLabelWidth() {
        if (!this.potentialLabelWidthArr.length) return 0;
        const max = Math.max(...this.potentialLabelWidthArr);
        return max ? `${max}px` : '';
      }
    },
    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: {
      resetFields() {
        if (!this.model) {
          console.warn('[Element Warn][Form]model is required for resetFields to work.');
          return;
        }
        this.fields.forEach(field => {
          field.resetField();
        });
      },
      clearValidate(props = []) {
        const fields = props.length
          ? (typeof props === 'string'
            ? this.fields.filter(field => props === field.prop)
            : this.fields.filter(field => props.indexOf(field.prop) > -1)
          ) : this.fields;
        fields.forEach(field => {
          field.clearValidate();
        });
      },
      validate(callback) {
        if (!this.model) {
          console.warn('[Element Warn][Form]model is required for validate to work!');
          return;
        }

        let promise;
        // if no callback, return 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;
        // 如果需要验证的fields为空,调用验证时立刻返回callback
        if (this.fields.length === 0 && callback) {
          callback(true);
        }
        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;
        }
      },
      validateField(props, cb) {
        props = [].concat(props);
        const fields = this.fields.filter(field => props.indexOf(field.prop) !== -1);
        if (!fields.length) {
          console.warn('[Element Warn]please pass correct props!');
          return;
        }

        fields.forEach(field => {
          field.validate('', cb);
        });
      },
      getLabelWidthIndex(width) {
        const index = this.potentialLabelWidthArr.indexOf(width);
        // it's impossible
        if (index === -1) {
          throw new Error('[ElementForm]unpected width ', width);
        }
        return index;
      },
      registerLabelWidth(val, oldVal) {
        if (val && oldVal) {
          const index = this.getLabelWidthIndex(oldVal);
          this.potentialLabelWidthArr.splice(index, 1, val);
        } else if (val) {
          this.potentialLabelWidthArr.push(val);
        }
      },
      deregisterLabelWidth(val) {
        const index = this.getLabelWidthIndex(val);
        this.potentialLabelWidthArr.splice(index, 1);
      }
    }
  };
</script>

form-item组件

<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';
  import LabelWrap from './label-wrap';
  export default {
    name: 'ElFormItem',

    componentName: 'ElFormItem',

    mixins: [emitter],

    provide() {
      return {
        elFormItem: this
      };
    },

    inject: ['elForm'],

    props: {
      label: String,
      labelWidth: String,
      prop: String,
      required: {
        type: Boolean,
        default: undefined
      },
      rules: [Object, Array],
      error: String,
      validateStatus: String,
      for: String,
      inlineMessage: {
        type: [String, Boolean],
        default: ''
      },
      showMessage: {
        type: Boolean,
        default: true
      },
      size: String
    },
    components: {
      // use this component to calculate auto width
      LabelWrap
    },
    watch: {
      error: {
        immediate: true,
        handler(value) {
          this.validateMessage = value;
          this.validateState = value ? 'error' : '';
        }
      },
      validateStatus(value) {
        this.validateState = value;
      }
    },
    computed: {
      labelFor() {
        return this.for || this.prop;
      },
      labelStyle() {
        const ret = {};
        if (this.form.labelPosition === 'top') return ret;
        const labelWidth = this.labelWidth || this.form.labelWidth;
        if (labelWidth) {
          ret.width = labelWidth;
        }
        return ret;
      },
      contentStyle() {
        const ret = {};
        const label = this.label;
        if (this.form.labelPosition === 'top' || this.form.inline) return ret;
        if (!label && !this.labelWidth && this.isNested) return ret;
        const labelWidth = this.labelWidth || this.form.labelWidth;
        if (labelWidth === 'auto') {
          if (this.labelWidth === 'auto') {
            ret.marginLeft = this.computedLabelWidth;
          } else if (this.form.labelWidth === 'auto') {
            ret.marginLeft = this.elForm.autoLabelWidth;
          }
        } else {
          ret.marginLeft = labelWidth;
        }
        return ret;
      },
      form() {
        let parent = this.$parent;
        let parentName = parent.$options.componentName;
        while (parentName !== 'ElForm') {
          if (parentName === 'ElFormItem') {
            this.isNested = true;
          }
          parent = parent.$parent;
          parentName = parent.$options.componentName;
        }
        return parent;
      },
      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;
      },
      isRequired() {
        let rules = this.getRules();
        let isRequired = false;

        if (rules && rules.length) {
          rules.every(rule => {
            if (rule.required) {
              isRequired = true;
              return false;
            }
            return true;
          });
        }
        return isRequired;
      },
      _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);
        });
      },
      clearValidate() {
        this.validateState = '';
        this.validateMessage = '';
        this.validateDisabled = false;
      },
      resetField() {
        this.validateState = '';
        this.validateMessage = '';

        let model = this.form.model;
        let value = this.fieldValue;
        let path = this.prop;
        if (path.indexOf(':') !== -1) {
          path = path.replace(/:/, '.');
        }

        let prop = getPropByPath(model, path, true);

        this.validateDisabled = true;
        if (Array.isArray(value)) {
          prop.o[prop.k] = [].concat(this.initialValue);
        } else {
          prop.o[prop.k] = this.initialValue;
        }

        // reset validateDisabled after onFieldChange triggered
        this.$nextTick(() => {
          this.validateDisabled = false;
        });

        this.broadcast('ElTimeSelect', 'fieldReset', this.initialValue);
      },
      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);
      },
      getFilteredRule(trigger) {
        const rules = this.getRules();

        return rules.filter(rule => {
          if (!rule.trigger || trigger === '') return true;
          if (Array.isArray(rule.trigger)) {
            return rule.trigger.indexOf(trigger) > -1;
          } else {
            return rule.trigger === trigger;
          }
        }).map(rule => objectAssign({}, rule));
      },
      onFieldBlur() {
        this.validate('blur');
      },
      onFieldChange() {
        if (this.validateDisabled) {
          this.validateDisabled = false;
          return;
        }

        this.validate('change');
      },
      updateComputedLabelWidth(width) {
        this.computedLabelWidth = width ? `${width}px` : '';
      },
      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);
        }
      },
      removeValidateEvents() {
        this.$off();
      }
    },
    mounted() {
      if (this.prop) {
        this.dispatch('ElForm', 'el.form.addField', [this]);

        let initialValue = this.fieldValue;
        if (Array.isArray(initialValue)) {
          initialValue = [].concat(initialValue);
        }
        Object.defineProperty(this, 'initialValue', {
          value: initialValue
        });

        this.addValidateEvents();
      }
    },
    beforeDestroy() {
      this.dispatch('ElForm', 'el.form.removeField', [this]);
    }
  };
</script>

label-warp组件

<script>

export default {
  props: {
    isAutoWidth: Boolean,
    updateAll: Boolean
  },

  inject: ['elForm', 'elFormItem'],

  render() {
    const slots = this.$slots.default;
    if (!slots) return null;
    if (this.isAutoWidth) {
      const autoLabelWidth = this.elForm.autoLabelWidth;
      const style = {};
      if (autoLabelWidth && autoLabelWidth !== 'auto') {
        const marginLeft = parseInt(autoLabelWidth, 10) - this.computedWidth;
        if (marginLeft) {
          style.marginLeft = marginLeft + 'px';
        }
      }
      return (<div class="el-form-item__label-wrap" style={style}>
        { slots }
      </div>);
    } else {
      return slots[0];
    }
  },

  methods: {
    getLabelWidth() {
      if (this.$el && this.$el.firstElementChild) {
        const computedWidth = window.getComputedStyle(this.$el.firstElementChild).width;
        return Math.ceil(parseFloat(computedWidth));
      } else {
        return 0;
      }
    },
    updateLabelWidth(action = 'update') {
      if (this.$slots.default && this.isAutoWidth && this.$el.firstElementChild) {
        if (action === 'update') {
          this.computedWidth = this.getLabelWidth();
        } else if (action === 'remove') {
          this.elForm.deregisterLabelWidth(this.computedWidth);
        }
      }
    }
  },

  watch: {
    computedWidth(val, oldVal) {
      if (this.updateAll) {
        this.elForm.registerLabelWidth(val, oldVal);
        this.elFormItem.updateComputedLabelWidth(val);
      }
    }
  },

  data() {
    return {
      computedWidth: 0
    };
  },

  mounted() {
    this.updateLabelWidth('update');
  },

  updated() {
    this.updateLabelWidth('update');
  },

  beforeDestroy() {
    this.updateLabelWidth('remove');
  }
};
</script>

(二)、用vue3重写 form

form组件

<template>
    <form class="el-form" :class="[
        labelPosition ? 'el-form--label-' + labelPosition : '',
        {'el-form--inline': inline}
    ]">
        <slot></slot>
    </form>
</template>
<script>
import '../../../plaginStyle/src/form.scss';
import {ref, reactive, computed, provide, watch, getCurrentInstance} from 'vue';
import objectAssign from '../../../plaginUtils/utils/merge.js';
import message from '../../../../../uniApp/components/uni-popup/message';
import { nextTick } from '../../../../../uniApp/wxcomponents/vant/common/utils';

export default {
    name: 'ElForm',

    componentName: 'ElForm',

    props: {
        model: Object,
        rules: Object,
        labelPosition: String,
        labelWidth: String,
        labelSuffix: {
            type: String,
            default: ''
        },
        inline: Boolean,
        inlineMessage:Boolean,
        statusIcon: Boolean,
        showMessage: {
            type: Boolean,
            default: true
        },
        size: String,
        disabled: Boolean,
        validateOnRuleChange: {
            type: Boolean,
            default: true
        },
        hideRequiredAsterisk: {
            type: Boolean,
            default: false
        }
    },

    setup(props, context) {
        const state = reactive({
            fields: [],
            potentialLabelWidthArr: [],
            autoLabelWidth: computed(() => {
                if (!state.potentialLabelWidthArr.length) return 0;
                const max = Math.max(...state.potentialLabelWidthArr);
                return max ? `${max}px` : '';
            }),
        })
        const currentInstance = getCurrentInstance();
        provide('elForm', currentInstance);
        watch(() => props.rules, (rules, preRules) => {
            
            state.fields.forEach(field => {
                field.removeValidateEvents();
                field.addValidateEvents();
            });

            if (props.validateOnRuleChange) {
                state.validate(() => {});
            }
        });

        // 初始执行
        // 监听子组件派发的方法
        nextTick(() => {
            const children = currentInstance.vnode.el.children;
            if (children.length > 0) {
                children.forEach((field) => {
                    state.fields.push(field.__vueParentComponent)
                })
            }
            console.log(state.fields);
            
        })
        

        function resetFields() {
            if (!props.model) {
                console.warn('[Element Warn][Form]model is required for resetFields to work.');
                return;
            }
            state.fields.forEach(field => {
                field.ctx.resetFields();
            });
        };

        function clearValidate(props = []) {
            const fields = props.length
                ? (typeof props === 'string'
                  ? state.fields.filter(field => props === field.prop)
                  : state.fields.filter(field => props.indexOf(field.prop) > -1)
                ) : state.fields;
            fields.forEach(field => {
                field.clearValidate();
            })
        };
        
        function validate(callback) {
            if (!props.model) {
                console.warn('[Element Warn][Form]model is required for validate to work!');
                return;
            }

            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;
            // 如果需要验证的fields为空,调用验证时立刻返回callback
            if (state.fields.length === 0 && callback) {
                callback(true);
            }
            let invalidFields = {};
            state.fields.forEach(field => {
                
                field.ctx.validate('', (message, field) => {
                    if (message) {
                        valid = false;
                    }
                    invalidFields = objectAssign({}, invalidFields, field);
                    if (typeof callback === 'function' && ++count === state.fields.length) {
                        callback(valid, invalidFields);
                    }
                });
            });

            if (promise) {
                return promise;
            }
        };
        function validateField(props, cb) {
            props = [].concat(props);
            
            const fields = state.fields.filter(field => {return props.indexOf(field.ctx.prop) !== -1});
            if (!fields.length) {
                console.warn('[Element Warn]please pass correct props!');
                return;
            }

            fields.forEach(field => {
                field.ctx.validate('', cb);
            });
        };
        function getLabelWidthIndex(width) {
            const index = state.potentialLabelWidthArr.indexOf(width);
            if (index === -1) {
                throw new Error('[ElementForm]unpected width ', width);
            }
            return index;
        };
        function registerLabelWidth(val, oldVal) {
            if (val && oldVal) {
                const index = getLabelWidthIndex(oldVal);
                state.potentialLabelWidthArr.splice(index, 1, val);
            } else if (val) {
                state.potentialLabelWidthArr.push(val);
            }
        };
        function deregisterLabelWidth(val) {
            const index = getLabelWidthIndex(val);
            state.potentialLabelWidthArr.splice(index, 1);
        }
        return {
            state,
            currentInstance,
            context,
            resetFields,
            clearValidate,
            validate,
            validateField,
            getLabelWidthIndex,
            registerLabelWidth,
            deregisterLabelWidth
        }
    }
}
</script>

form-item组件

<template>
    <div class="el-form-item" :class="[{
        'el-form-item--feedback': elForm && elForm.ctx.statusIcon,
        'is-error': state.validateState === 'error',
        'is-validating': state.validateState === 'validating',
        'is-success': state.validateState === 'success',
        'is-required': state.isRequired || required,
        'is-no-asterisk': elForm && elForm.ctx.hideRequiredAsterisk
    },
    state.sizeClass ? 'el-form-item--' + state.sizeClass : ''
    ]">
        <label-wrap 
            :is-auto-width="state.labelStyle && state.labelStyle.width === 'auto'"
            :update-all="state.form.ctx.labelWidth === 'auto'">
            <label :for="state.labelFor" class="el-form-item__label" :style="state.labelStyle" v-if="label || context.slots.label">
                <slot name="label">{{label + state.form.ctx.labelSuffix}}</slot>
            </label>
        </label-wrap>
        <div class="el-form-item__content" :style="state.contentStyle">
            <slot></slot>
            <transition name="el-zoom-in-top">
                <slot
                    v-if="state.validateState === 'error' && showMessage && state.form.ctx.showMessage"
                    name="error"
                    :error="state.validateMessage">
                    <div
                        class="el-form-item__error"
                        :class="{
                            'el-form-item__error--inline': typeof inlineMessage === 'boolean'
                            ? inlineMessage
                            : (elForm && elForm.ctx.inlineMessage || false)
                        }">
                        {{state.validateMessage}}
                    </div>
                    </slot>
            </transition>
        </div>
    </div>
</template>
<script>
import '../../../plaginStyle/src/form-item.scss';
import AsyncValidator from 'async-validator';
import emitter from '../../../plaginSrc/mixins/emitter';
import objectAssign from '../../../plaginUtils/utils/merge';
import fun from '../../../plaginUtils/utils/util';
import LabelWrap from './label-wrap';
import {ref, reactive, provide, inject, getCurrentInstance, computed, watch, nextTick, onMounted, onBeforeUnmount} from 'vue';
export default {
    name: 'ElFormItem',

    componentName: 'ElFormItem',

    props: {
        label: String,
        labelWidth: String,
        prop: String,
        required: {
            type: Boolean,
            default: undefined
        },
        rules: [Object, Array],
        error: String,
        validateStatus: String,
        for: String,
        inlineMessage: {
            type: [String, Boolean],
            default: ''
        },
        showMessage: {
            type: Boolean,
            default: true
        },
        size: String
    },
    components: {
        LabelWrap
    },
    setup(props, context) {
        const currentInstance = getCurrentInstance();
        provide('elFormItem', currentInstance);
        const elForm = inject('elForm', null);
        const {dispatch} = emitter(currentInstance);
        const {getPropByPath} = fun;
        const state = reactive({
            validateState: '',
            validateMessage: '',
            validateDisabled: false,
            validator: {},
            isNested: false,
            computedLabelWidth: '',
            labelFor: computed(() => {
                return props.for || props.prop;
            }),
            labelStyle: computed(() => {
                const ret = {};
                if (state.form.ctx.labelPosition === 'top') return ret;
                
                const labelWidth = props.labelWidth || state.form.ctx.labelWidth;
                if (labelWidth) {
                    ret.width = labelWidth;
                }
                
                return ret;
            }),
            contentStyle: computed(() => {
                const ret = {};
                const label = props.label;
                if (state.form.ctx.labelPosition === 'top' || state.form.ctx.inline) return ret;
                if (!label && !props.labelWidth && state.isNested) return ret;
                const labelWidth = props.labelWidth || state.form.ctx.labelWidth;
                if (labelWidth === 'auto') {
                    if (props.labelWidth === 'auto') {
                        ret.marginLeft  = state.computedLabelWidth;
                    } else if (state.form.ctx.labelWidth === 'auto') {
                        
                        ret.marginLeft = elForm.ctx.autoLabelWidth;
                    }
                } else {
                    ret.marginLeft = labelWidth;
                }
                return ret;
            }),
            form: computed(() => {
                let parent = currentInstance.parent;

                let parentName = parent.type.componentName;
                while (parentName !== 'ElForm') {
                    if (parentName === 'ElFormItem') {
                        state.isNested = true;
                    }
                    parent = parent.parent;
                    parentName = parent.type.componentName;
                }
                return parent;
            }),
            fieldValue: computed(() => {
                const model = state.form.ctx.model;
                if (!model || !props.prop) { return; }

                let path = props.prop;
                if (path.indexOf(':') !== -1) {
                    path = path.replace(/:/, '.');
                }
                
                return getPropByPath(model, path, true).v;

            }),
            isRequired: computed(() => {
                let rules = getRules();
                let isRequired = false;

                if (rules && rules.length) {
                    rules.every(rule => {
                        if (rule.required) {
                            isRequired = true;
                            return false;
                        }
                        return true;
                    })
                }
                return isRequired;
            }),
            _formSize: computed(() => {
                return elForm.ctx.size;
            }),
            elFormItemSize: computed(() => {
                return props.size || state._formSize;
            }),
            sizeClass: computed(() => {
                return state.elFormItemSize
            })
        });
        watch(() => props.error, (value, prevalue) => {
            state.validateMessage = value;
            state.validateState = value ? 'error' : '';
        });
        watch(() => state.validateStatus, (value) => {
            state.validateState = value;
        });
        onMounted(() => {
            if (props.prop) {
                dispatch('ElForm', 'el.form.addField', [currentInstance]);

                let initialValue = state.fieldValue;
                
                if (Array.isArray(initialValue)) {
                    initialValue = [].concat(initialValue);
                }
                // Object.assign({}, currentInstance.ctx, {
                //     initialValue: {
                //         value: initialValue
                //     }
                // })
                Object.defineProperty(currentInstance.ctx, 'initialValue', {
                    value: initialValue
                })
                
                addValidateEvents();
            }
        });
        onBeforeUnmount(() => {
            dispatch('ElForm', 'el.form.removeField', [currentInstance]);
        });
        function validate(trigger, callback = noop) {
            state.validateDisabled = false;
            const rules = getFilteredRule(trigger);
            // rules没有不需校验,直接返回true
            if ((!rules || rules.length === 0) && props.required === undefined ) {
                callback();
                return true;
            }

            state.validateState = 'validating';

            // 如果存在rule,将rules的每一项中的trigger去掉 并赋值
            const descriptor = {};
            if (rules && rules.length > 0) {
                rules.forEach(rule => {
                    delete rule.trigger;
                })
            }
            descriptor[props.prop] = rules;

            const validator = new AsyncValidator(descriptor);
            const model = {};

            model[props.prop] = state.fieldValue;

            validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
                state.validateState = !errors ? 'success' : 'error';
                state.validateMessage = errors ? errors[0].message : '';

                callback(state.validateMessage, invalidFields);
                elForm && elForm.emit('validate', props.prop, !errors, state.validateMessage || null);
            });
        }; 
        function clearValidate() {
            state.validateState = '';
            state.validateMessage = '';
            state.validateDisabled = false;
        };
        function resetFields() {
            state.validateState = '';
            state.validateMessage = '';

            let model = state.form.model;
            let value = state.fieldValue;
            let path = props.prop;
            if (path.indexOf(':') !== -1) {
                path = path.replace(/:/, '.');
            }

            let prop = getPropBypath(model, path, true);

            this.validateDisabled = true;
            if (Array.isArray(value)) {
                prop.o[prop.k] = [].concat(state.initialValue);
            } else {
                prop.o[prop.k] = state.initialValue;
            }

            const changeValidateDisabled = async () => {
                state.validateDisabled = false;
                await nextTick();
            } 

            state.broadcast('ElTimeSelect', 'fieldReset', state.initialValue);
        };
        function getRules() {
            let formRules = state.form.ctx.rules; // form的校验规则
            const selfRules = props.rules;  // form-item自身的校验规则
            const requiredRule = props.required !== undefined ? { required: !!props.required } : []; // 直接required属性

            const prop = getPropByPath(formRules, props.prop || '');

            formRules = formRules ? (prop.o[props.prop || ''] || prop.v) : [];
            
            return [].concat(selfRules || formRules || []).concat(requiredRule);
        };
        function getFilteredRule(trigger) {
            const rules = getRules(); // 获取所有需要检测的项
            
            return rules.filter(rule => {
                if (!rule.trigger || trigger === '') return true;
                if (Array.isArray(rule.trigger)) {
                    return rule.trigger.indexOf(trigger) > -1;
                } else {
                    return rule.trigger === trigger;
                }
            }).map(rule => {
                return objectAssign({}, rule)
            }); // 对需要检测的项,返回
        };
        function onFieldBlur() {
            validate('blur');
        };
        function onFieldChange() {
            if (state.validateDisabled) {
                state.validateDisabled = false;
                return;
            }

            validate('change');
        };
        function updateComputedLabelWidth(width) {
            state.computedLabelWidth = width ? `${width}px`: '';
        };
        function addValidateEvents() {
             const rules = getRules();
             
             if (rules.length || props.required !== undefined) {
                //  向form传值
             }
        };
        function removeValidateEvents() {
            currentInstance.off();
        }

        return {
            state,
            context,
            elForm,
            currentInstance,
            validate,
            clearValidate,
            resetFields,
            getRules,
            getFilteredRule,
            onFieldBlur,
            onFieldChange,
            updateComputedLabelWidth,
            addValidateEvents,
            removeValidateEvents
        }
    }
}
</script>

label-warp组件

<template>
    <div v-if="context.slots.default">
        <!-- v-if="isAutoWidth"  -->
        <div 
            class="el-form-item__label-wrap"
            :style="state.style">
            <slot></slot>
        </div>
        <!-- <template>
            {{context.slots[0]}}
        </template> -->
    </div>
</template>
<script>
import {ref, reactive, inject, onMounted, onUpdated, onBeforeUnmount, watch, getCurrentInstance, render, h} from 'vue';
export default {
    props: {
        isAutoWidth: Boolean,
        updateAll: Boolean
    },

    setup(props, context) {
        const elForm = inject('elForm', null);
        const elFormItem = inject('elFormItem', null);
        const currentInstance = getCurrentInstance();
        const state = reactive({
            computedWidth: 0,
            style: {}
        })
        
        watch(() => state.computedWidth, (val, oldVal) => {
            if (props.updateAll) {
                elForm.ctx.registerLabelWidth(val, oldVal);
                elFormItem.ctx.updateComputedLabelWidth(val);
            }
        })
        onMounted(() => {
            updateLabelWidth('update');
        });
        onUpdated(() => {
            updateLabelWidth('update');
        })
        onBeforeUnmount(() => {
            updateLabelWidth('remove');
        })
        function getLabelWidth() {
            if (currentInstance.vnode.el && currentInstance.vnode.el.firstElementChild) {
                const computedWidth = window.getComputedStyle(currentInstance.vnode.el.firstElementChild).width;
                return Math.ceil(parseFloat(computedWidth));
            } else {
                return 0;
            }
        }
        function updateLabelWidth(action = 'update') {
            if (currentInstance.slots.default && props.isAutoWidth && currentInstance.vnode.el.firstElementChild) {
                if (action === 'update') {
                    state.computedWidth = getLabelWidth();
                } else if (action === 'remove') {
                    elForm.deregisterLabelWidth(state.computedWidth);
                }
            }
        }
        
        (() => {
            
            const slots = context.slots.default;
            if (!slots) return null;
            if (props.isAutoWidth) {
                const autoLabelWidth = elForm.autoLabelWidth;
                const style = {};
                if (autoLabelWidth && autoLabelWidth !== 'auto') {
                    const marginLeft = parseInt(autoLabelWidth, 10) - this.computedWidth;
                    if (marginLeft) {
                        style.marginLeft = marginLeft + 'px';
                    }
                }
                state.style = style;

            } else {
                return slots[0]
            }
        })
        return {
            state,
            context,
            getLabelWidth,
            updateLabelWidth
        }
    }
}
</script>

九、vue3中引入插件的方式,及其他差异点

  • vue3使用插件方式,使用use方法
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './assets/js/flexible.debug'
import { Button, Skeleton } from 'vant';
import ELButton from './plagins/button/index.js';
import ElRow from './plagins/row/index.js';
import ElCol from './plagins/col/index.js';
import ElContainer from './plagins/container/index.js';
import ElHeader from './plagins/header/index.js';
import ElAside from './plagins/aside/index.js';
import ElLink from './plagins/link/index.js';
import ElRadio from './plagins/radio/index.js';
import ElRadioGroup from './plagins/radio-group/index.js';
import ElRadioButton from './plagins/radio-button/index.js';
import ElCheckbox from './plagins/checkbox/index.js';
import ElCheckboxGroup from './plagins/checkbox-group/index.js';
import ElCheckboxButton from './plagins/checkbox-button/index.js';
import ElInput from './plagins/input/index.js';
import ElForm from './plagins/form/index.js';
import ElFormItem from './plagins/form-item/index.js';

createApp(App).use(store).use(router)
.use(Button).use(Skeleton)
.use(ELButton)
.use(ElRow)
.use(ElCol)
.use(ElContainer)
.use(ElHeader)
.use(ElAside)
.use(ElLink)
.use(ElRadio)
.use(ElRadioGroup)
.use(ElRadioButton)
.use(ElCheckbox)
.use(ElCheckboxGroup)
.use(ElCheckboxButton)
.use(ElInput)
.use(ElForm)
.use(ElFormItem)
.mount('#app')


  • 具名插槽使用方式
    在这里插入图片描述
  • 组件定义
    在setup里面直接用ref()定义,名称为组件名,初始值为空即可

总结

写到后面就不想再写文字了,主要目的是记录一下代码,顺带写一下差异,基本上的差异都列举出来了,以后复习应该看得懂

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值