深入浅出Vue.extend(源码导读+实现一个编程式组件)

5,143 阅读2分钟

简介

Vue.extend作为一个全局api,当然值得我们去深入学习的,同时也是实现编程式组件的重要途径,所以我们通过源码导读加实践的方式开始吧。首先我们会带着几个问题来进行学习,如果你都不会,哈哈哈恭喜你,学完本篇你就会明白了。

  • Vue.extend在Vue当中起到的作用?
  • 讲讲Vue.extend内部实现?
  • 实现一个编程式组件,说说具体思路?

目录

  • 基本用法
  • 源码赏析
  • 源码导读
  • 手动实现一个编程式组件
  • 总结

基本用法

参数:

{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>

源码赏析

在我们赏析源码之前,我们首先肯定要找到这个api及调用的位置,这样才能更好的理解它的作用。通过官方用法我们就知道,它肯定和组件有关系,那么我们很容易找到在源码中创建组件的位置,接下来我们一步一步找:

1创建组件:在src/core/vdom/create-element:(113)

     vnode = createComponent(Ctor, data, context, children, tag)

2进入createComponent函数:在src/core/vdom/create-component:(115)

if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

这里, baseCtor其实就是Vue,具体原因在以后分析组件源码会讲解,这里不作研究。我们终于找到了Vue.extend调用的位置,就是在Vue创建每个组件的时候调用,通过构造器创建子类。下面就是完整的源码:

Vue.cid = 0
  let cid = 1
Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name)
    }

    const Sub = function VueComponent (options) {
      this._init(options)
    }
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['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(Sub)
    }
    if (Sub.options.computed) {
      initComputed(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]
    })
    // enable recursive self-lookup
    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.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    cachedCtors[SuperId] = Sub
    return Sub
  }
}

接下来我们就一步一步彻底搞懂源码。

源码导读

a:

  extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid

首先,extendOptions使我们传入进去的模板,这里面的this就是调用extend的对象,就是Vue,然后保存在变量Super中,变量SuperId就保存Vue中的唯一标识(每个实例都有自己唯一的cid).

b:

 const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

这一段作为缓存策略的,放在下面说。

c:

 const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name)
    }

用一个name变量来保存组件的名字,就是我写组件时候的name,如果没写就使用父组件的name,然后对name通过validateComponentName函数验证,主要就是判断name不能是html元素和不能是非法命名。

d:

Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++

上面我们创建一个子类Sub,这里我们通过继承,使Sub拥有了Vue的能力,并且添加了唯一id(每个组件的唯一标识符)

e:

 Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

这里调用了mergeOptions函数实现了父类选项与子类选项的合并,并且子类的super属性指向了父类。

f:

if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }

初始化了props和computed.

g:

 // 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]
    })
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub
    }
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

将父类的方法复制到子类,包括extend,mixin,use,component,directive,filter.还新增属性superOptions,extendOptions,sealedOptions 。

h:

 // cache constructor
    cachedCtors[SuperId] = Sub

之前的代码结合,将父类的id保存在子类的属性上,属性值为子类,在之前会进行判断如果构造过子类,就直接将父类保存过的id值给返回了,就是子类本身不需要重新初始化,,作为一个缓存策略。

总体来说,其实就是创建一个Sub函数并继承了父级。

手动实现一个编程式组件

我们在一般调用组件时,都会先组件注册,再在模板中引用。一个两个还是可以接受的,那么如果这个组件很多很多呢,所以每次注册应用就显得力不从心了。用过element-ui我们都知道,可以不用注册,直接通过命令调用:

this.$message.success('成功了')

是不是特别特别方便,简单明了,而且还符合编程思维。接下来我们将手动实现一下:

a:创建一个组件(用于编程式调用的)

<template>
  <div class='toast'
       v-show='isShow'>
    {{message}}
  </div>
</template>
<script>
export default {
  data () {
    return {
      message: '',
      isShow: false
    }
  },
  methods: {
    show (message, duration) {
      this.message = message
      this.isShow = true
      setTimeout(() => {
        this.isShow = false
        this.message = ''
      }, duration)
    }
  }
}
</script>
<style scoped>
.toast {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 999;
  padding: 8px 10px;
  background-color: rgba(0, 0, 0, 0.3);
}
</style>

这个组件非常简单,两个变量分别是显示的消息和是否显示。我还定义了一个方法,作用就是使模板取消隐藏,并且传入一个duration参数通过定时器表示显示的时间,时间一到isShow就变false,可以说是非常简单了,模板有了,下面开始重点了。

b:实现编程式

import Toast from './toast'
const obj = {}
obj.install = function (Vue) {
  // 创建构造器
  const ToastContrystor = Vue.extend(Toast)
  // new的方式 根据组件构造器,可以创建组件对象
  const toast = new ToastContrystor()
  // 手动挂载某一个元素上
  toast.$mount(document.createElement('div'))
  // toast.$el对应的就是div
  document.body.appendChild(toast.$el)
    //组件挂载到Vue原型上
  Vue.prototype.$toast = toast
}
export default obj

把组件当作插件一样,通过Vue.use()使用,注释都写的非常清楚了,一步一步理解。

c:在main.js注册

import Vue from 'vue'
import toast from '@/components/common/toast/index.js'
Vue.use(toast)

d:使用

   this.$toast.error('注册失败,请重新输入', 1000)

就可以在项目各个地方调用了。我们就实现了一个编程式的组件。

总结

VUe.extend总体来说其实就是创建一个类来继承了父级,顶级一定是Vue.这个类就表示一个组件,我们可以通过new的方式来创建。学习了extend我们就很容易的实现一个编程式组件。