This article participated in the weekly reading activity of source code jointly initiated by the public account @Ruochuan Vision.Click to learn more about participating.

Source structure

This article selects Element vue@2 version of the source code for reading. Source code from Github Clone github.com/ElemeFE/ele… .

. ├ ─ ─ build -- -- -- -- -- -- -- -- -- -- -- -- -- -- packing tool configuration file ├ ─ ─ examples -- -- -- -- -- -- -- -- -- -- - ElementUI component sample ├ ─ ─ packages -- -- -- -- -- -- -- -- -- -- - component source ├ ─ ─ the SRC -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - entry documents as well as the method of tool ├ ─ ─ the test -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- unit test └ ─ ─ types -- -- -- -- -- -- -- -- -- -- -- -- -- -- declaration file, used in the typescriptCopy the code

It can be seen that element’s source code structure is relatively simple and clear.

Next, do the dependency installation to get the project up and running.

Yarn // Installation depends on yarn dev/to start the projectCopy the code

Directly lock the message components, source code files in the element/packages/message directory, the test file is located in the element/test/unit/specs/message. Spec. Js

The source code parsing

Why can I call message with this.$message

The only global method calls you can make in Vue using this. XXX are basically method or attribute additions in Vue.prototype.

Element/SRC /index.js [:203]

Off-topic: a few suggestions for the introduction of the header of the file. There are a lot of files introduced here, and there is no sorting, if there is sorting in the code to find more convenient, plus I have obsessive-compulsive disorder. A vscode plug-in, Sort lines, is recommended. The sorted code looks like this. Which one do you prefer

Message is defined in the element/packages/Message/SRC/main js

Message method implementation

element/packages/message/src/main.js

The message skeleton is preserved and the code is simplified as follows:

Distinguish between Message and Message!!

  • [8] Current Message instance
  • [9] Message instance collection
  • [10] Used to generate message IDS
  • [12] Message initialization method
  • [47] Message: $this.$Message ‘success’/’waring’/’info’/’error’
  • [62] Message.close method, mainly used to update the set of Message instances and change the top value of the specified Message instance, and called in the onclose of the Message instance
  • [85] The message. closeAll method iterates through the collection of Message instances and calls the close method of Message instances to close all Message instances

[12] Message initialization method

const Message = function(options) {
  if (Vue.prototype.$isServer) return; // If it is server-side rendering, it is not executed
  options = options || {}; // If options are empty, you can also set the default parameters to achieve this effect
  if (typeof options === 'string') { // Convert an argument type passed directly to a string to an object
    options = {
      message: options
    };
  }
  let userOnClose = options.onClose; // Get "closed callback function"
  let id = 'message_' + seed++; // Generates the message instance ID, applied to message. close to find the specified message instance

  options.onClose = function() { // Override the onClose method
    Message.close(id, userOnClose);
  };
  instance = new MessageConstructor({ // Create a message instance (note!! At this point, only a vue instance is created, rendering is not complete.
    data: options
  });
  instance.id = id; / / set id
  if (isVNode(instance.message)) { // If the argument message is of type VNode, assign it to instance.$slots.default and render it through slot
    instance.$slots.default = [instance.message];
    instance.message = null;
  }
  instance.$mount(); // Generate the actual node of the instance
  document.body.appendChild(instance.$el); // dom is mounted to body
  let verticalOffset = options.offset || 20; // Get the set offset, if not the default value 20. Finally, set the top value of the Message instance
  instances.forEach(item= > { // Iterate over existing message instances, increasing the top value based on the height of the elements + spacing (16). You can see that for each additional message, the space between them is 16
    verticalOffset += item.$el.offsetHeight + 16;
  });
  instance.verticalOffset = verticalOffset; // Set the final calculated verticalOffset
  instance.visible = true; / / display the message
  instance.$el.style.zIndex = PopupManager.nextZIndex(); // Set z-index via global popover management
  instances.push(instance); // Add to the collection of message instances
  return instance; Return the current message instance
};
Copy the code

[47] Different types of Message methods

