[Wakagawa Vision x Source Code Reading] Issue 28 | vue message component

This article participated in the weekly source code reading activity initiated by the public account @ Ruo Chuan Vision, click for details to participate together.

source code structure

This article selects the source code of the Element vue@2 version for reading. From github clone the source code github.com/ElemeFE/ele… .

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

It can be seen that the source code structure of element is relatively simple and clear.

Then install the dependencies and run the project.

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

Directly lock the message component, the source code file is located in the element/packages/message directory, and the test file is located in element/test/unit/specs/message.spec.js

Source code analysis

Why can I call message with this.$message

In vue, you can use this.xxx for global method calls, basically adding methods or properties in Vue.prototype.

Check the source code element/src/index.js [:203] to find:

Off topic: There is a little suggestion about the import part of the header of this file. There are many files introduced here, and they are not sorted. If there is sorting, it will be more convenient to find the code, and I have obsessive-compulsive disorder. A vscode plugin Sort lines is recommended here. The sorted code is as follows, which one do you prefer?

Message is defined in element/packages/message/src/main.js

Message method implementation

element/packages/message/src/main.js

Simplify the complicated and keep the message skeleton part. The code is simplified as follows:

Note the distinction between Message and message! ! !

  • [8] Current message instance
  • [9] message instance collection
  • [10] Used to generate message id
  • [12] Initialization method of 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

__vue__ This is why the vm can be accessed through the real dom node . If you are interested, you can read the relevant source code of vue2.

The remaining test cases are not expanded here.

Summarize

instance collection

The set of message instances is stored through instances, and the message is marked with an auto-increment id, which is convenient for searching in subsequent operations.

Closure application

When overriding the onClose method, the closure is used to cache the id. At the same time, by overriding the options.onClose method, the Message and the component are associated, so that when onClose is called in the subsequent component instance method, the instance can be accurately found in the instances.

The magic of vm.$mount()

Usually, when calling this method, an el is passed in for mounting. But there is no input here, just to generate the real dom node to vm.$el, and manually add it to document.body later

PopupManager

PopupManager manages the pop-up window level globally, the PopupManager will not be expanded here

Event monitoring and destruction

My personal habit here might be to write the code as follows:

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

In the element vue3 version, this is changed to useEventListener:

__vue__

The vm instance can be accessed through the vue of the real node . This is applied in the test case. Of course, we can also learn from our actual development.

Guess you like

Origin juejin.im/post/7080083555446947848