随着初始化函数的执行,实例的生命周期也开始运转,在初始化函数里可以看到每个模块向实例集成的功能,这些功能的具体内容以后在单独的文章里继续探索。现在来详细看看类初始化函数的详细代码。

头部引用

* 下面代码位于vue/src/core/instance/init.js

import config from '../config'
import { initProxy } from './proxy'
import { initState } from './state'
import { initRender } from './render'
import { initEvents } from './events'
import { mark, measure } from '../util/perf'
import { initLifecycle, callHook } from './lifecycle'
import { initProvide, initInjections } from './inject'
import { extend, mergeOptions, formatComponentName } from '../util/index'
复制代码

头部注入的一些方法是在生命周期运行中开始初始化的功能,之前在核心类实现的文章中有提到过,在这里不展开。config对象是作为基本的配置参数,在不同运行环境里会更改当中的属性值来适应不同的平台需求,在这个文件中只用到了其中的性能检测属性,与具体的类的实现没有太大关联,与引入的markmeasureformatComponentName方法配合主要是做性能评估用的。

在初始化组件的时候主要用到的是工具方法extendmergeOptions

辅助函数 extend

extend函数是一个很简单的为对象扩展属性的方法,代码位于这个文件中vue/src/shared/util.js,具体实现非常基础,看看就好。

/**
 * Mix properties into target object.
 */
export function extend (to: Object, _from: ?Object): Object {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}
复制代码

辅助函数 mergeOptions

mergeOptions函数代码位于 vue/src/core/util/options.js中,它是初始化合并 options 对象时非常重要的函数,为了看明白它在初始化函数里的用途,稍微花点时间来仔细看一下它的具体实现。

// 该函数用于将两个配置对象合并为一个新的配置对象,
// 核心实体既用于实例化也用于继承
/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
// 导出 mergeOptions 函数
// 接收 Object 类型的 parent、child 参数,Component 类型的 vm 参数
// 函数返回对象
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  // 非生产环境时检查 child 对象的 components 属性中是否有不合适的引用组件名称
  // 不合适的组建名主要是指与 Vue 内建 html 标签或保留标签名相同的组件名称如 slot,component
  // 有兴趣了解的可以参照同一文件中的 L246 到 L269 查看具体实现
  // 其中的辅助工具函数位于 src/shared/util.js 的 L94 到 L112
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

// 如果 child 传入的是函数对象,则将函数的 options 属性赋值给 child,确保 child 引用 options
if (typeof child === ‘function’) {
child = child.options
}

// 下面三个函数都是将 child 的各个属性格式化成预定好的对象格式
// 标准化属性
normalizeProps(child, vm)
// 标准化注入
normalizeInject(child, vm)
// 标准化指令
normalizeDirectives(child)
// 定义扩展
const extendsFrom = child.extends
// 如果存在则向下递归合并
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm)
}
// 如果存在 mixins,则合并每一个 mixin 对象
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
// 定义以空对象 options
const options = {}
let key
// 对每一个 parent 中的属性进行合并,添加到 options 中
for (key in parent) {
mergeField(key)
}
// 如果 parent 中不含有 key 属性,则对每一个 child 中 key 属性进行合并
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
// 定义 mergeField 函数,接收 key 参数
function mergeField (key) {
// 如果 strats[key] 有定义好的合并策略函数,则复制给 strat
// 否则将默认的 defaultStrat 方法赋给 strat
const strat = strats[key] || defaultStrat
// 合并属性
options[key] = strat(parent[key], child[key], vm, key)
}
// 返回最终 options 对象
return options
}

复制代码

尽管 mergeOptions 函数的实现有些复杂,但它的作用其实比较明确,就是解决初始化的过程中对继承的类的 options 对象和新传入的 options 对象之间同名属性的冲突,即使用继承的属性值还是新传入的属性值的问题。在代码的一开始官方就已说明它是一个递归函数,可以一并解决添加了扩展内容和使用了 mixins 的场景,总而言之,这个步骤就是确保我们初始化的实例的 options 对象正确唯一。

代码中有几个标准化属性的函数,具体实现也在以上代码的同一文件中,虽然有一堆代码,但实现还是比较简单,主要目的就是把传入的 options 对象的各个属性格式化成基于对象的预定格式,在以后的运行中方便使用。

hasOwn 函数是对 Object.prototype.hasOwnProperty 方法的一个包装,比较简单,需要了解的话就去util 工具函数文件中查看。

值得一提的是 strats 的使用。在代码的一开始的部分就定义 strats 变量,并说明它是用来处理父子选项合并属性的功能。

/**
 * Option overwriting strategies are functions that handle
 * how to merge a parent option value and a child option
 * value into the final value.
 */
const strats = config.optionMergeStrategies
复制代码

对于 elpropsData 属性的合并策略赋予 defaultStrat 函数,该函数的原则是 child 对象属性优先,没有 child 对象属性则返回 parent 的对应属性。

/**
 * Options with restrictions
 */
if (process.env.NODE_ENV !== 'production') {
  strats.el = strats.propsData = function (parent, child, vm, key) {
    if (!vm) {
      warn(
        `option "${key}" can only be used during instance ` +
        'creation with the `new` keyword.'
      )
    }
    return defaultStrat(parent, child)
  }
}
复制代码

datawatchpropsmethodsinjectcomputedprovide、各种钩子函数和ASSET_TYPES里包含的componentdirectivefilter 三个属性都分别定义了相关的合并方法,有兴趣继续了解的同学可以在同一分文件中查看,代码太长但是实现比较基础,所以没什么好详说的,可以关注一下的是某些属性是替换覆盖,而某些属性是合并成数组如各种钩子的监听函数。

