elementui 中 loading 组件源码解析

开始前的准备

  1. 将 elementui 的 master 分支完整的 clone 下来,传送门

  2. 执行npm run dev下载好依赖,如果总是执行失败,一般都是 nodesass 为下载成功导致的,这时需要提前下载好 nodesass 然后再执行npm run dev:play就好.成功之后打开 localhost:8085

  3. 代码定位,因为执行的是npm run dev:play,具体代码如下

"dev:play": "npm run build:file && cross-env NODE_ENV=development PLAY_ENV=true webpack-dev-server --config build/webpack.demo.js"

然后我们查找build/webpack.demo.js,然后找到如下代码

entry: isProd ? {
    docs: './examples/entry.js'
  } : (isPlay ? './examples/play.js' : './examples/entry.js'),

很明显,我们的入口在./examples/play.js,然后我们根据这个文件找到 examples/play 文件夹下的 vue 文件,那么我们的准备工作就算是完成了。

正式开始

文档分析

该组件有两种调用方式,一种是指令(通过 v-loading 去调用),一种是通过 Vue 实例方法调用(通过 this.$loading 去调用)

目录结构

image.png

loading 文件夹下面的 index 文件代码如下

import directive from './src/directive';
import service from './src/index';

export default {
  install(Vue) {
    Vue.use(directive);
    Vue.prototype.$loading = service;
  },
  directive,
  service
};

根据以上代码,我们就可以知道支持指令来调用的在./src/directive文件中,支持添加 Vue 实例方法调用的在./src/index文件中。

添加 Vue 实例方法

  1. 查看源文件
    通过目录结构,我们已经知道两种方法调用的源文件其实就是loading.vue文件,另外两个只是在此基础上做的扩展
    源码如下:
<template>
  <transition name="el-loading-fade" @after-leave="handleAfterLeave">
    <div
      v-show="visible"
      class="el-loading-mask"
      :style="{ backgroundColor: background || '' }"
      :class="[customClass, { 'is-fullscreen': fullscreen }]">
      <div class="el-loading-spinner">
        <svg v-if="!spinner" class="circular" viewBox="25 25 50 50">
          <circle class="path" cx="50" cy="50" r="20" fill="none"/>
        </svg>
        <i v-else :class="spinner"></i>
        <p v-if="text" class="el-loading-text">{{ text }}</p>
      </div>
    </div>
  </transition>
</template>

<script>
  export default {
    data() {
      return {
        text: null,
        spinner: null,
        background: null,
        fullscreen: true,
        visible: false,
        customClass: ''
      };
    },

    methods: {
      handleAfterLeave() {
        this.$emit('after-leave');
      },
      setText(text) {
        this.text = text;
      }
    }
  };
</script>

源码内容比较简单,里面就是 transition 动画包裹的一个绝对定位的盒子,里面装着默认的 svg 以及自定位文字,
接下来是扩展部分,内容在 src 文件夹下的 index.js 。因为其中有很大一部分代码在处理样式及层级关系,所以被我剔除掉了,下面只展示核心代码:

import loadingVue from './loading.vue';

const LoadingConstructor = Vue.extend(loadingVue);
        const defaults = {
            text: null,
            fullscreen: true,
            body: false,
            lock: false,
            customClass: ''
        };
        let fullscreenLoading;//用来保存弹窗实例
        // 关闭弹窗的方法
        function afterLeave(instance, callback, speed = 300, once = false) {
            let called = false;
            const afterLeaveCallback = function () {
                if (called) return;
                called = true;
                if (callback) {
                    callback.apply(null, arguments);
                }
            };

            setTimeout(() => {
                afterLeaveCallback();
            }, speed + 100);
        };

        LoadingConstructor.prototype.close = function () {
            // 清除弹窗实例
            if (this.fullscreen) {
                fullscreenLoading = undefined;
            }
            afterLeave(this, _ => {
                // 结束后删除节点
                if (this.$el && this.$el.parentNode) {
                    this.$el.parentNode.removeChild(this.$el);
                }
                this.$destroy();
            }, 300);
            this.visible = false;
        };
        let service = (options = {}) => {
            // 合并options,源码中用的是 Object.assign 的 profill
            options = Object.assign({}, defaults, options);
            if (typeof options.target === 'string') {
                options.target = document.querySelector(options.target);
            }
            options.target = options.target || document.body;
            if (options.target !== document.body) {
                options.fullscreen = false;
            } else {
                options.body = true;
            }
            // 覆盖整个body的loading只能有一个
            if (options.fullscreen && fullscreenLoading) {
                return fullscreenLoading;
            }
            let instance = new LoadingConstructor({
                el: document.createElement('div'),
                data: options
            });
            // 如果没有 target ,那么弹窗将会直接挂载在body上
            let parent = options.body ? document.body : options.target;
            parent.appendChild(instance.$el);
            Vue.nextTick(() => {
                instance.visible = true;
            });
            // 将实例赋值给 fullscreenLoading
            if (options.fullscreen) {
                fullscreenLoading = instance;
            }
            return instance;
        }

        Vue.prototype.$loading = service

