[vue源码05] - Vue.extend

2,192 阅读4分钟

导航

[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数

[react] Hooks

[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程
[源码-vue03] watch 侦听属性 - 初始化和更新
[源码-vue04] Vue.set 和 vm.$set
[源码-vue05] Vue.extend

[源码-vue06] Vue.nextTick 和 vm.$nextTick

前置知识

一些单词

built-in tag:内置标签

reserved:保留
( tag is reserved so that it cannot be registered as a component 如果是保留标签,不能组件名 )

specification:规范
(  html5 specification HTML5规范 )

further:进一步
( allow further extension/mixin/plugin usage 允许进一步扩展... )

(1) 组件注册

  • 全局注册 和 局部注册
  • (1) 全局注册
    • Vue.component( id, [definition] )
      • 参数
        • id:string类型,可以是 MyComponent 或 my-component
        • definition:可选,函数或对象
          • data 必须是函数
          • 不包含 el
      • 作用
        • 注册 或 获取 全局组件
        • 全局注册的组件能在
      • 注意点:
        • definition 对象中的 ( data ) 必须是 ( 函数 ),这样每个组件实例才能维护一份返回对象的独立拷贝
        • id 可以是 MyComponent 或 my-component 这两种写法的字符串
        • 全局注册的组件可以供所有子组件使用
  • (2) 局部注册
    • 在new Vue()的参数对象中通过 components 属性对象进行局部注册
  • 案例123
1. 
// 注册组件,传入一个扩展过的构造器
Vue.component('my-component', Vue.extend({ /* ... */ }))

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

// 获取注册的组件 (始终返回构造器)
var MyComponent = Vue.component('my-component')



--------------
2. 
Base/BaseButton.ts
// 全局注册组件 
// 1. 这里是 ts 文件
// 2. 如果是 .vue 文件可以使用 webpack 的 require.context
Vue.component('BaseButton', {
  data() {
    return {
      message: '这是一个基础组件-button'
    }
  },
  template: `
  <div>
    <div>BaseButton</div>
    <div>{{message}}</div>
  </div>
`,
})
// 注意:在vue-cli3中需要在vue.config.js中配置 ( runtimeCompiler: true ) 表示开启runtime+compiler版本
// vue.config.js
module.exports = {  
  runtimeCompiler: true,  // runtime + compiler 版本
}


--------------
3. 
Base
    / BaseButton.ts ------------------ 简单的 Vue.component 全局注册 BaseButton 组件
    / BaseButton.vue ----------------- 利用 require.context 实现 Base 文件夹中的所有组件的自动化全局注册
    / index.ts ----------------------- 自动化全局注册逻辑
    
index.ts如下
--
import Vue from 'vue'
const requireContext = require.context('.', false, /\.vue$/)
requireContext.keys().forEach(fileName => {
  const componentModule = requireContext(fileName)
  const component = componentModule.default
  Vue.component(component.name, component)
})

require.context在vue中的使用官网案例: cn.vuejs.org/v2/guide/co…

(2) Vue.extend( options ) - api

  • 参数
    • options:一个包含组件选项对象
    • 注意:
      • options.data 必须是一个函数
      • Vue.component()Vue.extend() 的参数对象中的 data 都必须是一个 函数
  • 用法
    • 使用基础的 Vue 构造器,创建一个子类
  • 案例 ( 封装一个全局基础toast组件 )
    • toast是一个基础组件,多个地方会用到,所以不要在每个用到的组件中import再在components中注册,而是挂在到vue.prototype上
    • toast的组件不放在vue项目的DOM根节点中,因为会受到路由的影响,而是独立的节点
Base全局基础组件
目录结构
src
    /components
        /base
            / index.js
            / toast.vue
  • src/components/base/toast.vue
第一步:
1. 正常的写一个展示的toast组件
2. toast组件中的data可以通过Vue.extend生成的子类的实例的参数对象中的data来修改
[src/components/base/toast.vue]
<template>
  <div
    class="base-toast"
    v-if="show"
    :class="[animateFn, backgrondType]"
  >{{message}}</div>
</template>
<script>
export default {
  name: "BaseToast",
  data() {
    return {
      message: "", // toast显示内容
      show: true, // 显示隐藏
      fade: true, // 显示隐藏动画
      type: "",
      typeArr: ['error', 'success']
    };
  },
  computed: {
    backgrondType() {
        return 'toast-' + this.typeArr.find(type)  
    },
    animateFn() {
        return this.fade ? 'fadein' : 'fadeout'
    }
  }
};
</script>
<style lang="css">
.base-toast {
  padding: 10px;
  background: rgba(0, 0, 0, 0.5);
  position: absolute;
  left: 50%;
  top: 10px;
  transform: translate(-50%, 0);
  display: inline-block;
  margin: 0 auto;
  text-align: center;
}
.fadein {
  animation: animation_fade_in 0.5s;
}
.fadeout {
  animation: animation_fade_out 0.5s;
}
@keyframes animation_fade_in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
@keyframes animation_fade_out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}
.toast-success {
  background: green;
}
.toast-error {
  background: red;
}
</style>
  • src/components/base/index.js
---
第二步:
[src/components/base/index.js]

import Vue from "vue";
import Toast from "./toast.vue";

const generatorToast = ({ message, type, duration = 200 }) => {
  const ToastConstructor = Vue.extend(Toast); // ------------------- Vue.extend()生成Vue子类
  const toastInstance = new ToastConstructor({ // ------------------ new子类,生成组件实例
    el: document.createElement("div"), // -------------------------- 组件挂在节点
    data() { // ---------------------------------------------------- 将和 Toast 组件中的 data 合并
      return {
        message,
        type,
        show: true,
        fade: true,
      };
    },
  });
  setTimeout(() => {
    toastInstance.fade = false; // -------------------------------- 动画,提前执行
  }, duration - 500);
  setTimeout(() => {
    toastInstance.show = false; // -------------------------------- 显示隐藏
  }, duration);
  document.body.appendChild(toastInstance.$el); // ---------------- 组件挂在位置
};


export default { // ----------------------------------------------- 插件对象的install方法
  install() {
    Vue.prototype.$BaseToast = generatorToast;
  },
};
// Vue.use(option)
    // option可以是 函数 或者 具有 install方法的对象
    // 这里将 toast/index封装成vue插件,通过Vue.use()注册,即执行install方法
  • src/main.js入口文件
第三步:
[src/main.js入口文件]
1. 引入src/components/base/index.js
2. Vue.use()注册插件

import Vue from 'vue'
import App from './App.vue'
import Toast from './components/base'

Vue.config.productionTip = false
Vue.use(Toast) // ----------------------------------------------- vue插件注册

new Vue({
  render: h => h(App),
}).$mount('#app')

  • src/App.vue
第四步:
[src/App.vue]

1. 使用
<template>
  <div id="app">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
  name: 'App',
  components: {
    HelloWorld
  },
  mounted() {
    this.$BaseToast({ // --------------------------------------- 通过 this.$BaseToast() 调用
      message: '111',
      duration: 3000,
      type: 'error'
    })
  }
}
</script>

