vue2.6源码学习之vue.extend的实现

1,817 阅读1分钟

介绍

vue.extend是一个全局的api,生成的是vue的子类是实现编程式组件的一个重要途径。在阅读源码时我们要带着几个问题:

  • vue.extend如何使用?
  • vue.extend常用的使用场景有哪些?
  • vue.extend内部原理是怎样实现的?
  • 通过vue.extend实现一个编程式组件

基本用法

探究一个API或者一个方法的实现原理时首先我们必须要知道这个API如何使用,才能更好的探究他的实现原理。

参数

{Object} options

用法

使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

data 选项是特例,需要注意 - 在 Vue.extend() 中它必须是函数。

<div id="mount-point"></div>
// 创建构造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')

结果

<p>Walter White aka Heisenberg</p>

使用场景

在element-ui(以下名称用el代替)中我们可以通过如下方式使用组件


//message组件
this.$message.success("提交成功");

//弹窗
  this.$alert('这是一段内容', '标题名称', {
          confirmButtonText: '确定',
          callback: action => {
            this.$message({
              type: 'info',
              message: `action: ${ action }`
            });
          }
        });
        
//通知组件
 this.$notify({
          title: '成功',
          message: '这是一条成功的提示消息',
          type: 'success'
        });

el组件是怎么做到可以通过vue的实例就可以调用组件的呢?用message组件为例:

//引入组件
import Message from '../packages/message/index.js';
//把组件挂载到vue的原型对象上
Vue.prototype.$message = Message;

Message内部又是怎么实现可以通过JS调用去渲染组件的呢?我会去掉暂时不用的代码,让我们更方便的阅读

//引入普通组件
import Main from './main.vue';
//创建vue的子类
let MessageConstructor = Vue.extend(Main);

    ...
    //实例化组件
    instance = new MessageConstructor({
    data: options
  });
  //渲染组件
    instance.$mount();
    //挂载组件
  document.body.appendChild(instance.$el);
  ...
   return instance;
}
...

通过代码可知 vue.extend和mount()方法是实现JS调用渲染组件的精髓所在。所以当你也想在vue中使用JS调用去渲染组件就使用它吧

内部实现

当我们知道了他的使用方式和使用场景,那他是怎么实现的呢?传入一个配置为什么可以生成vue的子类?我先把全部的代码贴上去,我会通过备注的方式分析。

    Vue.extend = function (extendOptions) {
      extendOptions = extendOptions || {};//传入了参数使用传入的如果没穿使用{}
      var Super = this; //这里指向vue
      var SuperId = Super.cid;//用作缓存
      
      //如果传入的对象参数不存在_Ctor属性,extend会给他添加上这个属性,用于缓存。
      //如果存在这个属性,并且这个对象上存在SuperId这个键的话就会直接返回缓存过的构造函数
      var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}); 
      if (cachedCtors[SuperId]) {
        return cachedCtors[SuperId]
      }
      //校验name名称是否符合规则
      var name = extendOptions.name || Super.options.name;
      if (name) {
        validateComponentName(name); 
      }
        //这就是子类的构造函数
      var Sub = function VueComponent (options) {
        this._init(options);
      };
      Sub.prototype = Object.create(Super.prototype); //继承vue的原型方法
      Sub.prototype.constructor = Sub; //把constructor的指向自己
      Sub.cid = cid++;//Sub添加一个自己的属性cid用于缓存
      Sub.options = mergeOptions(  //合并配置
        Super.options,
        extendOptions
      );
      Sub['super'] = Super; //把父类(也就是vue)挂载到super属性方便以后使用

      // For props and computed properties, we define the proxy getters on
      // the Vue instances at extension time, on the extended prototype. This
      // avoids Object.defineProperty calls for each instance created.
      //判断是否有props属性如果有就初始化props
      if (Sub.options.props) {
        initProps$1(Sub);
      }
      //判断是否有computed属性如果有就初始化computed
      if (Sub.options.computed) {
        initComputed$1(Sub);
      }

      // allow further extension/mixin/plugin usage
      //把父类的extend mixin use 方法挂载到Sub上允许他进一步扩展
      Sub.extend = Super.extend;
      Sub.mixin = Super.mixin;
      Sub.use = Super.use;

      // create asset registers, so extended classes
      // can have their private assets too.
      
      //ASSET_TYPES是数组['component','directive','filter']
      //把super(父类)的component,directive,filter 方法挂载到sub(子类)上
      //使sub具有注册组件 ,注册指令,注册自定义过滤器
      ASSET_TYPES.forEach(function (type) {
        Sub[type] = Super[type];
      });
      // enable recursive self-lookup
      
      //name有值的时候将自己存到options.components[name]上
      if (name) {
        Sub.options.components[name] = Sub;
      }

      // keep a reference to the super options at extension time.
      // later at instantiation we can check if Super's options have
      // been updated.
      
      //把父类的配置,传入的配置和合并的配置挂载到sub(子类)上
      //方便以后检查options是否有更新。
      Sub.superOptions = Super.options;
      Sub.extendOptions = extendOptions;
      Sub.sealedOptions = extend({}, Sub.options);

      // cache constructor
      //把自己缓存到Sub.options._Ctor对象上并返回Sub
      cachedCtors[SuperId] = Sub;
      return Sub
    };

