2
This article records and imitates an el-tabs component, which will help you better understand the specific working details of the wheel of Ele.me ui. This article is another article in the elementui source code learning and imitation series. I will continue to update and imitate other components when I am free. The source code is on github, you can pull it down, npm start to run, and comments are helpful for better understanding. The github warehouse address is as follows: https://github.com/shuirongshuifu/elementSrcCodeStudy

Knowledge point review

In order to better read the subsequent code, we need to review the knowledge again

The render function in vue writes jsx syntax

In Vue, we write component pages, which are often separated from structure, logic, and style, such as:

 <template>
    <!-- 这里写结构 -->
</template>
<script>
    // 这里写逻辑
</script>
<style>
    // 这里写样式
</style>

If you are familiar with react syntax, you will find that react is a (jsx) syntax that writes structure and logic together. In fact, jsx syntax can also be used in vue, but it must be written in the render function. For example, we want to use the render function to write an H3 tag with a red font and a yellow-green background. The code can be written as follows:

 <script>
export default {
  data() { return { name: "孙悟空" }; },
  render(h) {
    /**
     * 第一步,准备一个dom,dom中使用单大括号作为占位符,单大括号中可以使用变量
     *        vue中一般是双大括号中使用变量,区别不大。再一个就是两头使用小括号
     *        包裹(方便换行书写)
     * */
    let dom = (
      <div class="box">
        <h3 class="h3Class">{this.name}</h3>
      </div>
    );
    // 第二步,返回。如此render函数就会自动翻译渲染之
    return dom;
  },
};
</script>
<style>
.box { background: yellowgreen; }
.h3Class { color: red; }
</style>

Effect picture:

In the above code, we only need to remember that single curly brackets are used in the jsx syntax to indicate the use of variables.

Another is in the render function, how to pass parameters to the child components? Also use 单大括号搭配点点点... , because single curly brackets represent variables. The following code:

 render(h) {
    const sendParamsData = { // 准备参数
      props: {
        name: this.name, 
        age: this.age, 
        home: this.home, 
      },
    };
    return (
      <div class="kkk">
        {/* 传递参数 */}
        <child {...sendParamsData}></child>
      </div>
    );
  },

Ele.me's official el-tabs component is written using jsx syntax (because jsx syntax is more flexible), so the template for general business needs is enough. Flexible and complex requirements, consider using jsx

this.$slots.default gets the part of the unnamed slot in the content of the component label

this.$slots.default This api may not be used very much by everyone. This api can be as the name suggests. $slots , the meaning of the slot (plural, there may be multiple), default the default meaning. That's what the default slot is for. That is this.$slots.default这个变量保存了所有不是命名插槽且不是作用域插槽的所有普通插槽内容。是个数组(如果有的话) . Let's take a look at the following code and it's easy to use

 // 父组件
<template>
    <child>
      <h3>孙悟空</h3>
      <h3>猪八戒</h3>
      <h3>沙和尚</h3>
    </child>
</template>
<script>
import child from "./child.vue";
export default { components: { child }};
</script>

// 子组件
<template>
  <div>我是子组件</div>
</template>
<script>
export default {
  mounted() { console.log(this)},
};
</script>

The printed this component instance diagram is as follows:


Of course, here we write the ordinary label h3, and we can also write the component label here, then this.$slots.default the storage is the component label. If you can get all the components of the component, you can go further, process and store it, and pass it on for use. . Combined with the use of the official el-tabs component, the code is as follows:

 <el-tabs v-model="activeName" @tab-click="handleClick"> 
    <el-tab-pane label="用户管理" name="first">用户管理</el-tab-pane> 
    <el-tab-pane label="配置管理" name="second">配置管理</el-tab-pane> 
    <el-tab-pane label="角色管理" name="third">角色管理</el-tab-pane> 
</el-tabs>

We found that it seems to be very similar.是的, this.$slots.default e10f58f5c22a329ff38fd23e99159f0d---这个数组,拿到每一个---28587bb75f3030a60495e09fc1ea6b3a el-tab-pane组件上的labelname的信息, Then pass it to the tab-nav component, so the tab-nav component will display tab information one after another (for users to click)