Vue.extend() 源码

  • 一句话总结:使用 基础Vue构造器,创建一个子类
Vue.extend = function (extendOptions) {
  extendOptions = extendOptions || {}; // 没有传参,就赋值空对象
  var Super = this; // this指的是Vue
  var SuperId = Super.cid; // SuperId => id

  var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
  // cachedCtors
  // 用来缓存 Constructor
  // 参数对象中不存在 _Ctor 属性,就将 extendOptions._Ctor = {} 赋值为空对象
  if (cachedCtors[SuperId]) {
    // 存在缓存,直接返回
    return cachedCtors[SuperId]
  }

  var name = extendOptions.name || Super.options.name;
  // name
  // 参数对象中不存在 name 属性,就是用父类的options的name属性

  if (name) {
    validateComponentName(name);
    // validateComponentName() 验证 name 的合法性
    // 1. 不能是 slot component 这样的内置标签名
    // 2. 不能是 HTML5 的保留关键字标签
  }

  var Sub = function VueComponent(options) { // 定义子类
    this._init(options);
  };
  Sub.prototype = Object.create(Super.prototype);
  // 将 ( 子类的prototype的原型 ) 指向 ( 父类prototype )
  // 这样 ( 子类的实例 ) 就能继承 ( 父类prototype ) 上的属性和方法

  Sub.prototype.constructor = Sub;
  // 将原型上的constructor属性指向自己,防止修改了原型后 prototype.constructor 不再是指向 Sub

  Sub.cid = cid++;
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  );
  // 合并options => 将父类的options和参数对象合并

  Sub['super'] = Super;
  // 在子类上挂载 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.
  if (Sub.options.props) {
    initProps$1(Sub);
    // props属性存在,就将props做一层代理
    // initProps方法可以让用户访问this[propName]时相当于访问this._props[propName]
  }
  if (Sub.options.computed) {
    initComputed$1(Sub);
    // 同上
  }

  // allow further extension/mixin/plugin usage
  // 继承相关属性
  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.forEach(function (type) {
    Sub[type] = Super[type];
  });
  // 继承 component directive filter
    // var ASSET_TYPES = [
    //   'component',
    //   'directive',
    //   'filter'
    // ];

  // enable recursive self-lookup
  if (name) {
    Sub.options.components[name] = Sub;
    // 保存Sub到components属性中
  }

  Sub.superOptions = Super.options;
  Sub.extendOptions = extendOptions;
  Sub.sealedOptions = extend({}, Sub.options);
  

  // cache constructor
  cachedCtors[SuperId] = Sub; //存在Sub
  
  return Sub
  // 返回 Sub
};

资料

Vue.extend源码 juejin.cn/post/684490…
Vue.extend源码 zhuanlan.zhihu.com/p/121799032
toast组件1 juejin.cn/post/684490…
toast组件2 juejin.cn/post/684490…