手写简化版Vue(七) 组件原理--1.注册

151 阅读7分钟

本系列文章会以实现Vue的各个核心功能为标杆(初始化、相应式、编译、虚拟dom、更新、组件原理等等), 不会去纠结于非重点或者非本次学习目标的细节, 从头开始实现简化版Vue, 但是, 即使是简化, 也需要投入一定的时间和精力去学习, 而不可能毫不费力地学习到相对复杂的知识; 所有简化代码都会附上原版源码的路径, 简化版仅仅实现了基本功能, 如需了解更多细节, 可以去根据源码路径去阅读对应的原版源码;

组件的注册

大家都知道, 全局组件注册, 我们一般会使用Vue.component或者Vue.extend 进行注册, 因此我们就从它们的源码着手, 探寻其中的原理; 既然Vue.component和Vue.extend都是Vue构造器上的方法, 既然是构造器上的方法, 那就是全局方法, Vue的全局方法在哪里定义的呢? 翻阅源码, 我们不难找到global-api文件夹下:

global-api

// 源码路径: /src/core/global-api/index.ts
import { initAssetRegisters } from "./assets";
import { initExtend } from "./extend";
import { ASSET_TYPES } from "../../shared/constant";
import config from '../../core/config'

// 设置vue全局方法
export function initGlobalAPI (Vue) {
  const configDef = {}
  configDef.get = () => config
  // 下面的逻辑可以有效防止Vue.config = xx这种误操作, 即将全局配置一次性全部替换掉
  if (process.env.NODE_ENV === 'development') {
    configDef.set = () => {
      console.warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)

  // 构造器上的options初始化
  Vue.options = Object.create(null)
  // 初始化components/directives/filters
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })
  // 注意._base的赋值
  Vue.options._base = Vue
  initExtend(Vue)
  initAssetRegisters(Vue)
}

// 源码路径: /src/shared/constants.ts
export const ASSET_TYPES = ['component', 'directive', 'filter']

我们从以上代码可以看出, 在initGlobalAPI中, 定义了很多构造器上的静态方法/属性 , 如不做特殊处理, 它们不会被其他实例所拥有! 在全局任何位置, 可以通过Vue.xx的方式访问它们; 这里要特别留意下Vue.options, 这个属性可以理解为'全局级别的入參', 也就是全局的用户自己定义的, 都可以被赋给它; 它虽然是静态属性, 但是它在后续会被实例继承(当然是经过特殊处理的, 而非Javascript原生支持!), 并以此将其所具备的所谓全局属性/方法传递到各个实例, 以实现所谓'全局'的效果! 这点后续会详细解析; 这里的Vue.options通过ASSET_TYPES.forEach被赋予了components属性, 因此可以预测, 全局的组件都会被放在这个Vue.options.components之下, 通过Vue.options, 传递给各个实例; 还有_base属性, 指向构造器, 这样, 其他实例也就可以轻松访问到构造器了! 所以可以总结一句: Vue.options就是一个实现所谓全局效果的通道, 全局组件/指令/过滤器都会通过它来实现传递!

initExtend

// 源码路径: /src/core/global-api/extend.ts
import { mergeOptions } from "../util/options"
import { getComponentName } from "../vdom/create-component"
export function initExtend (Vue) {
  Vue.cid = 0
  let cid = 1
  Vue.extend = function (extendOptions) {
    // 注意, 这里的this指向的是Vue构造器
    const Super = this
    const SuperId = Super.cid
    // 缓存构造器
    const CachedCtor = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (CachedCtor[SuperId]) {
      return CachedCtor[SuperId]
    }

    // 获取组件名称
    const name = getComponentName(extendOptions) // 逻辑就是extendOptions.name

    // 组件构造器
    const Sub = function VueComponent (options) {
      this._init(options)
    }
    // 原型链继承
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    // Sub.cid, 用以区分不同的Sub实例
    Sub.cid = cid++
    Sub['super'] = Super
    // mergeOptions, 继承逻辑的关键所在
    Sub.options = mergeOptions(Super.options, extendOptions)
    if (name) {
      // 处理组件递归调用的情况
      Sub.options.components[name] = Sub
    }
    CachedCtor[SuperId] = Sub
    return Sub
  }
}

这个initExtend方法主要做的事就是初始化了Vue.extend方法, 而Vue.extend, 这个方法我们并不陌生, 它也就是我们注册全局组件的方法之一, 代码也很简单, 总体上看其实就是我们最常见的原型链继承:

  1. 声明一个构造函数VueComponent, 我们发现其函数体内的逻辑和Vue构造函数中的内容一致, 都是调用了this._init方法;
  2. 将Vue的原型对象赋给VueComponent的原型对象, 注意, 这里使用了Object.create方法, 它所创造的对象更加纯净;
  3. 将Sub的原型对象上的constructor修正为Sub;

mergeOptions

注意这里的Sub.options, 前面说了Vue.options代表的是'全局级别的入參', 而它承担了将全局性数据向下传的任务, 那怎么传? 原生Javascript并不支持静态属性被实例继承, 所以, 就要人为实现传递, 而mergeOptions就是传递的核心所在, 它会将Vue.options和Sub.options合并:

