1️⃣ 前言

Vue.extend 是 Vue 里的一个全局 API,它提供了一种灵活的挂载组件的方式,这个 API 在日常开发中很少使用,毕竟只在碰到某些特殊的需求时它才能派上用场( 比如全局的弹窗提示组件 )

1️⃣ Vue.extend 定义

官方定义
image.png

1️⃣ 源码分析

  1. export function initExtend(Vue: GlobalAPI) {
  2. // 这个cid是一个全局唯一的递增的id
  3. // 缓存的时候会用到它
  4. Vue.cid = 0
  5. let cid = 1
  6. /**
  7. * 类继承
  8. */
  9. Vue.extend = function(extendOptions: Object): Function {
  10. // extendOptions 就是我我们传入的组件options
  11. extendOptions = extendOptions || {}
  12. const Super = this
  13. const SuperId = Super.cid
  14. // 每次创建完 Sub 构造函数后,都会把这个函数储存在 extendOptions 上的 _Ctor 中
  15. // 下次如果用再同一个 extendOptions 创建 Sub 时
  16. // 就会直接从 _Ctor 返回
  17. const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  18. if (cachedCtors[SuperId]) {
  19. return cachedCtors[SuperId]
  20. }
  21. const name = extendOptions.name || Super.options.name
  22. if (process.env.NODE_ENV !== 'production' && name) {
  23. validateComponentName(name)
  24. }
  25. // 创建 Sub 构造函数
  26. const Sub = function VueComponent(options) {
  27. this._init(options)
  28. }
  29. // 继承 Super,如果使用 Vue.extend,这里的 Super 就是 Vue
  30. Sub.prototype = Object.create(Super.prototype)
  31. Sub.prototype.constructor = Sub
  32. Sub.cid = cid++
  33. // 将组件的 options 和 Vue 的 options 合并,得到一个完整的 options
  34. // 可以理解为将 Vue 的一些全局的属性,比如全局注册的组件和 mixin,分给了 Sub
  35. Sub.options = mergeOptions(Super.options, extendOptions)
  36. Sub['super'] = Super
  37. // 下面两个设置了下代理,
  38. // 将 props 和 computed 代理到了原型上
  39. // 你可以不用关心这个
  40. if (Sub.options.props) {
  41. initProps(Sub)
  42. }
  43. if (Sub.options.computed) {
  44. initComputed(Sub)
  45. }
  46. // 继承 Vue 的 global-api
  47. Sub.extend = Super.extend
  48. Sub.mixin = Super.mixin
  49. Sub.use = Super.use
  50. // 继承 assets 的 api,比如注册组件,指令,过滤器
  51. ASSET_TYPES.forEach(function(type) {
  52. Sub[type] = Super[type]
  53. })
  54. // 在 components 里添加一个自己
  55. // 不是主要逻辑,可以先不管
  56. if (name) {
  57. Sub.options.components[name] = Sub
  58. }
  59. // 将这些 options 保存起来
  60. // 一会创建实例的时候会用到
  61. Sub.superOptions = Super.options
  62. Sub.extendOptions = extendOptions
  63. Sub.sealedOptions = extend({}, Sub.options)
  64. // 设置缓存
  65. // 就是上文的缓存
  66. cachedCtors[SuperId] = Sub
  67. return Sub
  68. }
  69. }
  70. function initProps(Comp) {
  71. const props = Comp.options.props
  72. for (const key in props) {
  73. proxy(Comp.prototype, `_props`, key)
  74. }
  75. }
  76. function initComputed(Comp) {
  77. const computed = Comp.options.computed
  78. for (const key in computed) {
  79. defineComputed(Comp.prototype, key, computed[key])
  80. }
  81. }
  1. 其实这个 Vue.extend 做的事情很简单,就是继承 Vue,正如定义中说的那样,创建一个子类.<br />最终返回的这个 Sub 是:
  1. const Sub = function VueComponent(options) {
  2. this._init(options)
  3. }
  1. 那么上文的例子中的 new Profile() 执行的就是这个方法了,因为继承了 Vue 的原型,这里的 _init 就是 Vue 原型上的 _init 方法
  1. Vue.prototype._init = function(options?: Object) {
  2. const vm: Component = this
  3. // a uid
  4. vm._uid = uid++
  5. let startTag, endTag
  6. /* istanbul ignore if */
  7. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  8. startTag = `vue-perf-start:${vm._uid}`
  9. endTag = `vue-perf-end:${vm._uid}`
  10. mark(startTag)
  11. }
  12. vm._isVue = true
  13. // merge options
  14. if (options && options._isComponent) {
  15. initInternalComponent(vm, options)
  16. } else {
  17. vm.$options = mergeOptions(
  18. resolveConstructorOptions(vm.constructor),
  19. options || {},
  20. vm
  21. )
  22. }
  23. /* istanbul ignore else */
  24. if (process.env.NODE_ENV !== 'production') {
  25. initProxy(vm)
  26. } else {
  27. vm._renderProxy = vm
  28. }
  29. // expose real self
  30. vm._self = vmnext
  31. initLifecycle(vm)
  32. initEvents(vm)
  33. initRender(vm)
  34. callHook(vm, 'beforeCreate')
  35. initInjections(vm) // resolve injections before data/props
  36. initState(vm)
  37. initProvide(vm) // resolve provide after data/props
  38. callHook(vm, 'created')
  39. /* istanbul ignore if */
  40. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  41. vm._name = formatComponentName(vm, false)
  42. mark(endTag)
  43. measure(`vue ${vm._name} init`, startTag, endTag)
  44. }
  45. if (vm.$options.el) {
  46. vm.$mount(vm.$options.el)
  47. }
  48. }
  1. 这个函数里有很多逻辑,它主要做的事情就是初始化组件的事件,状态等,大多不是我们本次分析的重点,目前只需要关心里面的这一段代码:
  1. if (options && options._isComponent) {
  2. initInternalComponent(vm, options)
  3. } else {
  4. vm.$options = mergeOptions(
  5. resolveConstructorOptions(vm.constructor),
  6. options || {},
  7. vm
  8. )
  9. }
  1. 执行 new Profile() 的时候没有传任何参数,所以这里的 options undefined,会走到 else 分值,然后 resolveConstructorOptions(vm.constructor)其实就是拿到 Sub.options 这个东西,你可以在上文的 Vue.extend 源码中找到它,然后将 Sub.options new Profile() 传入的options合并,再赋值给实例的$options,所以如果 new Profile() 的时候传入了一个 options,这个 options 将会合并到 vm.$options上,然后在这个 _init 函数的最后判断了下vm.$options.el 是否存在,存在的话就执行 vm.$mount 将组件挂载到 el 上,因为我们没有传 options,所以这里的 el 肯定是不存在的,所以你才会看到例子中的 new Profile().$mount('#mount-point') 手动执行了 $mount 方法,其实经过这些分析你就会发现,我们直接执行 new Profile({ el: '#mount-point' }) 也是可以的,除了 el 也可以传其他参数,接着往下看就知道了。<br />$mount 方法会执行“挂载”,其实内部的整个过程是很复杂的,会执行 renderupdatepatch 等等,由于这些不是本次文章的重点,你只需要知道她会将组件的 dom 挂载到对应的 dom 节点上就行了,如$mount('#mount-point') 会把组件 dom 挂载到 #mount-point这个元素上。

