我的开源库:
- fly-barrage 前端弹幕库,项目官网:https://fly-barrage.netlify.app/,可实现类似于 B 站的弹幕效果,并提供了完整的 DEMO,Gitee 推荐项目;
- fly-gesture-unlock 手势解锁库,项目官网:https://fly-gesture-unlock.netlify.app/,在线体验:https://fly-gesture-unlock-online.netlify.app/,可高度自定义锚点的数量、样式以及尺寸;
最好先看下我的上篇文章,上篇文章的内容在本篇文章中能够用到,点击这里。
Vue.extend API 的官方文档点击这里。
Vue.extend 是 Vue 中很重要的一个方法,虽然在平时的开发中很少用到它,但是在 Vue 源码内部,extend 方法却很重要。为什么说这个方法很重要呢?是因为在 Vue 中,组件的本质就是通过 extend 方法创建出来的 Vue 构造函数的子级构造函数。我们知道 extend 方法的参数是一个组件选项对象,extend 方法的内部会创建一个 Sub 函数并最终返回,这个 Sub 函数就是上面所说的子级构造函数,这个 Sub 函数的 options 属性保存着组件选项对象中的数据,当通过这个 Sub 函数实例化组件实例对象的时候,保存在 Sub.options 对象中能够描述该组件的数据便会被整形转移到组件实例对象中(vm.$options 属性中)。至此,组件的 Vue 实例就创建出来了,并且该组件实例对象中还保存着被处理过的组件选项对象数据。
extend 方法的作用和重要性就讲到这里,接下来开始解析 extend 方法的源码,该方法定义在 src/core/global-api/extend.js 文件中。
1,extend 方法总览
首先总览一下 extend 方法,接下来进行一步步的解析。
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
// Super = this = 父级构造器
const Super = this
// 拿到父级构造器的 cid
const SuperId = Super.cid
// extendOptions._Ctor 对象用于缓存这一个 extendOptions 对象已经创建了的构造函数
// 缓存的 key 是父级构造器的 cid。
// extendOptions + cid 能够唯一确定某一个子类构造器
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
// 如果 extendOptions 已经和这一个父级构造器生成了子级构造器的话,
// 在这里,直接 return 缓存中的子级构造器
return cachedCtors[SuperId]
}
// 组件 name 校验
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production') {
// 非生产环境下,如果不合规范的话,打印出警报
if (!/^[a-zA-Z][\w-]*$/.test(name)) {
warn(
'Invalid component name: "' + name + '". Component names ' +
'can only contain alphanumeric characters and the hyphen, ' +
'and must start with a letter.'
)
}
}
// 定义子构造函数 Sub
const Sub = function VueComponent (options) {
this._init(options)
}
// 将父级构造函数的原型对象赋值给 Sub.prototype
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
// 赋值自增长的 cid
Sub.cid = cid++
// 下面是实现继承的关键,使用 mergeOptions 方法将 父级的options 和 extendOptions 进行合并
// 并将结果赋值给 Sub 子构造函数的 options 属性上
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)
}
// 赋值全局的静态方法
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// ASSET_TYPES = [ 'component', 'directive', 'filter' ]
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}
// 在 Sub 函数上面保存:(1)父级的 options;(2)当前拓展的 extendOptions;(3)上面两个 options 合并后的 options
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// 缓存创建的 Sub 子构造器
cachedCtors[SuperId] = Sub
return Sub
}
2,缓存处理
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
// Super = this = 父级构造器
const Super = this
// 拿到父级构造器的 cid
const SuperId = Super.cid
// extendOptions._Ctor 对象用于缓存这一个 extendOptions 对象已经创建了的构造函数
// 缓存的 key 是父级构造器的 cid。
// extendOptions + cid 能够唯一确定某一个子类构造器
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
// 如果 extendOptions 已经和这一个父级构造器生成了子级构造器的话,
// 在这里,直接 return 缓存中的子级构造器
return cachedCtors[SuperId]
}
......
......
// 缓存创建的 Sub 子构造器
cachedCtors[SuperId] = Sub
return Sub
}
extendOptions 对象中的 _Ctor 属性用于缓存通过 extendOptions 选项对象创建出的子级构造函数。_Ctor 属性是一个对象,该对象的 key 是父级构造函数的 cid,每个 Vue 构造函数都会有一个唯一标识 cid,cid 是一个递增的整数,_Ctor 对象的 value 是 Vue 构造函数,这个函数是通过当前的 extendOptions 对象和当前的父级构造函数借助 extend 方法创建出来的,所以某个子级 Vue 构造函数能够通过 extendOptions + SuperId 唯一标识。
在源码中,通过 if (cachedCtors[SuperId]) 判断 cachedCtors 对象有没有缓存当前想要创建的子级构造函数,如果当前想要创建的子级构造函数已经被创建过一次,则在这里直接返回 cachedCtors[SuperId] 即可,相同的子级构造函数不用重复创建,提高性能。
3,校验组件名称
在非生产环境下,对组件的名称进行校验,校验的规则是只能包含字母、数字和连字符,并且必须以字母开头,如果不满足要求的话,则打印出警告信息。
Vue.extend = function (extendOptions: Object): Function {
......
......
// 组件 name 校验
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production') {
// 非生产环境下,如果不合规范的话,打印出警报
if (!/^[a-zA-Z][\w-]*$/.test(name)) {
warn(
'Invalid component name: "' + name + '". Component names ' +
'can only contain alphanumeric characters and the hyphen, ' +
'and must start with a letter.'
)
}
}
......
......
}
4,定义子级构造函数 Sub
Vue.extend = function (extendOptions: Object): Function {
......
......
// 定义子级构造函数 Sub
const Sub = function VueComponent (options) {
this._init(options)
}
......
......
}
定义子级构造函数 Sub,函数体是 this._init(options),Vue 构造函数的函数体也是这样,保持一致即可。
Vue 构造函数。
function Vue (options) { // 如果当前的环境不是生产环境,并且当前命名空间中的 this 不是 Vue 的实例的话, // 发出警告,Vue 必须通过 new Vue({}) 使用,而不是把 Vue 当做函数使用 if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } // 执行 vm 原型上的 _init 方法,该方法在 initMixin 方法中定义 this._init(options) }
5,赋值 Sub 的原型对象
Vue.extend = function (extendOptions: Object): Function {
......
......
// 将父级构造函数的原型对象赋值给 Sub.prototype
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
......
......
}
将 Super.prototype 对象赋值给 Sub.prototype,这里借助原型链,使得 Sub 构造函数的实例可以访问到定义在原型链中的原型方法,原型链中的原型方法如下所示。
// 下面函数的作用是:往 Vue 的原型上写入原型函数,这些函数是给 Vue 的实例使用的
// 写入 vm._init
initMixin(Vue)
// 写入 vm.$set、vm.$delete、vm.$watch
stateMixin(Vue)
// 写入 vm.$on、vm.$once、vm.$off、vm.$emit
eventsMixin(Vue)
// 写入 vm._update、vm.$forceUpdate、vm.$destroy
lifecycleMixin(Vue)
// 写入 vm.$nextTick、vm._render
renderMixin(Vue)
6,合并构造函数的 options
Vue.extend = function (extendOptions: Object): Function {
......
......
// 赋值自增长的 cid
Sub.cid = cid++
// 下面是实现继承的关键,使用 mergeOptions 方法将 父级的options 和 extendOptions 进行合并
// 并将结果赋值给 Sub 子构造函数的 options 属性上
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
......
......
}
这段代码最重要的是合并 Super.options 和 extendOptions 到 Sub.options,这使得 Sub 构造函数的实例能够使用到父级构造函数的 options 和当前要拓展的 options,mergeOptions 方法的具体解释可以看我的这篇文章。
7,处理 options.props 和 options.computed
Vue.extend = function (extendOptions: Object): Function {
......
......
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
......
......
}
function initProps (Comp) {
const props = Comp.options.props
for (const key in props) {
proxy(Comp.prototype, `_props`, key)
}
}
function initComputed (Comp) {
const computed = Comp.options.computed
for (const key in computed) {
defineComputed(Comp.prototype, key, computed[key])
}
}
options 中定义的 props 和 computed 属性,Vue 的处理方法是将它们定义到子级构造函数的原型对象中,这样做的好处是不用在每个创建出的实例上重复定义 props 和 computed,实例通过原型链访问定义在原型对象中的 props 和 computed 即可。
8,赋值全局的静态方法
Vue.extend = function (extendOptions: Object): Function {
......
......
// 赋值全局的静态方法
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// ASSET_TYPES = [ 'component', 'directive', 'filter' ]
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
......
......
}
将父级构造函数上面的一些全局静态方法赋值到 Sub 子级构造函数上,这些静态方法通过 initGlobalAPI 方法已经被赋值到 Vue 构造函数上了,相关源码如下。
export function initGlobalAPI (Vue: GlobalAPI) {
// 初始化 Vue.use()
initUse(Vue)
// 初始化 Vue.mixin()
initMixin(Vue)
// 初始化 Vue.extend()
initExtend(Vue)
// 初始化 Vue.component()、Vue.directive()、Vue.filter(),用于向 Vue 中注册资源
initAssetRegisters(Vue)
}
9,在子级构造函数上定义一些 extend 特有的属性
Vue.extend = function (extendOptions: Object): Function {
......
......
// 在 Sub 函数上面保存:
// (1)父级的 options;
// (2)当前拓展的 extendOptions;
// (3)上面两个 options 合并后的 options
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
......
......
}
10,缓存 Sub 函数并 return 出去
将创建出来的 Sub 函数缓存到 cachedCtors 对象中,并且在最后 return Sub 函数。
Vue.extend = function (extendOptions: Object): Function {
......
......
// 缓存创建的 Sub 子构造器
cachedCtors[SuperId] = Sub
return Sub
}