The parent component v-model passes parameters, the key in the child component props is 'value' to receive, and the 'input' event changes

Looking at the official use case, we also found that v-model is bound to the component el-tabs component. Usually we use v-model are usually bound to form control components such as input boxes, drop-down boxes, and switches. You only need to write one v-model and you don't need to do anything else. But v-model If it is bound to a common custom component and used for parent-child component transfer parameters (two-way data binding), we need to write more code. Let's take a look at the case:

 // 父组件
<template>
  <child v-model="age"></child>
</template>
<script>
import child from "./child.vue";
export default {
  components: { child },
  data() {
    return {
      age: 500,
    };
  },
};
</script>

// 子组件
<template>
  <div>
    <h3>孙悟空年龄是:{{ ageValue }}</h3>
    <button @click="add">加一</button>
  </div>
</template>

<script>
export default {
  props: {
    value: null, // 声明接收
  },
  data() {
    return {
      ageValue: this.value, // 取到值用于显示
    };
  },
  methods: {
    add() {
      this.$emit("input", this.ageValue); // 父组件更新值的时候,子组件也要更新
      this.ageValue = this.ageValue + 1; // 子组件手动更新(另,使用watch监听value值变化自动更新也行)
    },
  },
};
</script>

The effect diagram is as follows:

reason:

 // 父组件v-model=age相当于
<child v-model="age"></child>

<child :value="age" @input="age = $event"></child>

Therefore, the subcomponent needs to use ---bc81ba5d9a04c65fc8998468f3f10511 props in value to receive, and use this.$emit("input", xxx) to trigger

start copying

First build a tabs structure

We know that tabs means tab switching. The whole can be divided into three parts: 选项卡部分 , 内容区部分 , 整个选项卡盒子部分 , so here we create three new files Implement this tabs component.

  1. tabs.vue file (the entire tab box part), the file used as a large container for the entire tab switching, in this file to process 选项卡部分 logic, and 内容区部分
  2. tabNav.vue File (tab section)
  3. tabContent.vue file (content area part)

The diagram is as follows:

Of course, the author has already packaged it here. Let's first see how to use it, as well as the renderings, and then look at the packaged code.

Use packaged components

 <template>
  <div>
    <my-tabs v-model="activeName" @tabClick="tabClick">
      <my-tab-content label="孙悟空" name="sunwukong"
        >孙悟空内容</my-tab-content
      >
      <my-tab-content label="猪八戒" name="zhubajie">猪八戒内容</my-tab-content>
      <my-tab-content label="沙和尚" name="shaheshang"
        >沙和尚内容</my-tab-content
      >
      <my-tab-content label="唐僧" name="tangseng">唐僧内容</my-tab-content>
      <my-tab-content label="白龙马" name="bailongma"
        >白龙马内容</my-tab-content
      >
    </my-tabs>
    <br />
    <hr />
    <my-tabs v-model="activeName2" :before-leave="beforeLeave">
      <my-tab-content label="武松" name="wusong">武松内容</my-tab-content>
      <my-tab-content label="宋江" name="songjiang">松江内容</my-tab-content>
      <my-tab-content label="林冲" name="linchong">林冲内容</my-tab-content>
      <my-tab-content disabled label="吴用" name="wuyong"
        >吴用内容</my-tab-content
      >
    </my-tabs>
  </div>
</template>

<script>
export default {
  data() {
    return {
      activeName: "sunwukong",
      activeName2: "wusong",
    };
  },
  methods: {
    tabClick(tabItem) {
      console.log("外层点击", tabItem);
    },
    beforeLeave(newTabName, oldTabName) {
      console.log("外层", newTabName, oldTabName);
      /**
       * 方式一:
       *    return true; // 表示允许切换tab
       *    return false; // 表示不允许切换tab
       * */
      /**
       * 方式二:
       *    使用Promise处理异步相关切换tab操作,比如问询操作
       * */
      var p = new Promise((resolve, reject) => {
        this.$confirm(`确认由${oldTabName}切换到${newTabName}`, "tab change", {
          confirmButtonText: "确认切换",
          cancelButtonText: "取消",
          type: "warning",
        })
          .then(() => {
            resolve(true); // 允许放行通过切换tab
          })
          .catch((err) => {
            reject(false); // 不允许切换tab
          });
      }).catch((reason) => {
        // 注意此处须用Promise的catch方法捕获错误,否则控制台报错 Uncaught (in promise)
        console.log("reason", reason);
      });
      // 最后返回Promise的结果
      return p;
    },
  },
};
</script>