1️⃣ 使用

经过上面的分析,你应该大致了解了Vue.extend的原理以及初始化过程,以及简单的使用,其实这个初始化和平时的new Vue()是一样的,毕竟两个执行的同一个方法。但是在实际的使用中,我们可能还需要给组件传 props,slots 以及绑定事件,下面我们来看下如何做到这些事情。

2️⃣ 使用 Prop

假如我们有一个 message 组件

  1. <template>
  2. <div class="message-box">
  3. {{ message }}
  4. </div>
  5. </template>
  6. <script>
  7. export default {
  8. props: {
  9. message: {
  10. type: String,
  11. default: ''
  12. }
  13. }
  14. }
  15. </script>

它需要一个 props 来显示这个 message,在使用 Vue.extend 时,要想给组件传参数,我们需要在实例化的时候传一个 propsData, 如:

  1. const MessageBoxCtor = Vue.extend(MessageBox)
  2. new MessageBoxCtor({
  3. propsData: {
  4. message: 'hello'
  5. }
  6. }).$mount('#target')

你可能会不明白为什么要传propsData,没关系,接下来就来搞懂它,毕竟文章的目的就是彻底分析。
在上文的 _init 函数中,在合并完 $options 后,还执行了一个函数 initState(vm),它的作用就是初始化组件状态(props,computed,data):

  1. export function initState(vm: Component) {
  2. vm._watchers = []
  3. const opts = vm.$options
  4. if (opts.props) initProps(vm, opts.props)
  5. if (opts.methods) initMethods(vm, opts.methods)
  6. if (opts.data) {
  7. initData(vm)
  8. } else {
  9. observe((vm._data = {}), true /* asRootData */)
  10. }
  11. if (opts.computed) initComputed(vm, opts.computed)
  12. if (opts.watch && opts.watch !== nativeWatch) {
  13. initWatch(vm, opts.watch)
  14. }
  15. }
  1. 别的不看,只看这个:
  1. if (opts.props) initProps(vm, opts.props)
  1. function initProps(vm: Component, propsOptions: Object) {
  2. const propsData = vm.$options.propsData || {}
  3. const props = (vm._props = {})
  4. // ...省略其他逻辑
  5. }
  1. 这里的 propsData 就是数据源,他会从 vm.$options.propsData 上取,上文说过在执行 _init 的时候 new MessageBoxCtor(options) options 会被合并和 vm.$options 上,所以我们就可以在 options 中传入 propsData 属性,使得 initProps() 能取到这个值,从而进行 props 的初始化。

