【手写 Vue2.x 源码】第三十六篇 - 组件部分 - Vue.extend 实现
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
一前言
上篇主要介绍了 Vue 初始化流程中的 Vue.component 实现
- Vue.component 全局 API 的初始化处理
- Vue.component 的定义和参数说明
- 组件构造函数全局存储的方式和作用
本篇组件部分 - Vue.extend 实现
二Vue.extend 简介
备注为了描述严禁以下示例引用 Vue 官网
1前文回顾
上篇在 Vue.component 的实现中通过 Vue.component 创建组件时
两种方式既可以传入函数也可以传入对象
// 写法 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.options.components
上备用
2Vue.extend 简介
Vue.extend
:使用基础 Vue 构造器创建一个“子类”。
options
参数是一个包含组件选项的对象。
data
选项是特例需要注意 - 在 Vue.extend()
中它必须是函数
示例
<div id="mount-point"></div>
// 创建构造器
var Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
data: function () { // data 必须是函数
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
})
// 创建 Profile 实例并挂载到一个元素上。
new Profile().$mount('#mount-point')
结果如下
<p>Walter White aka Heisenberg</p>
三Vue.extend 实现
1当前代码
// src/global-api/index.js
export function initGlobalAPI(Vue) {
Vue.options = {}; // 全局属性Vue.options
Vue.options.components = {};// 存放全局组件
/**
* 使用基础的 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;
}
}
2Vue.extend 内部逻辑
Vue.extend 会使用基础 Vue 构造器生成一个子类
所以Vue.extend 内部需要生成一个继承 Vue 的子类 Sub
- 父类 Vue 即当前 this;
- 子类继承自父类即子类 Sub 继承 Vue 的原型方法
// src/global-api/index.js#initGlobalAPI
Vue.extend = function (definition) {
// 父类 Vue 即当前 this;
const Super = this;
// 创建子类 Sub
const Sub = function (options) {
}
}
3组件的初始化
创造一个组件实际就是 new 这个组件的类
在前面的组件初始化过程中当执行 new Vue 时会调用 Vue 原型方法 _init
// src/index.js
/**
* 在vue 中所有的功能都通过原型扩展原型模式的方式来添加
* @param {*} options vue 实例化传入的配置对象
*/
function Vue(options) {
this._init(options); // 调用Vue原型上的方法_init
}
initMixin(Vue)
renderMixin(Vue) // 混合一个 render 方法
lifeCycleMixin(Vue)
initGlobalAPI(Vue) // 初始化 global Api
Vue 原型方法 _init
// src/init.js#initMixin
Vue.prototype._init = function (options) {
const vm = this; // this 指向当前 vue 实例
// vm.$options = options; // 将 Vue 实例化时用户传入的options暴露到vm实例上
// 此时需使用 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)
}
}
所以当 new 组件时就会进行组件的初始化也会执行 Vue 初始化时的 _init 方法
Vue.extend = function (definition) {
// 父类 Vue 即当前 this;
const Super = this;
// 创建子类 Sub
const Sub = function (options) {
// 当 new 组件时执行组件初始化
this._init(options);
}
}
所以当 new sub 时也会调用初始化方法
function Vue(options) {
this._init(options);
}
4子类继承父类
那么子类如何继承于父类
子类 Sub 继承于父类即继承 Vue 的原型方法
Vue.extend = function (definition) {
// 父类 Vue 即当前 this;
const Super = this;
// 创建子类 Sub
const Sub = function (options) {
// new 组件时执行组件初始化
// 由于 Sub 继承于 Vue会执行 Vue._init 方法
this._init(options);
}
// 继承 Vue 的原型方法:Sub.prototype.__proto__ = Supper.prototype父类的原型
Sub.prototype = Object.create(Super.prototype);
}
备注还可以使用 ES6 方式 Object.setPrototypeOf能够通过链拿到父类上的所有属性
面试题
问组件中的 data 为什么必须是一个函数而不能是对象
-
如果 data 是对象由于对象是引用类型指向同一个引用地址new Component 后 data 是共用的
-
data 是函数每次 new Component 时组件内部都是一个独立的对象
5修复 constructor 指向问题
问题分析
Object.create 实现原理
// Object.create会生成一个具有父类原型的新实例
function create(parentPrototype) {
// 声明空函数 Fn
const Fn = function () {};
// 将 Fn 的 prototype 赋值为父类原型
Fn.prototype = parentPrototype;
// 返回 Fn 的实例 fn
return new Fn();
}
当调用 Object.create 时内部会构建一个具有父类原型的新实例;
// 通过 new Fn 产生的实例 fnfn 的原型指向父类的原型
let fn = Object.create(Super.prototype);
// Sub.prototype 指向 fn
Sub.prototype = fn;
这样子类就可以通过链拿到父类上的方法了
但是这种写法也产生了一个严重的问题
- Sub.prototype 是 fn但 fn.constructor 指向的还是 Fn
- 此时 fn.constructor 应该指向当前的子类 Sub
问题解决
经以上分析可知由于 Object.create 内部会产生一个新的实例作为子类的原型这会导致子类的 constructor 指向错误
修复 constructor 指向问题
// src/global-api/index.js
export function initGlobalAPI(Vue) {
/**
* 使用基础的 Vue 构造器创造一个子类
* new 子类时执行组件的初始化 _init
* @param {*} definition
*/
Vue.extend = function (definition) {
const Super = this;
const Sub = function (options) {
this._init(options);
}
Sub.prototype = Object.create(Super.prototype);
// 修复 constructor 指向问题Object.create 会产生一个新的实例作为子类的原型导致constructor指向错误
Sub.prototype.constructor = Sub;
return Sub;
}
}
这样就通过 Vue.extend 生成了子类即组件的构造函数
接下来再将组件的构造函数保存到全局Vue.options.components
上备用即可
// todo 补充父类和子类选项的合并
四结尾
本篇介绍了 Vue.extend 实现主要涉及以下几个点
- Vue.extend 简介
- Vue.extend 实现包括组件初始化子类继承父类修复 constructor 指向问题
下一篇组件的合并策略
维护日志
- 20210810
- 微调部分语句描述与排版
- 添加部分三级标题使内容划分更清晰易懂
- 20210812
- 添加“修复 constructor 指向问题-问题分析”部分内容通过 Object.create 实现原理分析constructor 指向问题产生原因