// 代码路径: /src/core/util/options.ts
import config from '../config'
import { ASSET_TYPES } from '../../shared/constant'
import { extend, hasOwn } from '../../shared/util'

// 合并策略对象, 初始化时为空对象
const strats = config.optionMergeStrategies

// 合并components/directives/filters, 此处暂时只展示components的情况
function mergeAssets(parent, child) {
  const res = Object.create(parent)
  if (child) {
    extend(res, child)
  } else {
    return res
  }
}
// 将components/directives/filters的合并策略全部定义为mergeAssets
ASSET_TYPES.forEach(type => {
  strats[type + 's'] = mergeAssets
})

// 默认合并策略
const defaultStrat = function (parentVal, childVal) {
  return childVal === undefined ? parentVal : childVal
}

// 合并options
export function mergeOptions (parent, child, vm) {
  // 声明一个空对象
  const options = {}

  let key
  // 根据parent的key值合并options
  for (key in parent) {
    mergeField(key)
  }

  for (key in child) {
    // parent没有才能被合并, 因为遍历
    // parent的key值的时候, 已经合并了parent和
    // child都有的属性
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }

  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key])
  }
  return options
}

// 代码路径: /src/core/config.ts
export default {
  optionMergeStrategies: Object.create(null),
}
export const optionMergeStrategies = Object.create(null)

// 代码路径: /src/shared/util.ts
export function extend (to, from) {
  for (const key in from) {
    to[key] = from[key]
  }
}

const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (value, key) {
  return hasOwnProperty.call(value, key)
}

mergeOptions整体上其实用到了一个策略模式, 首先strats是一个对象, 这个对象中会有不同的属性, 这些属性的键, 一般都是诸如: components、directives、filters、data、computed等全局和局部都可能存在的api; 而这些键所对应的值, 就是合并这些api所对应的合并策略, 所谓策略, 就是一个函数; 我们这里介绍的是组件, 所以重点研究下组件的合并策略(当然, 这也是指令和过滤器的合并策略), 从代码中可以看到, ASSET_TYPES.forEach设置了strats.components的值, 即组件合并策略为mergeAssets

// 组件的合并策略
function mergeAssets(parent, child) {
  const res = Object.create(parent)
  if (child) {
    extend(res, child)
  } else {
    return res
  }
}
// 源码路径: /src/shared/util.ts
// 将from的属性全部复制到to
export function extend (to, from) {
  for (const key in from) {
    to[key] = from[key]
  }
}

可以看到, 首先用Object.create(parent)创建了一个空对象res, parent就在这个对象的原型链上(如不熟悉Object.create可以自行学习, 这里不展开), 这里的parent是什么吗? 没错, 就是Vue.options.components! 通过这种方式, 得到的res其实就是继承了Vue.options.components! 紧接着, 如果存在child, 即组件的options也存在components属性, 则将其直接赋给res, 从而实现了局部和全局components属性的合并! 只不过, 局部组件信息在res上, 而全局组件信息在res的原型链上! 这里要顺带提一嘴, 纵使这一波操作下来, options也还是在构造器上, 只是从Vue.options合并到了Sub.options, 并没有和实例产生什么关系, 所以, mergeOptions后续还会用到! 毕竟, 实例也有options, 只有把实例的options和构造器的options合并了,才算是真正实现了'继承'

initAssetRegisters

了解完了initExtend方法, 接着看initAssetRegisters

// 源码路径: /src/core/global-api/assets.ts
import { ASSET_TYPES } from "../../shared/constant";
import { isPlainObject } from "../../shared/util";
export function initAssetRegisters (Vue) {
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (id, definition) {
      // 如果是组件
      if (type === 'component' && isPlainObject(definition)) {
        definition.name = definition.name || id
        definition = this.options._base.extend(definition)
      }
      this.options[type + 's'][id] = definition
    }
  })
}

initAssetRegisters中, 我们又双叒叕看到了ASSET_TYPES.forEach, 没错, 又是组件/指令/过滤器三剑客, 还是老规矩, 我们只看组件的情况, 我们发现, Vue.components方法, 在这里, 终于被赋予具体逻辑了, 它会确定组件的名字, 即definition.name, 如果无组件名,则以第一个参数id来代替; 再看this.options._base.extend(definition), 其实就是执行我们刚才介绍的Vue.extend方法, 所以, Vue.component方法本质上还是基于Vue.extend方法的! 而Vue.extend执行的结果是一个组件构造器, 它被赋给了this.options[type + 's'][id], 即挂到了全局! 我们可以看到这就是一个全局组件的注册过程, 最终的产物, 就是将一个组件构造器, 赋值给Vue.options.components[组件名]之上

往期回顾

手写简化版Vue(一) 初始化

手写简化版Vue(二) 响应式原理

手写简化版Vue(三) 编译器原理

手写简化版Vue(四) render的实现

手写简化版Vue(五) _update的实现解析

手写简化版Vue(六) 更新队列