2️⃣ 绑定事件

可能有时候我们还想给组件绑定事件,其实这里应该很多小伙伴都知道怎么做,我们可以通过vm.$on给组件绑定事件,这个也是平时经常用到的一个 api

  1. const MessageBoxCtor = Vue.extend(MessageBox)
  2. const messageBoxInstance = new MessageBoxCtor({
  3. propsData: {
  4. message: 'hello'
  5. }
  6. }).$mount('#target')
  7. messageBoxInstance.$on('some-event', () => {
  8. console.log('success')
  9. })

2️⃣ 使用示例

创建一个 hint 组件

  1. <template>
  2. <div class="hint" v-if="show" ref="modal">
  3. <div class="title">{{ title }}</div>
  4. <div class="content">{{ content }}</div>
  5. <div class="but">
  6. <div class="no" @click="cancel">{{ cancelText }}</div>
  7. <div class="ok" @click="confirm">{{ confirmText }}</div>
  8. </div>
  9. </div>
  10. </template>
  11. <script>
  12. export default {
  13. data() {
  14. return {
  15. show: false,
  16. title: "",
  17. content: "",
  18. confirmText: "确定",
  19. cancelText: "取消",
  20. onConfirm: () => {
  21. // 确认执行函数
  22. this.$emit("confirm");
  23. },
  24. onCancel: () => {
  25. // 取消执行函数
  26. this.$emit("cancle");
  27. },
  28. };
  29. },
  30. methods: {
  31. // 取消
  32. cancel() {
  33. console.log("取消了吗?");
  34. this.onCancel("cancel");
  35. this.remove();
  36. },
  37. // 确认
  38. confirm() {
  39. console.log("确定了吗?");
  40. this.onConfirm("confirm");
  41. this.remove();
  42. },
  43. // 确认或取消后移除元素
  44. remove() {
  45. this.show = false;
  46. },
  47. },
  48. };
  49. </script>
  50. <style lang="less">
  51. .hint {
  52. position: absolute;
  53. top: 0;
  54. right: 0;
  55. bottom: 0;
  56. left: 0;
  57. margin: auto;
  58. width: 300px;
  59. height: 200px;
  60. background-color: #fff;
  61. box-shadow: 0 0 5px #ccc;
  62. border-radius: 10px;
  63. overflow: hidden;
  64. .title,
  65. .content {
  66. display: flex;
  67. align-items: center;
  68. justify-content: center;
  69. padding: 10px 0;
  70. font-size: 20px;
  71. }
  72. .but {
  73. cursor: pointer;
  74. width: 100%;
  75. height: 50px;
  76. position: absolute;
  77. bottom: 0;
  78. border-top: 1px solid #f5f5f5;
  79. display: flex;
  80. align-items: center;
  81. justify-content: space-around;
  82. .ok,
  83. .no {
  84. display: flex;
  85. align-items: center;
  86. justify-content: center;
  87. width: 50%;
  88. height: 100%;
  89. }
  90. .ok {
  91. background-color: rgb(83, 165, 241);
  92. }
  93. }
  94. }
  95. </style><template>
  96. <div class="hint" v-if="show" ref="modal">
  97. <div class="title">{{ title }}</div>
  98. <div class="content">{{ content }}</div>
  99. <div class="but">
  100. <div class="no" @click="cancel">{{ cancelText }}</div>
  101. <div class="ok" @click="confirm">{{ confirmText }}</div>
  102. </div>
  103. </div>
  104. </template>
  105. <script>
  106. export default {
  107. data() {
  108. return {
  109. show: false,
  110. title: "",
  111. content: "",
  112. confirmText: "确定",
  113. cancelText: "取消",
  114. onConfirm: () => {
  115. // 确认执行函数
  116. this.$emit("confirm");
  117. },
  118. onCancel: () => {
  119. // 取消执行函数
  120. this.$emit("cancle");
  121. },
  122. };
  123. },
  124. methods: {
  125. // 取消
  126. cancel() {
  127. console.log("取消了吗?");
  128. this.onCancel();
  129. this.remove();
  130. },
  131. // 确认
  132. confirm() {
  133. console.log("确定了吗?");
  134. this.onConfirm();
  135. this.remove();
  136. },
  137. // 确认或取消后移除元素
  138. remove() {
  139. this.show = false;
  140. },
  141. },
  142. };
  143. </script>
  144. <style lang="less">
  145. .hint {
  146. position: absolute;
  147. top: 0;
  148. right: 0;
  149. bottom: 0;
  150. left: 0;
  151. margin: auto;
  152. width: 300px;
  153. height: 200px;
  154. background-color: #fff;
  155. box-shadow: 0 0 5px #ccc;
  156. border-radius: 10px;
  157. overflow: hidden;
  158. .title,
  159. .content {
  160. display: flex;
  161. align-items: center;
  162. justify-content: center;
  163. padding: 10px 0;
  164. font-size: 20px;
  165. }
  166. .but {
  167. cursor: pointer;
  168. width: 100%;
  169. height: 50px;
  170. position: absolute;
  171. bottom: 0;
  172. border-top: 1px solid #f5f5f5;
  173. display: flex;
  174. align-items: center;
  175. justify-content: space-around;
  176. .ok,
  177. .no {
  178. display: flex;
  179. align-items: center;
  180. justify-content: center;
  181. width: 50%;
  182. height: 100%;
  183. }
  184. .ok {
  185. background-color: rgb(83, 165, 241);
  186. }
  187. }
  188. }
  189. </style>
  1. main.js 中注册
  1. import Vue from 'vue'
  2. import App from './App.vue'
  3. import hintComp from './view/hint.vue';
  4. Vue.config.productionTip = false
  5. function hint(options) {
  6. // 返回一个 vue 子类
  7. const Hint = Vue.extend(hintComp);
  8. // 创建实例并且挂载到一个空的 div 上
  9. const app = new Hint().$mount(document.createElement('div'));
  10. // 初始化参数 - 将传入的参数赋值给实例的 data
  11. for (let key in options) {
  12. app[key] = options[key];
  13. }
  14. // 将元素插入body中
  15. document.body.appendChild(app.$el);
  16. }
  17. // 添加的原型上
  18. Vue.prototype.$hint = hint;
  19. new Vue({
  20. render: h => h(App),
  21. }).$mount('#app')
  1. 在组件中使用
  1. <template>
  2. <div id="app"></div>
  3. </template>
  4. <script>
  5. export default {
  6. name: "App",
  7. components: {},
  8. mounted() {
  9. this.$hint({
  10. show: true,
  11. title: "这是标题",
  12. content: "这是内容这是内容这是内容",
  13. onCancel(v) {
  14. console.log(v);
  15. console.log("取消了!");
  16. },
  17. onConfirm(v) {
  18. console.log(v);
  19. console.log("确定了!");
  20. },
  21. });
  22. },
  23. };
  24. </script>