element-ui源码分析之notify通知组件

3,513 阅读3分钟

notify通知组件

在我们项目中,notify组件比较常用,像一些请求失败提示啊之类的,使用也非常简单,一句js就可以:

this.$notify({
  title: '提示',
  message: '成功',
  type: 'success'
})

分析

我们在使用notify的时候,通过this直接调用,所以可以想到,notify组件是应该挂载到vue的原型上的,在源码element-ui/src/index.js可以看到:

import Notification from '../packages/notification/index.js';
Vue.prototype.$notify = Notification;

另外notify是个函数,我们在使用的时候,调用一个函数,通过传入一个配置对象就能直接生成一个组件,并且你如果审查元素,会发现是挂载body元素最后面的,如图所示: 1 不难想到,源码中肯定有一句document.body.appendChild(el)
一开始我好奇,调用notify函数,传入一个对象就能生成一个组件,这种是怎么做到的,看源码的时候,渐渐有了一些理解,简化一下代码如下:

// packages/notification/src/main.js
import Vue from 'vue';
import Main from './main.vue';

const NotificationConstructor = Vue.extend(Main);

let instance;
const Notification = function(options) {
  instance = new NotificationConstructor({
    data: options
  });
  return instance;
}
export default Notification;

看着好像是我们通过this.$notify({})调用的时候,调用的是Notification方法,该方法通过NotificationConstructor构造函数new一个instance实例并返回,在顺着看实例对应的构造函数NotificationConstructor,此时就看到了其庐山真面目,Vue.extend(main)

extend

说实话,如果不是去查看notify组件源码,我可能一直不会去关注Vue.extend()这个api,有些东西,因为不知道,所以在开发遇到某些问题的时候,根本就想不到这个点。

既然element-ui源码中是通过这个实现的,那我们就看一下vue官网对于这个api的介绍
2 看着好像和Vue.component()第二个参数差不多,果然,在Vue.componentapi中发现这样一段话:

// 注册组件,传入一个扩展过的构造器
Vue.component('my-component', Vue.extend({ /* ... */ }))

// 注册组件,传入一个选项对象 (自动调用 Vue.extend)
Vue.component('my-component', { /* ... */ })

看到这里你应该明白了其作用,我在简单总结一下:

// Vue.extend返回一个扩展实例构造器,能帮你预先设定一些选项
let NotificationConstructor = Vue.extend({
  data() {
    return {
      name: 'aaa',
      age: 22
    }
  },
  // 这里可能会报错,提示我们Vue仅运行时版本不支持template,可将模板编译为渲染函数
  // 正好复习一下上节课讲的render函数
  render(h) {
    return h('h1', `姓名:${this.name}  年龄:${this.age}`)
  }
});

// 在使用的时候,直接new一个实例就行,并且可以传入配置对象去覆盖掉Vue.extend中预设的属性
let instance = new NotificationConstructor({
  data: {
    name: 'bbb'
  }
});
// 渲染结果
<h1>姓名:bbb 年龄:22</h1>

看完上面的,你应该完全理解Vue.extend()这个api的用法了。

Vue.extend除了上面那样传入一个对象外,也可以传入一个.vue组件,这样在写的时候,就更加方便了,不需要写render函数或者template模板。

下面来正式进入notify源码,探究其内部实现原理。

基本实现

notify组件源码位于element-ui/packages/notification/src/main.js,简化一下逻辑如下:

import Vue from 'vue';
import Main from './main.vue';
// 这里不用多说了吧,看完上面的你应该了解其作用了
const NotificationConstructor = Vue.extend(Main);

// 当前实例对象,创建在Notification函数外面,是因为在Notification外要用到。
let instance;
const Notification = function(options) {

  // 将我们传入的options拷贝一份。
  // 这里merge方法作用和Object.assign()相同,所以这里为了简化,就用Object.assign替代
  options = Object.assign({}, options);

  instance = new NotificationConstructor({
    data: options
  });
  // 挂载
  instance.$mount();
  // $el是组件生成的dom对象
  document.body.appendChild(instance.$el);
  return instance;
};

export default Notification;

main.js引用了main.vue组件,在来看main.vue的,同样我会简化一下代码:

<template>
  <!-- vue过渡动画,需要配合v-if或者v-show使用 -->
  <transition name="el-notification-fade">
    <div 
      :class="['el-notification']"
      v-show="visible"
    >
      <div class="el-notification__group">
        <h2 class="el-notification__title" v-text="title"></h2>
        <div class="el-notification__content" v-show="message">
          <slot>
            <p>{{ message }}</p>
          </slot>
        </div>
      </div>
    </div>
  </transition>
</template>

<script>

export default {
  data() {
    return {
      visible: false,
      title: '',
      message: ''
    };
  }
};
</script>

这里面的代码就比较简单了,没什么好说的。接下来测试,看我们调用this.$notify({ title: '提示', message: 'hello world' })看能否正常显示。 注意入口文件element-ui/src/index.js中需要引入组件,并且讲其挂载到vue的原型上。

