一,前言
上篇,主要介绍了 Vue 初始化流程中的 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上备用;
2,Vue.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;  }}
   复制代码
 2,Vue.extend 内部逻辑
Vue.extend 会使用基础 Vue 构造器,生成一个子类;
所以,Vue.extend 内部需要生成一个继承 Vue 的子类 Sub:
 // 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 为什么必须是一个函数,而不能是对象?
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 产生的实例 fn,fn 的原型指向父类的原型;let fn = Object.create(Super.prototype);// Sub.prototype 指向 fnSub.prototype = fn;
   复制代码
 
这样,子类就可以通过链拿到父类上的方法了;
但是,这种写法也产生了一个严重的问题:
问题解决
经以上分析可知:由于 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;  }}
   复制代码
 
这样,就通过 Vue.extend 生成了子类,即组件的构造函数;
接下来,再将组件的构造函数保存到全局Vue.options.components上备用即可;
四,结尾
本篇,介绍了 Vue.extend 实现,主要涉及以下几个点:
下一篇,组件的合并策略;
评论