['success'.'warning'.'info'.'error'].forEach(type= > { // Iterate over the four types of type
  Message[type] = (options) = > { // Add a type method for Message, so it can be called as this.$message.success
    if(isObject(options) && ! isVNode(options)) {// If options is passed as object type && not VNode
      return Message({ // Return Message of the specified type,. options, type }); }return Message({
      type,
      message: options
    });
  };
});
Copy the code

[62] message.close method

Message.close = function(id, userOnClose) {
  let len = instances.length; // Get the number of existing message instances
  let index = -1; / / index
  let removedHeight; // Remove the height of message element
  for (let i = 0; i < len; i++) { // Iterate through the collection of message instances
    if (id === instances[i].id) { // Find the corresponding message instance by id
      removedHeight = instances[i].$el.offsetHeight; // Get the height of the removed message
      index = i;
      if (typeof userOnClose === 'function') { // If the userOnClose callback is set, the function is called and the message instance is passed
        userOnClose(instances[i]);
      }
      instances.splice(i, 1); // Remove the currently closed instance from the message instance collection
      break; }}if (len <= 1 || index === -1 || index > instances.length - 1) return; / / if only one | | instances in the collection does not contain the incoming id | | to shut down the instance is the last one; The subsequent logic to change the top value is not performed
  for (let i = index; i < len - 1 ; i++) { // Start with the closed instance and update the top value after that instance. The purpose is to move subsequent messages up to that location after the message is removed. However, the current instance is not destroyed and still exists on the page
    let dom = instances[i].$el;
    dom.style['top'] =
      parseInt(dom.style['top'].10) - removedHeight - 16 + 'px'; }};Copy the code

You can use this example to get a feel for the message.close method

<template>
  <el-button :plain="true" @click="open">Open message prompt</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(" this is a message ");
      // },
      openVn() {
        const h = this.$createElement;
        this.$message({
          message: h('p'.null, [
            h('span'.null.'Content can be'),
            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>
Copy the code

Copy this code to Element /examples/docs/ zh-cn /message.md to preview. Click the “Open message prompt” button several times to generate multiple messages, and then click the “Close” button to observe that messages can be sent without being destroyed, but the position of subsequent messages has been changed.

[85] message. closeAll method

Message.closeAll = function() {
  for (let i = instances.length - 1; i >= 0; i--) { // Iterate over the collection of message instances, calling the instance's close method. Note not message.close. The method to define the element/packages/message/SRC/main vueinstances[i].close(); }};Copy the code

Message component implementation

element/packages/message/src/main.vue

The same simplification is shown in the picture:

  • [1-25] Component template
  • [28] Type corresponds to icon mapping map
  • [36] Component Data
  • [55] typeClass is the type calculated according to type, mainly defining different icon displays according to type type and mapping relationship [28]. This also reflects the beauty of [28] defining mapping relations. For example, if you want to change the type of icon later, you can modify the mapping relations.
  • [60] Calculate the top value according to verticalOffset
  • [68] Listen for closed changes, if true, set this.visible = false; V-show is false and the element is hidden. Call transition after-leave, i.e. [76] handleAfterLeave, to perform component destruction and remove the DOM node.
  • [76] Perform component destruction and remove the DOM node
  • [81] Set this.close to true, => 68 => 76. This method simply closes the current MESSAGE API
  • [88] Clear timer
  • [92] Start the timer. When duration > 0 is set, the timer is closed
  • [101] Press esc to close
  • [109] Mounted Enables the timer to listen to keyDown events
  • [113] beforeDestroy Cancels listening for keyDown events

The test case

The Version of Element VU2 uses Karma.

Test file is located in the message: element/test/unit/specs/message. Spec. Js

afterEach

AfterEach: this method is executed afterEach test case is executed.

  • [6] Find the element whose class name is el-message.
  • [7] If the node does not exist, return
  • [8] If there is a parent node of the node, remove the node from the parent node
  • [11] If the node has__vue__Property to call the vue instancedestroyhook

el.__vue__

Here’s a trick. It may not be common to write this, manipulating vue instances through real nodes. By the way, explain why a Vue instance can be accessed through the __vue__ attribute of a real node.

In vue @ 2 source SRC/core/instance/lifecycle. In js:

Prototype.$mount -> mountComponent -> new Watcher -> vm._update -> vm.$el.__vue__ = vm

This is why the VM can be accessed through the __vue__ of the real DOM node. Those interested can read the vue2 source code.

The rest of the test cases will not be expanded here.

conclusion

Examples of collections

Instances store the collection of Message instances, and mark messages with self-increasing ids to facilitate searching in subsequent operations.

Closure application

Use closures to cache ids when overriding the onClose method. At the same time, we can rewrite the options.onClose method to associate Message with the component, so that when onClose is called in subsequent component instance methods, the instance can be accurately found in the Instances.

The use of vm. $mount ()

Normally, this method is called with an EL to mount. But there is no passing in here, just to generate the actual DOM node to vm.$el, which will be added manually to document.body later

PopupManager

PopupManager global popup management level, PopupManager will not be expanded here

Event listening and destruction

My personal habit might be to write code like this:

mounted() { this.startTimer(); document.addEventListener('keydown', this.keydown); this.$on("hook:beforeDestroy",()=>{ document.removeEventListener('keydown', this.keydown); })},Copy the code

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 test cases. Of course, it can be used for reference in our actual development.