renderings

tabs.vue component (equivalent to data transfer station)

 <script>
import tabNav from "./tabNav.vue"; // 引入tab导航页组件
export default {
  name: "myTabs",
  components: { tabNav }, // 注册之
  props: {
    // 父组件用v-model传参,子组件须用value接参,方可接到v-model="activeName"绑定的activeName的值
    value: null, // 接收到的值即为当前高亮的是哪一项
    // 传递一个函数,作为tab切换的钩子函数
    beforeLeave: {
      // 切换标签之前的钩子,若返回 false 或者返回 Promise 且被 reject,则阻止切换
      type: Function,
      default: () => {
        return true; // 默认为true,始终允许切换tab
      },
    },
  },
  data() {
    return {
      tabItemArr: [], // 用于传递给tabNav组件信息数据的数组
      activeName: this.value, // 高亮的是哪个tab标签页选项卡
    };
  },
  mounted() {
    /**
     * 计算收集tab页内容信息,将需要用到的信息存在tabItemArr数组中
     * 并传递给tabNav组件,tabNav组件根据tabItemArr信息去v-for渲染有哪些
     * */
    this.calcTabItemInstances();
  },
  methods: {
    calcTabItemInstances() {
      // 重点方法
      // 获取使用的地方的my-tab标签中间的内容
      if (this.$slots.default) {
        // 收集my-tab标签中间的插槽内容数组
        let slotTabItemArr = this.$slots.default; // console.log("slotTabItemArr", slotTabItemArr);
        // 然后把这些数据交给tab-nav动态渲染
        this.tabItemArr = slotTabItemArr.map((item) => {
          return item.componentInstance; // 只保留componentInstance组件实例即可,可以理解为组件的this
        });
        // consoloe.log('this.tabItemArr',this.tabItemArr)
      } else {
        this.tabItemArr = []; // 没传递就置为空,当然需要规范使用组件,规范传递相关参数
      }
    },
    handleTabClick(tabItem) {
      this.$emit("tabClick", tabItem); // 通知父元素点击的是谁,是哪个tab-nav
      let newTabName = tabItem.name; // 获取传出来的最新的name名字
      this.setCurrentName(newTabName); // 执行更新方法
    },
    // 考虑到可能有异步操作,所以加上async await(比如在切换tab标签页之前,做一个问询)
    async setCurrentName(newTabName) {
      let oldTabName = this.activeName; // 要更新了,所以当下的就变成旧的了
      let res = await this.beforeLeave(newTabName, oldTabName);
      if (res) {
        this.$emit("input", newTabName); // 更新父组件的v-model绑定的值
        this.activeName = newTabName; // 自身也更新一下
      }
    },
  },
  render(h) {
    // 准备参数,以便把参数传递给tab-nav组件
    const navData = {
      props: {
        tabItemArr: this.tabItemArr, // 内容区相关信息数组
        activeName: this.activeName, // 当前高亮的是哪一项
        onTabClick: this.handleTabClick, // 点击某一tab项的回调
      },
    };
    return (
      <div class="tab-Box">
        <tab-nav {...navData}></tab-nav>
        <div class="my-tab-content-item-box">{this.$slots.default}</div>
      </div>
    );
    /**
     * 注意:<div class="my-tab-content-item-box">{this.$slots.default}</div>写法,正常会把所有的都渲染出来
     * 所以我们在myTabContent组件中再加一个判断(v-show="isActiveToShowContent"),看看当前高亮的名字是否和组件的名字一致,
     * 一致才渲染.这样的话,同一时刻,根据myTabContent组件的name属性,只会对应渲染一个
     * */
  },
};
</script>