初始化内部组件时 options 的合并

对于初始化合并 options 的操作分为了两个方向,一是创建内部组件时的合并,二是创建非内部组件实例时的合并,先来说说内部组件初始化的详细内容,在类的实现中对应着这一句代码 initInternalComponent(vm, options)

// 输出 initInternalComponent 函数
// 接受 Component 类型的 vm 参数和 InternalComponentOptions 类型的 options 参数
// 这里 vm 和 options 分别是创建实例时将要传入的实例对象和配置对象
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
 // 定义 opts,为 opts 和 vm.$options 创建以 vm.constructor.options 为原型的对象
 const opts = vm.$options = Object.create(vm.constructor.options)
 // 以下为手动赋值,目的为了提升性能,因为比通过动态枚举属性来赋值的过程快
 // doing this because it's faster than dynamic enumeration.
 // 定义父虚拟节点 parentVnode 并赋值
 const parentVnode = options._parentVnode
 // 设置 opts 对象 parent 和 _parentVnode 属性
 opts.parent = options.parent
 opts._parentVnode = parentVnode

// 定义 vnodeComponentOptions 并赋值
const vnodeComponentOptions = parentVnode.componentOptions
// 定义 opts 各属性
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag

// options.render 属性存在,则设置 opts 的 render 和 staticRenderFns 属性
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}

复制代码

可以看出 initInternalComponent 函数的内容比较简单,主要是为创建的内部组件的 options 对象手动赋值,提升性能,因为按照官方注释的说法是所有的内部组件的初始化都没有列外可以同样处理。至于什么时候会创建内部组件,这种场景目前还不太了解,能确定的是通常创建 Vue 实例来初始化视图页面的用法是非内部组件性质的。在这里留下一个疑问。

初始化实例时 options 的合并

下面三个函数就是初始化实例合并 options 这条线时用到的方法,后两个函数作辅助用。对应如下带代码。主要是用来解决构造函数的默认配置选项和扩展选项之间的合并问题。

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)
复制代码
// 导出 resolveConstructorOptions 函数,接受 Component 构造函数 Ctor 参数
export function resolveConstructorOptions (Ctor: Class<Component>) {
  // 定义传入的构造函数的 options 属性
  let options = Ctor.options
  // 如果 Ctor.super 存在则执行下面代码,这里是用来判断实例对象是否有继承
  // 如果有的话递归的把继承的父级对象的 options 都拿出来合并
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // 如果父级的 option 变化了则要更新
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // 检查是否有任何后期修改 / 附加选项,这是为了解决之前误删注入选项的问题
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // 如果返回有修改的选项,则扩展 Ctor.extendOptions
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      // 合并继承选项和扩展选项
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      // 设置 options.components[options.name] 的引用
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  // 返回 options
  return options
}

// 以下两个函数是为了解决 #4976 问题的方案
// 定义 resolveModifiedOptions 函数,接受 Ctor 参数,返回 Object
function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
// 定义 modified 变量储存最终要选择保留的属性
let modified
// 分别定义最新、扩展和密封的配置选项
const latest = Ctor.options
const extended = Ctor.extendOptions
const sealed = Ctor.sealedOptions
// 遍历传入的配置选项对象
for (const key in latest) {
// 如果最新的属性与密封的属性不相等,则执行去重处理
if (latest[key] !== sealed[key]) {
if (!modified) modified = {}
modified[key] = dedupe(latest[key], extended[key], sealed[key])
}
}
// 返回 modified
return modified
}

// 定义 dedupe 函数,接收 latest 最新对象,extended 扩展对象,sealed 密封对象
function dedupe (latest, extended, sealed) {
// 合并选项的时候比较最新和密封的属性,确保生命周期钩子不重复
// compare latest and sealed to ensure lifecycle hooks won’t be duplicated
// between merges
// 如果 latest 是数组
if (Array.isArray(latest)) {
// 定义 res 变量
const res = []
// 格式化 sealed 和 extended 为数组对象
sealed = Array.isArray(sealed) ? sealed : [sealed]
extended = Array.isArray(extended) ? extended : [extended]
for (let i = 0; i < latest.length; i++) {
// 返回扩展的选项中存在的最新对象的属性而非密封选项以排除重复选项
// push original options and not sealed options to exclude duplicated options
if (extended.indexOf(latest[i]) >= 0 || sealed.indexOf(latest[i]) < 0) {
res.push(latest[i])
}
}
// 返回包含了扩展选项的数组变量
return res
} else {
// 否则直接返回 latest
return latest
}
}

复制代码

使用 resolveConstructorOptions 函数解决了继承的构造函数的选项之后,新创建的实例 vm 的 $options 对象就是继承选项和创建时传入的 options 选项的合并。其中虽然有很多复杂的递归调用,但是这些函数的目的都是为了确定最终的选项,理解这个目的非常重要。


初始化函数的执行不仅在于开始生命周期的运行,对于 options 对象的各个属性值如何取舍的问题给出了非常复杂但健全的解决方法,这为生命周期正常运行铺垫了非常坚实的基础,有了清晰的 options 选项,之后的功能才能如期顺利执行。在这里也可以看出 Vue 处理各种属性的合并原则,对此有良好的理解可以确保在使用时立即定位遇到的相关问题。

  • Vue.js

    Vue.js 是构建 Web 界面的 JavaScript 库,提供数据驱动的组件,还有简单灵活的 API,使得 MVVM 更简单。 主要特性: 可扩展的数据绑定 将普通的 JS 对象作为 model 简洁…

    109 引用
感谢    赞同    分享    收藏    关注    反对    举报    ...