【若川视野 x 源码共读】第28期 | vue message 组件

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

源码结构

本文选择的是 Element vue@2 版本的源码进行阅读。 从github clone源代码 github.com/ElemeFE/ele…

.
├── build -------------- 打包工具配置文件
├── examples ----------- ElementUI 组件示例
├── packages ----------- 组件源码
├── src ---------------- 入口文件以及工具方法
├── test --------------- 单元测试
└── types -------------- 声明文件,用于typescript
复制代码

可以看出来element源码结构还是比较简单和清晰的。

那么接下来进行依赖安装,将项目运行起来。

yarn //安装依赖
yarn dev //启动项目
复制代码

直接锁定 message 组件,源码文件位于 element/packages/message 目录下,测试文件位于 element/test/unit/specs/message.spec.js

源码解析

为什么能够使用 this.$message 调用 message

在 vue 中能够使用 this.xxx 进行全局方法调用的,基本上是在 Vue.prototype 进行方法或属性添加 。

查阅源码 element/src/index.js [:203] 可以发现:

题外话:有关该文件头部引入部分的,有一点建议。这里引入的文件很多,且没有排序,如果有排序在代码查找的时候会更加便捷,加上本人有强迫症。这里推荐一个 vscode 插件 Sort lines。经过排序之后的代码如下,你更喜欢哪一种呢

扫描二维码关注公众号,回复: 13751775 查看本文章

Message定义在 element/packages/message/src/main.js

Message方法实现

element/packages/message/src/main.js

删繁就简,保留message骨架部分,代码简化如下:

注意区分 Message 和 message !!!

  • 【8】当前 message 实例
  • 【9】message 实例集合
  • 【10】用于生成 message id
  • 【12】Message 的初始化方法
  • 【47】Message不同类型方法,可以通过 this.$message'success'/'waring'/'info'/'error' 的方式调用并指定type
  • 【62】Message.close方法,主要作用是更新 message 实例集合和改变指定id的message实例的top值,并在 message 实例的 onclose 中调用
  • 【85】Message.closeAll 方法,遍历 message 实例集合,并调用 message 实例的 close 方法,关闭所有 message 实例

【12】Message 的初始化方法

const Message = function(options) {
  if (Vue.prototype.$isServer) return; // 如果是服务端渲染则不执行
  options = options || {}; // 兼容options为空,也可以通过设置默认参数值达到该效果
  if (typeof options === 'string') { // 对直接传入字符串的参数类型转换成对象形式
    options = {
      message: options
    };
  }
  let userOnClose = options.onClose; // 获取 “关闭时的回调函数”
  let id = 'message_' + seed++; // 生成 message 实例 id,作用于 Message.close 查找指定的 message 实例

  options.onClose = function() { // 重写 onClose 方法
    Message.close(id, userOnClose);
  };
  instance = new MessageConstructor({ // 创建 message 实例(注意!!!,此时只是创建一个 vue 实例,并未完成渲染)
    data: options
  });
  instance.id = id; // 设置 id
  if (isVNode(instance.message)) { // 如果参数 message 是 VNode 类型,将其赋值到 instance.$slots.default,通过 slot 的方法进行渲染
    instance.$slots.default = [instance.message];
    instance.message = null;
  }
  instance.$mount(); // 生成实例真实节点
  document.body.appendChild(instance.$el); // dom 挂载到 body
  let verticalOffset = options.offset || 20; // 获取设置的偏移量,如果没有设置默认值 20。最终是设置 message 实例的 top 值
  instances.forEach(item => { // 遍历已经存在的 message 实例,根据元素的高度 + 间距(16)增加 top 值。可以看出来每增加一个 message ,它们之间的间距是 16
    verticalOffset += item.$el.offsetHeight + 16;
  });
  instance.verticalOffset = verticalOffset; // 设置最终计算所得的verticalOffset
  instance.visible = true; // 显示 message
  instance.$el.style.zIndex = PopupManager.nextZIndex(); // 通过全局的弹窗管理设置 z-index
  instances.push(instance); // 添加到message实例集合
  return instance; // 返回当前 message 实例
};
复制代码

【47】Message不同类型方法

['success', 'warning', 'info', 'error'].forEach(type => { // 遍历四种type类型
  Message[type] = (options) => { // 为 Message 添加类型方法,所以能够通过 this.$message.success 的方式进行调用
    if (isObject(options) && !isVNode(options)) { // 如果传入的 options 是对象类型 && 不是 VNode
      return Message({ // 返回指定 type 的 Message,
        ...options,
        type
      });
    }
    return Message({
      type,
      message: options
    });
  };
});
复制代码

【62】Message.close方法

Message.close = function(id, userOnClose) {
  let len = instances.length; // 获取当前存在的 message 实例个数
  let index = -1; // 索引
  let removedHeight; // 移除 message 元素的高度
  for (let i = 0; i < len; i++) { // 遍历 message 实例集合
    if (id === instances[i].id) { // 根据 id 查找对应的 message 实例
      removedHeight = instances[i].$el.offsetHeight; // 获取移除的 message 的高度
      index = i;
      if (typeof userOnClose === 'function') { // 如果设置了 userOnClose 回调,则调用该函数并传入 message 实例
        userOnClose(instances[i]);
      }
      instances.splice(i, 1); // message 实例集合中移除当前关闭的实例
      break;
    }
  }
  if (len <= 1 || index === -1 || index > instances.length - 1) return; // 如果只有一个 || 实例集合中不包含传入的id || 关闭的实例是最后一个;则不执行后续的修改top值的逻辑
  for (let i = index; i < len - 1 ; i++) { // 从关闭的实例开始,更新该实例之后的top值。目的是该 message 被移除之后,将之后的 message 上移至该位置。但此时当前实例并未销毁,还存在于页面上
    let dom = instances[i].$el;
    dom.style['top'] =
      parseInt(dom.style['top'], 10) - removedHeight - 16 + 'px';
  }
};
复制代码