tabNav.vue component (receiving data from tabs.vue for v-for dynamic rendering)

 <template>
  <div class="my-tab-nav-item-box">
    <div
      :class="[
        'my-tab-nav-item',
        tabItem.name === activeName ? 'highLight' : '',
        tabItem.disabled ? 'isForbiddenItem' : '',
      ]"
      v-for="(tabItem, index) in tabItemArr"
      :key="index"
      @click="changeActiveName(tabItem)"
    >
      {{ tabItem.label }}
    </div>
  </div>
</template>
<script>
export default {
  name: "myTabNav",
  props: {
    // 源自于内容区的数组数据,非常重要
    tabItemArr: {
      type: Array,
      default: [],
    },
    // 当前激活的名字
    activeName: {
      type: String,
      default: "",
    },
    // 接收点击选项卡函数,在点击tab选项卡的时候,通过此函数传递出去
    onTabClick: {
      type: Function,
    },
  },
  methods: {
    changeActiveName(tabItem) {
      // 自己点自己就不让执行了
      if (tabItem.name === this.activeName) {
        return;
      }
      // 如果包含禁用项disabled属性(即处于禁用状态),也不让执行(搭配.isForbiddenItem类名)
      if (tabItem.disabled) {
        return;
      }
      this.onTabClick(tabItem);
    },
  },
};
</script>
<style lang="less" scoped>
.my-tab-nav-item-box {
  width: 100%;
  border-bottom: 1px solid #e9e9e9;
  .my-tab-nav-item {
    // 转换成行内盒子,每一项都水平排列
    display: inline-block;
    // 垂直居中
    height: 40px;
    line-height: 40px;
    // 字体样式位置设置一下
    font-size: 14px;
    font-weight: 500;
    color: #303133;
    margin: 0 12px;
    cursor: pointer;
  }
  // 非禁用时鼠标悬浮样式,注意这里not的使用
  .my-tab-nav-item:not(.isForbiddenItem):hover {
    color: #409eff;
  }
  // 高亮项样式
  .highLight {
    color: #409eff;
    border-bottom: 1px solid #409eff;
  }
  // 禁用项样式
  .isForbiddenItem {
    cursor: not-allowed;
    color: #aaa;
  }
}
</style>

myTabContent.vue component (only one is rendered with v-show conditional comparison)

 <template>
  <div class="my-tab-content-item" v-show="isActiveToShowContent">
    <slot></slot>
  </div>
</template>

<script>
export default {
  name: "myTabContent",
  props: {
    label: String, // 标签名
    name: String, // 每个下方内容区都有自己的name名字
    disabled: {
      // 是否禁用这一项
      type: Boolean,
      default: false, // 默认不禁用
    },
  },
  computed: {
    // 控制根据高亮的tab显示对应标签页内容
    isActiveToShowContent() {
      let activeName = this.$parent.value; // 比如当前高亮的是 sunwukong
      let currentName = this.name; // this.name的值有很多个,有:sunwukong、zhubajie、shaheshang...
      // 谁等于,就显示谁
      return activeName === currentName;
    },
  },
};
</script>

<style>
.my-tab-content-item { padding: 12px;}
</style>

Summarize

For the tabs switching effect, you can write it yourself, such as using dynamic components (I also wrote the article link of dynamic components: https://juejin.cn/post/6957696034449391646 ), or encapsulate a tabs component yourself.

However, this article is just a simple component that is officially packaged by Ele.me (the writing in some places may not be the same as the official one, but the ideas are the same). When creating an official package component, there are many situations that need to be considered (maybe some situations are rarely used), this article is just imitating and achieving common effects. In actual development, the degree of encapsulation of components needs to be considered according to the needs of the project. Do not over-encapsulate or not encapsulate. After all, component reuse can indeed improve development efficiency.

If this article can help you better understand the process principle and data transmission method of el-tabs, I would like to thank you github仓库star一下哦 . After all elementui源码学习之仿写xxx is a series of articles, your start is the driving force for our creation ^_^


水冗水孚
1.1k 声望585 粉丝

每一个不曾起舞的日子,都是对生命的辜负