实现编程式组件

那自己应该怎么通过vue.extend实现一个编程式组件呢?下面我们自己实现

this.$AFMessage.success("成功")
this.$AFMessage({
    type:"success",
    message:"成功"
})

这两种方式使用组件。

创建组件(用于编程式调用)

<template>
  <div :class="messageClass" v-if="!closed">
    <i :class="typeClass"></i>
    <p class="af-message__content">
      {{ message }}
    </p>
  </div>
</template>
<script>
const typeMap = {
  success: 'success',
  info: 'info',
  warning: 'warning',
  error: 'error'
};
export default {
  name: 'af-message',
  data() {
    return {
      message: '测试信息', //显示信息
      type: 'success', //显示类型
      closed: false, //是否显示
      duration: 3000, //自动消失时间为0时不消失
      timer: ''
    };
  },
  computed: {
    typeClass() {
      return this.type ? `af-message__icon af-icon-${typeMap[this.type]}` : '';
    },
    messageClass() {
      return this.type ? `af-message af-message--${this.type}` : `af-message`;
    }
  },
  created() {
    this.startTimer();
  },
  methods: {
    close() {
      this.closed = true;
    },
    startTimer() {
      if (this.duration > 0) {
        this.timer = setTimeout(() => {
          if (!this.closed) {
            this.close();
            clearTimeout(this.timer);
          }
        }, this.duration);
      }
    }
  }
};
</script>
<style lang="scss">
.af-message {
  border-radius: 4px;
  overflow: hidden;
  min-width: 380px;
  box-sizing: border-box;
  border-width: 1px;
  border-style: solid;
  border-color: #ebeef5;
  position: fixed;
  left: 50%;
  top: 20px;
  transform: translateX(-50%);
  background-color: #edf2fc;
  transition: opacity 0.3s, transform 0.4s, top 0.4s;
  padding: 15px 15px 15px 20px;
  display: flex;
  align-items: center;
  .af-message__content {
    padding: 0;
    font-size: 14px;
    line-height: 1;
    margin: 0;
  }
}
.af-message--success {
  background-color: #f0f9eb;
  border-color: #e1f3d8;
  .af-message__content {
    color: #67c23a;
  }
}
</style>

这块没什么要说的,主要实现组件的显示。组件的样式我是模仿element组件写的。下面是重点

实现编程式

import vue from 'vue';
import afMessage from './message.vue';
//创建构造器
const AFMessage = vue.extend(afMessage);
function messgae(option) {
  option = option || {};
  if (option === 'string') {
    option = { messgae: option };
  }
  //生成你导入的组件
  const instance = new AFMessage({
    data: option
  });
  //挂载组件
  instance.$mount();
  document.body.appendChild(instance.$el);
  return instance;
}
//支持this.$AFMessage.success()调用
['success', 'error', 'info'].map((itemType) => {
  messgae[itemType] = function(options) {
    if (typeof options === 'string') {
      options = {
        message: options
      };
    }
    options.type = itemType;
    return messgae(options);
  };
});
export default messgae;

注册

main.js

import Vue from 'vue';
import AFmessage from '@/common/components/afMessge';
Vue.prototype.$AFMessage = AFmessage;

使用

this.$AFMessage({
    type:"success",
    message:"成功"
})

这样就可以在项目中愉快的使用了。

总结

vue.extend说到底就是创建vue的子类。