可以通过该示例来体会 Message.close 方法

<template>
  <el-button :plain="true" @click="open">打开消息提示</el-button>
  <el-button :plain="true" @click="openVn">VNode</el-button>
  <el-button :plain="true" @click="close">close</el-button>
</template>

<script>
  export default {
    data() {
      return {
        message: [],
        open: (() => {
          let index = 0;
          const types = ["info", "success", "warning", "error"];
          return () => {
            const msg = this.$message({
              message: "this is a message",
              duration: 0,
              type: types[index++ % 4],
            });
            this.message.push(msg);
          };
        })(),
      };
    },
    methods: {
      // open() {
      //   this.$message("这是一条消息提示");
      // },
      openVn() {
        const h = this.$createElement;
        this.$message({
          message: h('p', null, [
            h('span', null, '内容可以是 '),
            h('i', { style: 'color: teal' }, 'VNode')
          ])
        });
      },
      close() {
        const index = 4
        this.message.length > index &&
          this.$message.close(
            this.message[this.message.length - index].id,
            (instance) => {
              console.log("instance: ", instance);
              this.message.splice(this.message.length - index, 1);
            }
          );
      },
    },
  };
</script>
复制代码

复制该代码到 element/examples/docs/zh-CN/message.md 即可预览。多次点击 “打开消息提示”按钮,生成多个 message,然后点击 “close”按钮,观察可以发 message 并未被销毁,而是改变了后续的 message 的位置。

【85】Message.closeAll 方法

Message.closeAll = function() {
  for (let i = instances.length - 1; i >= 0; i--) { // 遍历 message 实例集合,调用实例的close方法。注意不是 Message.close。该方法定义在 element/packages/message/src/main.vue
    instances[i].close();
  }
};
复制代码

message 组件实现

element/packages/message/src/main.vue

同样删繁就简,如图所示:

  • 【1-25】组件模板
  • 【28】type对应icon映射map
  • 【36】组件 data
  • 【55】typeClass 根据 type 计算所得类型,主要是根据 type 类型结合【28】映射关系定义不同 icon显示。这里也体现了 【28】定义映射关系的妙处,例如之后如果想改 icon 的类型,可以修改映射关系即可。
  • 【60】根据 verticalOffset 计算 top值
  • 【68】监听 closed 变化,如果为 true ,则设置 this.visible = false; v-show为false,元素隐藏。调用 transition 的 after-leave,即调用 【76】handleAfterLeave,执行组件销毁,并移除该dom节点。
  • 【76】执行组件销毁,并移除该dom节点
  • 【81】设置 this.close 为 true,=> 68 => 76。该方法也就是关闭当前 message 的 api
  • 【88】清除定时器
  • 【92】开启定时器。当设置 duration > 0 ,定时关闭
  • 【101】按键盘 esc 实现关闭
  • 【109】mounted 开启定时器,监听 keydown 事件
  • 【113】beforeDestroy 取消监听 keydown 事件

测试用例

element vue2 版本中使用的是 karma

message 的测试文件位于:element/test/unit/specs/message.spec.js

afterEach

afterEach:在执行完每一个测试案例之后,都会执行该方法。

  • 【6】查找class名为 el-message 的元素,即 message。
  • 【7】若不存在该节点,则返回
  • 【8】如果存在该节点的父节点,则从父节点中移除该节点
  • 【11】如果该节点上有__vue__属性,则调用 vue 实例的 destroy钩子

el.__vue__

这里算是一个技巧。可能平常很少有这么去写,通过真实节点来操作 vue 实例。顺带解释一下为什么能够通过真实节点的 __vue__属性访问 vue 实例。

vue@2 源码 src/core/instance/lifecycle.js中:

在 vue 中,初始化阶段会经历以下步骤:Vue.prototype.$mount -> mountComponent -> new Watcher -> vm._update -> vm.$el.__vue__ = vm

这也是为什么能通过真实dom 节点的__vue__ 访问 vm。感兴趣的可以阅读 vue2 相关源码。

剩余的测试用例在这里就不做展开了。

总结

实例集合

通过 instances 存储 message 实例集合,并且通过 id 自增的方式标记 message,方便在后续操作中进行查找。

闭包应用

在重写 onClose 方法的时候利用闭包对 id 有缓存作用。同时通过重写 options.onClose 方法对 Message 和组件进行关联,这样在后续的组件实例方法中调用 onClose 的时候,能够准确的在 instances 中找到该实例。

vm.$mount() 的妙用

通常情况下,在调用该方法的时候都会传入一个 el 进行挂载。但是这里并没有进行传入,只是为了生成真实dom节点到 vm.$el,在后续进行手动添加到 document.body

PopupManager

PopupManager 全局管理弹窗层级,有关 PopupManager 这里就不展开了

事件监听和销毁

这里我的个人习惯可能会将代码写成如下:

    mounted() {
      this.startTimer();
      document.addEventListener('keydown', this.keydown);
      this.$on("hook:beforeDestroy",()=>{
        document.removeEventListener('keydown', this.keydown);
      })
    },
复制代码

在 element vue3 版本中,更是将这里改成了 useEventListener:

__vue__

可以通过真实节点的 vue 访问 vm 实例。这一点在测试用例中得到应用。当然是我们实际开发中也是可以借鉴的。

猜你喜欢

转载自juejin.im/post/7080083555446947848