最后再验证一下

<div id="app">
        <!-- 因为loading组件是绝对定位,所以在扩展时会在父节点加上 el-loading-parent--relative-->
        <div id="loading" class="el-loading-parent--relative"></div>
    </div>

实例化

new Vue({
            el: '#app',
            data: function () {
                return {
                    visible: true
                }
            },
            mounted() {
                const loading = this.$loading({
                    lock: true,
                    text: 'Loading',
                    spinner: 'el-icon-loading',
                    background: 'rgba(0, 0, 0, 0.7)',
                    target: "#loading"
                })
                setTimeout(() => {
                    loading.close();
                }, 2000);
            }
        })

效果图如下

element-ui-loading-serve.gif

芜湖,添加 Vue 实例方法部分内容完成
关于自定义指令,详细的前置信息还是官方文档最全,传送门
接下来就是 elementui 中的源码了,为了更方便阅读,我也是做了一定的简化

import loadingVue from './loading.vue';
const Mask = Vue.extend(loadingVue);
Vue.directive('loading', {
    // 只调用一次 指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
    bind: function (el, binding, vnode) {
        const textExr = el.getAttribute('element-loading-text');
        const spinnerExr = el.getAttribute('element-loading-spinner');
        const backgroundExr = el.getAttribute('element-loading-background');
        const customClassExr = el.getAttribute('element-loading-custom-class');
        const vm = vnode.context;
        const mask = new Mask({
            el: document.createElement('div'),
            data: {
                text: vm && vm[textExr] || textExr,
                spinner: vm && vm[spinnerExr] || spinnerExr,
                background: vm && vm[backgroundExr] || backgroundExr,
                customClass: vm && vm[customClassExr] || customClassExr,
                fullscreen: !!binding.modifiers.fullscreen
            }
        });
        el.instance = mask;
        el.mask = mask.$el;
        el.maskStyle = {};

        binding.value && toggleLoading(el, binding);
    },

    update: function (el, binding) {
        el.instance.setText(el.getAttribute('element-loading-text'));
        if (binding.oldValue !== binding.value) {
            toggleLoading(el, binding);
        }
    },

    unbind: function (el, binding) {
        if (el.domInserted) {
            el.mask &&
                el.mask.parentNode &&
                el.mask.parentNode.removeChild(el.mask);
            toggleLoading(el, { value: false, modifiers: binding.modifiers });
        }
        el.instance && el.instance.$destroy();
    }
});
// 判断 loading 组件挂载的el
const toggleLoading = (el, binding) => {
    if (binding.value) {
        Vue.nextTick(() => {
            // 判断 fullscreen 如果有则绑在 body 上
            if (binding.modifiers.fullscreen) {
                insertDom(document.body, el, binding);
            } else {
                // 判断是否要绑定在 body 上
                if (binding.modifiers.body) {
                    insertDom(document.body, el, binding);
                } else {
                    insertDom(el, el, binding);
                }
            }
        });
    } else {
        afterLeave(el.instance, _ => {
            if (!el.instance.hiding) return;
            el.domVisible = false;
            el.instance.hiding = false;
        }, 300, true);
        el.instance.visible = false;
        el.instance.hiding = true;
    }
};
// 顾名思义 将DOM插入到文档中,并控制源码中 loading.vue 的显示和隐藏
const insertDom = (parent, el, binding) => {
    // 判断 domVisible 以及 el 中样式是否消失或隐藏
    if (!el.domVisible && el.style['display'] !== 'none' && el.style['visibility'] !== 'hidden') {
        Object.keys(el.maskStyle).forEach(property => {
            el.mask.style[property] = el.maskStyle[property];
        });

        el.domVisible = true;

        parent.appendChild(el.mask);
        Vue.nextTick(() => {
            if (el.instance.hiding) {
                el.instance.$emit('after-leave');
            } else {
                el.instance.visible = true;
            }
        });
        el.domInserted = true;
    } else if (el.domVisible && el.instance.hiding === true) {
        el.instance.visible = true;
        el.instance.hiding = false;
    }
};

添加自定义指令部分内容完成,更多详情见github
最后,源码中是将以上的代码包裹在 loadingDirective 这个对象中,然后在 loadingDirective 上面添加 install 方法,最终在外层 index 文件中通过vue.use调用,简化代码如下

const loadingDirective = {};
loadingDirective.install = Vue => {
// 上面的代码
}

下面是源码的简略图


loading组件源码结构分析.png

自此,loading 组件的源码分析全部结束

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容