演示代码

<template>
  <div>
    <el-button @click="handleClick">显示</el-button>
  </div>
</template>

<script>
export default {
  methods: {
    handleClick() {
      this.$notify({
        title: '提示',
        message: 'hello world',
        type: 'success'
      })
    }
  }
}
</script>

点击后发现页面毛也没有,审查元素后发现,组件以及追加到body后了,只是display设置为none,在main.jsdocument.body.appendChild(instance.$el)后添加一句:

instance.visible = true;

再次点击发现出现了,效果如下: 3 出来了,但是位置好像不太多,接下来我们调整一下位置。

position自定义弹框位置

仔细看文档关于position的介绍,是可以设置在页面四个角,默认是在右上角(top-right)。 审查元素看一下: 4 通过在style中添加top来控制垂直方向,添加right类来控制左右,下面分别来看看在源码中是如何实现的,在main.vue中添加如下代码: 5 6 查看效果: 7 现在就到了正确的位置,它这position的位置设计挺巧妙的,可以好好学习一下。

多个notify实例

我们每点击一次,就会弹出一个notify框,并且为之在上一个的下面,如图所示: 8 那这个是怎么实现的呢,每点击一次我们就会执行一遍Notification,生成一个弹框实例,并且挂载到body后面,这个很简单。难点是新生成的弹框位置如何设置。

还记得上面我们设置positionStyle的值的时候,源码中是获取verticalOffset的值,我们为了好理解就直接设置为了0,现在改回来,看看源码中是如何动态设置其值的。
你顺着verticalOffset往上找,发现main.js中有这么一段关于verticalOffset值的代码:

let instances = [];

const position = options.position || 'top-right';


// offset是用户设置的偏移距离,不知道的看一下element-ui的文档
let verticalOffset = options.offset || 0;
// instances是一个数组,用来存放当前所有生成的notify组件实例
instances.filter(item => item.position === position).forEach(item => {
  // 当前组件偏移的位置,不明白的可以看下图
  verticalOffset += item.$el.offsetHeight + 16;
});
verticalOffset += 16;
// 设置当前实例的偏移距离
instance.verticalOffset = verticalOffset;
// 将当前实例push进instances数组
instances.push(instance);

9main.vue中将positionStyle的值改回来,并在data中添加verticalOffset:0

close关闭

关闭步骤为以下三步:

1.调用removeChild()将其从dom中移除,并且销毁当前组件实例
2.删除instances数组中当前实例
3.将之后的notify弹框位置整体向上移一位

分别来看源码中如何实现:

// 1.调用removeChild()将其从dom中移除,并且销毁当前组件实例


// 点击关闭按钮是,调用了close方法
methods: {
  close() {
    // 设置closed为true,触发watch对closed的监听
    this.closed = true;
  },
  destroyElement() {
    this.$el.removeEventListener('transitionend', this.destroyElement);
    // 销毁组件
    this.$destroy(true);
    // 移除dom元素
    this.$el.parentNode.removeChild(this.$el);
  },
},
watch: {
  closed(newVal) {
    if (newVal) {
      this.visible = false;
      // transitionend 事件会在 CSS transition 结束后触发
      this.$el.addEventListener('transitionend', this.destroyElement);
    }
  }
}
// 2.删除instances数组中当前实例

// 移除dom元素后,调用onClose方法
const userOnClose = options.onClose;      // 用户自定义的关闭时的回调函数
options.onClose = function() {
  userOnClose
  Notification.close(id, userOnClose);
};

Notification.close = function(id, userOnClose) {
  let index = -1;
  const len = instances.length;
  // 通过id筛选出当前实例,并且获取其下标
  const instance = instances.filter((instance, i) => {
    if (instance.id === id) {
      index = i;
      return true;
    }
    return false;
  })[0];
  // 根据下标删除当前实例
  instances.splice(index, 1);

  // 3.将之后的notify弹框位置整体向上移一位
  const position = instance.position;
  // 获取当前要移除实例的高度
  const removedHeight = instance.dom.offsetHeight;
  // 循环instances数组,重新设置剩下的实例偏移量
  for (let i = index; i < len - 1 ; i++) {
    if (instances[i].position === position) {
      instances[i].dom.style[instance.verticalProperty] =
        parseInt(instances[i].dom.style[instance.verticalProperty], 10) - removedHeight - 16 + 'px';
    }
  }
  // 上面for循环简化一下如下
  for(let i = 1; i < instances.length; i++>) {
    // 判断位置是否和要移除的元素位置相同,如果是不同位置的,肯定就不需要移动了
    if(instances[i].position === instance.position) {
      // instances[i]的高度 - 要移除元素的高度 - 两者的间隙(16px)
      parseInt(instances[i].dom.style[instance.verticalProperty], 10) - removedHeight - 16 + 'px';
    }
  }
}

看到这里,你应该对notify的大致流程以及实现原理有一个大致的了解了,剩下的一些点相信你自己也能看懂了。element-ui有很多点处理的很细节,值得好好学习

本章节源码地址