一,前言
上篇,主要介绍了 Vue 初始化流程中的 Vue.component 实现:
- Vue.component 全局 API 的初始化处理;
- Vue.component 的定义和参数说明;
- 组件构造函数全局存储的方式和作用;
本篇,组件部分 - Vue.extend 实现;
二,Vue.extend 简介
以下示例引用自 Vue 官网
1,前文回顾
上篇,介绍 Vue.component 实现时提到,第二个参数组件定义 definition 支持两种形式:
- 既可以传入函数、
- 也可以传入对象;
以下代码引用自官方文档:
// 写法 1:注册组件,传入一个扩展过的构造器
Vue.component('my-component', Vue.extend({ /* ... */ }))
// 写法 2:注册组件,传入一个选项对象 (自动调用 Vue.extend)
Vue.component('my-component', { /* ... */ })
// 获取注册的组件 (始终返回构造器)
var MyComponent = Vue.component('my-component')
两种形式的差异点:若组件定义
definition为对象,则在Vue.component内部会通过Vue.extend进行一次包装,结果会产生一个组件的构造函数;(在Vue.component处理结束时,被保存到全局的Vue.options.components上备用)
2,Vue.extend 简介
Vue.extend(options) 方法:使用基础 Vue 构造器,创建一个“子类”;
options参数:是一个包含组件选项的对象。
注意:在
Vue.extend()中,data必须是函数;
Vue.extend 示例(以下示例引用自 Vue 官网):
<div id="mount-point"></div>
// 创建构造器
var Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
// data 必须是函数
data: function () {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
})
// 创建 Profile 实例,并挂载到一个元素上
new Profile().$mount('#mount-point')
// 组件挂载后的结果:
<p>Walter White aka Heisenberg</p>
三,Vue.extend 实现的分析与代码框架
1,当前代码
在上一篇介绍 Vue.component 时提到:“若组件定义 definition 为对象时,需要通过 Vue.extend 处理为组件的构造函数”,Vue.extend 方法尚未实现:
// src/global-api/index.js
export function initGlobalAPI(Vue) {
Vue.options = {}; // 全局属性:Vue.options
Vue.options.components = {};// 存放全局组件 name: definition
/**
* 使用基础的 Vue 构造器,创造一个子类
*
* @param {*} definition
*/
Vue.extend = function (definition) {
// todo...
}
/**
* Vue.component
*
* @param {*} id 组件名(默认)
* @param {*} definition 组件定义:可能是对象或函数
*/
Vue.component = function (id, definition) {
// 获取组件名 name:优先使用definition.name,默认使用 id
let name = definition.name || id;
definition.name = name;
// 如果传入的 definition 是对象,需要用 Vue.extend 处理
if(isObject(definition)){
definition = Vue.extend(definition)
}
// 将 definition 对象保存到全局:Vue.options.components
Vue.options.components[name] = definition;
}
}
Vue.component方法执行完成的最终结果,就是将组件定义(子类)保存到Vue.options.components中;
2,Vue.extend 的逻辑分析
Vue.extend的官方定义:Vue.extend会使用基础Vue构造器,生成一个子类;
所以,在 Vue.extend 内部,需要生成一个继承自 Vue 的子类 Sub:
- 需要先拿到父类
Vue:即Vue.extend中的this; - 创建子类继承父类:创建一个子类
Sub继承Vue的原型方法;
3,Vue.extend 的实现框架
以上分析的代码框架如下:
// src/global-api/index.js#initGlobalAPI
Vue.extend = function () {
// 父类 Vue:当前 this 就是 Vue;
const Super = this;
// 创建子类 Sub
const Sub = function (options) {
}
// 子类 Sub 继承 Vue 的原型方法
Sub.prototype = Object.create(Super.prototype);
// 返回生成的子类
return Sub;
}
其中,const Sub = function (options) {} 就是组件的构造函数;
四,组件初始化的分析与实现
1,组件初始化的分析
提问:如何创造一个组件?
实际上,创造一个组件就是要得到一个组件的实例,即实例化组件的构造函数:
组件实例 = new 组件类
这里所说的组件的类或组件的构造函数,正是 Vue.extend 处理后返回的子类 Sub;(Vue.extend 返回的子类 Sub 继承自 Vue)
当组件被实例化时,需要进行组件的初始化操作,即执行 Vue 原型上的 _init 方法;
这里,先来回顾一下 new Vue 的初始化流程:
1)通过 initMixin,在 Vue 的原型上扩展了 _init 方法:
// src/index.js
/**
* 在vue 中所有的功能都通过原型扩展(原型模式)的方式来添加
* @param {*} options vue 实例化传入的配置对象
*/
function Vue(options) {
this._init(options); // 调用 Vue 原型上的方法 _init
}
initMixin(Vue) // 扩展 Vue,添加原型方法 _init
renderMixin(Vue)
lifeCycleMixin(Vue)
initGlobalAPI(Vue)
2)Vue 的原型方法 _init:选项合并、初始化状态、挂载...
// src/init.js#initMixin
Vue.prototype._init = function (options) {
const vm = this; // this 指向当前 vue 实例
// 使用 options 与 mixin 合并后的全局 options,再进行一次合并
vm.$options = mergeOptions(vm.constructor.options, options);
// 目前在 vue 实例化时,传入的 options 只有 el 和 data 两个参数
initState(vm); // 状态的初始化
if (vm.$options.el) {
// 将数据挂在到页面上(此时,数据已经被劫持)
vm.$mount(vm.$options.el)
}
}
2,组件初始化的实现
当 new Sub(options) 时,要进行组件的初始化操作,即执行 _init 方法;
所以,在组件实例化时,需要调用 this._init 执行组件的初始化操作:
Vue.extend = function () {
// 父类 Vue 即当前 this;
const Super = this;
// 创建子类 Sub ==> new Sub(options)
const Sub = function (options) {
// 当 new 组件时,执行组件初始化
this._init(options);
}
// 子类 Sub 继承 Vue 的原型方法
Sub.prototype = Object.create(Super.prototype);
// 返回生成的子类
return Sub;
}
通过
Vue.extend方法会产生一个子类,new这个子类时,就会执行组件的初始化流程;
五,子类继承父类的分析与实现
1,子类继承父类的分析
提问:那么,子类如何才能继承自父类呢?
让子类 Sub 继承自父类,即继承 Vue 的原型方法:
2,子类继承父类的实现
Vue.extend = function () {
// 父类 Vue,即当前 this;
const Super = this;
// 创建子类 Sub
const Sub = function (options) {
// new 组件时,就会执行组件的初始化;
// 由于 Sub 继承自 Vue,所以会执行 Vue._init 方法
this._init(options);
}
// Sub 继承 Vue 的原型方法:Sub.prototype.__proto__ = Supper.prototype(父类的原型)
Sub.prototype = Object.create(Super.prototype);
return Sub;
}
原型继承,能够通过链拿到父类上的所有属性,常用以下两种方式:
Object.create()ES6方法Object.setPrototypeOf()说明:
Sub.prototype:指Sub的原型;Sub.__proto__:指向所属类的原型,即:Sub.__proto__ = Function.prototype;Sub.prototype = Object.create(Super.prototype),相当于Sub.prototype.__proto__ = Super.prototype这样,
Sub就能够通过链找到Super上的原型方法;
3,关于 JS 实现继承的补充
JS 的属性分为以下三种:
- 原型方法:公共方法,比如
Vue._init方法; - 静态属性:可以直接通过静态类进行调用;
- 实例属性:每
new出一个实例,就会产生一份;
function A(){ ... }
function B(){ ... }
// 1,继承原型上的属性
A.prototype = Object.create(B.prototype)
// 2,继承类上的属性
A.__proto__ = B // 类
// 3,call 继承实例上的属性
function B(){
A.call(this, {})
}
4,constructor 指向问题的分析
原型继承 Object.create 方法的实现原理分析:
// Object.create:会生成一个具有父类原型的新实例
function create(parentPrototype) {
// 声明一个空函数 Fn
const Fn = function () {};
// 将 Fn 的 prototype 赋值为父类原型
Fn.prototype = parentPrototype;
// 返回 Fn 的实例 fn
return new Fn(); // fn.prototype.__proto__ = parentPrototype
}
当调用 Object.create 时,内部会构建一个具有父类原型的新实例:
// 通过 new Fn 产生的实例 fn,fn 的原型指向父类的原型;
let fn = Object.create(Super.prototype);
// Sub.prototype 指向 fn
Sub.prototype = fn;
这样,子类就可以通过链拿到父类上的方法了;
但是,这种写法也产生了一个严重的问题:
Sub.prototype指Sub类的原型,被赋值为fn;- 但是,
fn.constructor指向的却还是Fn!!!
此时,fn.constructor 应该指向当前的子类 Sub;
5,constructor 指向问题的修复
经以上分析可知:由于 Object.create 内部会产生一个新的实例作为子类的原型,这会导致子类的constructor 指向错误(没有指向子类自己,而是指向这个新产生的实例 fn 所属的类 Fn);
修复constructor指向问题,让子类 Sub 的 constructor 指向自己即可:
// src/global-api/index.js
export function initGlobalAPI(Vue) {
/**
* 使用基础的 Vue 构造器,创造一个子类
* 当 new 子类时,将进行组件的初始化,执行 _init 方法
*/
Vue.extend = function () {
const Super = this;
const Sub = function (options) {
this._init(options);
}
Sub.prototype = Object.create(Super.prototype);
// 修复 constructor 指向问题:使子类 Sub 的 constructor 指向 Sub 自己
// Object.create 会产生一个新的实例作为子类的原型,导致 constructor 指向错误
Sub.prototype.constructor = Sub;
return Sub;
}
}
这样,通过 Vue.extend 就生成了正确的子类,得到了组件的构造函数;
Vue.extend处理完成后,回到Vue.component中,会将返回的组件构造函数保存到全局Vue.options.components上备用;
六,Vue.extend 功能测试
添加断点,查看两次合并的情况:
Vue.extend = function (extendOptions) {
const Super = this;
const Sub = function (options) {
this._init(options);
}
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
debugger
// 合并父类和子类的选项:为了让子类能够拿到 Vue 定义的全局组件
Sub.options = mergeOptions(Super.options, extendOptions);
return Sub;
}
添加日志输出,查看最终输出结果:
// src/global-api/index.js#initGlobalAPI
Vue.component = function (id, definition) {
let name = definition.name || id;
definition.name = name;
if(isObject(definition)){
definition = Vue.extend(definition)
}
Vue.options.components[name] = definition;
// 打印全局组件信息
console.log(Vue.options.components)
}
声明两个全局组件:
<body>
<div id="app">
<my-button></my-button>
</div>
<script>
// 全局组件 1
Vue.component('my-button1', {
name:'my-button1',
template:'<button>Hello Vue 1 全局组件</button>'
})
// 全局组件 2
Vue.component('my-button2', {
name:'my-button2',
template:'<button>Hello Vue 2 全局组件</button>'
})
</script>
</body>
通过控制台,查看执行结果:
日志共输出 2 次,最终两个全局组件被注册到 Vue.options.components 中;
此时,如果 new Vue.options.components[my-button1] 就会执行组件的初始化流程,即调用 Vue 上原型方法 _init;
七,子类 Sub 的选项合并
这部分放到“
Vue.extend功能测试”之后进行说明的原因:
- 在
Vue.extend实现中,选项的合并是非常重要的核心功能;- 当前
mergeOptions选项合并方法中,组件的合并策略尚未实现;
1,选项合并的必要性
所有的组件,不管是全局组件还是局部组件,最终都要通过 Vue.extend 的处理并生成组件的构造函数,即子类 Sub;
- 在
Vue全局上的Vue.options中,存在的父类属性; - 在
Vue.extend()入参中,存在的组件自身属性(可能是全局组件,也可能是局部组件)
在 Vue.extend() 中,父类全局的 Vue.options 和子类全局的 extendOptions,也需要合并在一起,合并到子类 Sub 上,即 Sub.options;
所以,给子类 Sub 也增加一个 options 属性;(这样,父类有一个 options ,子类也有一个 options)
2,Sub.options 逻辑分析
Sub.options 的作用:合并父类的选项和(组件)自身的选项,将合并后的选项放到子类中;
场景分析:在父类中已经定义了一个组件的前提下,在定义第二组件时,选项合并的逻辑:
- 在
Vue.extend(extendOptions)中通过mergeOptions方法,将组件的选项extendOptions与父类的选项Super.options进行合并,结果就是子类Sub的选项;
3,选项合并的代码实现
Vue.extend = function (extendOptions) {
const Super = this;
const Sub = function (options) {
this._init(options);
}
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
// 将全局上的属性,全部合并到子类上:让子类能拿到在Vue上定义的全局组件
// 父类中的选项:Super.options
// 组件自身的选项:extendOptions
Sub.options = mergeOptions(Super.options, extendOptions);
return Sub;
}
这样一来,在子组件中,就拥有了在全局中定义的选项;
通过全局的
Vue来声明组件(Vue.component),当创造子组件时,他也会把Vue 全局上的属性放到自身子组件的 options 上;
mergeOptions方法中,关于组件合并策略的实现,将在下一篇集中进行介绍;
八,Vue.extend 补充说明
关于 Vue.extend 方法,除本文已介绍的核心操作外,在 Vue.extend 内部还进行了以下处理:
- 通过组件的
cid对结果进行缓存,当多次执行相同的Vue.extend(options)操作时,会返回已缓存的组件子类; - 会在子类
Sub上标记父类是谁:Sub[super] = Super; - 会对其他属性也进行初始化和合并,比如:
- 属性的初始化:
initProps(Sub) - 计算属性的初始化:
initComputed(Sub) - extend 继承:
Sub.extend = Super.extend - mixin 继承:
Sub.mixin = Super.mixin - use 继承:
Sub.use = Super.use
- 属性的初始化:
- 会将子类本身也放入到
Sub.options.components中,以实现组件的递归访问:Sub.options.components[name] = Sub;
重要概念:
Vue.options的作用:全局对象,会把当前定义的组件放到Vue.options.components中,所有全局 API 都会保存在此;Vue.component的作用:把定义好的组件,放到Vue.options.components中Vue.extend的作用:根据入参extendOptions对象,生成并返回一个继承自Vue的子类,并将全局选项合并到子类上;mergeOptions的作用: 将全局属性合并到子类Sub上;
九,相关面试题
问题 1:组件中的 data 为什么必须是一个函数,而不能是对象?
回答:通过对 Vue.extend 的了解可知,组件的 options 会在 Vue.extend 中被合并到子类的选项 Sub.options 上;
- 如果
data是对象,由于对象是引用类型,指向同一个引用地址,执行new Sub创建组件实例后的data对象就是共用的;此时,多个组件会共享同一个对象,任何一个组件修改了data中的值,会导致所有组件被更新; - 如果
data是函数,那么每次执行new Sub时,组件实例化时通过调用data函数返回一个新的对象;这样,对data进行修改,多个组件的实例之间各不影响;
十,结尾
本篇,介绍了 Vue.extend 实现,主要涉及以下几个点:
- Vue.extend 简介;
- Vue.extend 实现的分析与代码框架;
- 组件初始化的分析与实现;
- 子类继承父类的分析与实现;
- Vue.extend 的功能测试;
- 组件的选项合并;
- Vue.extend 的补充说明;
下一篇,组件的合并;
维护日志:
- 20210810:
- 微调部分语句描述与排版;
- 添加部分三级标题,使内容划分更清晰易懂;
- 20210812:
- 添加“修复 constructor 指向问题-问题分析”部分内容,通过 Object.create 实现原理分析constructor 指向问题产生原因;
- 20230224:
- 添加了内容中的代码和关键字高亮;
- 优化了部分内容描述,使表达更加准确易懂;
- 优化了代码注释和换行,使逻辑更加清晰;
- 20230226:
- 补充了关于“原型继承”的说明;
- 添加了“全局属性与子类的合并”部分;
- 调整了目录结构;
- 更新了文章摘要;
- 20230303:
- 重新梳理本篇,调整了部分内容描述,使表述和语义更加准确,更易理解;
- 添加 todo:本篇 Vue.extend 到 4 截止,5、6 组件合并内容放到下一篇;
- 20230304:
- 对“组件部分”整体进行了重新梳理,重新设计了本篇的重点内容;
- 调整了文章的目录结构:
- 对核心问题进行了拆分说明,将“Vue.extend的实现”拆分为三个部分进行说明;
- 添加了“功能测试”和“补充说明”部分;
- 移除了“组件合并”部分,与下一篇组件合并内容进行合并;
- 调整了部分内容的描述和相关代码示例,删除了若干冗余描述;
- 解决了之前遗留的两个 todo 问题;
- 更新了文章摘要;
- 20230305:
- 调整了文章的目录结构:
- 添加了“Vue.extend 生成子类 Sub 时的选项合并”部分;
- 更新了文章摘要;
- 调整了文章的目录结构:
- 20230316:
- 添加“关于 JS 实现继承的补充”部分;