【手写 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 指向问题产生原因
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: vue