Vue源码学习系列03——Vue构造函数解析(一): 选项规范化(normalize)

博客更新地址啦~,欢迎访问:https://jerryyuanj.github.io/

在上一节分析了,Vue的构造函数中,只有一句this._init(options),可见这行代码的重要性。今天我们就来详细的看看这个函数主要干了什么事。为了不那么抽象,我会用一个简单的例子来贯穿整个讲解。这个例子非常简单。


var app = new Vue({
  el: '#app',
  data(){
    return {
      name: 'hello'
    }
  }
})

如上一节所说,这个 _init 方法在 initMixin 中。Ok,我们来看看:

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    // 响应式入口
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

首先,把当前实例(this)赋给vm 变量,在给这个实例的uid自增。接下来我们会看到这么些东西,

let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  startTag = `vue-perf-start:${vm._uid}`
  endTag = `vue-perf-end:${vm._uid}`
  mark(startTag)
}

....一些逻辑....

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  vm._name = formatComponentName(vm, false)
  mark(endTag)
  measure(`vue ${vm._name} init`, startTag, endTag)
}

...其他逻辑...

不难看出,两段 if 的作用,就是计算中间包裹的逻辑代码的性能的。这个我们在以后的分析中会直接跳过,有时间会单独写一节来讲解。这不影响我们分析逻辑代码。

我们继续,然后到了这句代码:

vm._isVue = true

通过注释可以看到,这个_isVue标记是为了不让vue的响应式系统观测它。后面说到响应式原理的时候会遇到。
接着我们就会碰到第一个比较复杂且重要的逻辑了:

// merge options
if (options && options._isComponent) {
  // optimize internal component instantiation
  // since dynamic options merging is pretty slow, and none of the
  // internal component options needs special treatment.
  // 优化内部组件的实例化过程
  // 因为动态的 options 合并是很慢的, 而且内部组件也不需要这些合并操作
  initInternalComponent(vm, options)
} else {
  // 对 $options 赋值
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}

这段逻辑主要是合并options。结合上面的例子,此时我们的:

options = {el: '#app', data(){return {name: 'hello'}}

所以此时走的是 else 分支。那么if分支中是做什么的呢?它判断了_isComponent属性,结合注释,它应该Vue内部处理组件实例化的。先不管,继续走我们的逻辑。此时应该是给 vm.$options 赋值了,但是这个赋值过程是通过一个mergeOptions函数来实现的。这个函数很重要。我们来看看这个函数的实现。

建议你泡杯咖啡☕️或者放松放松,下面的函数包含的逻辑还是挺多的,以防你看一半不想看了,hahah。


合并选项(megreOptions)

该函数位于:src/core/util/options.js

先看下代码:

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 * 
 * 将两个option对象合并到一个新的对象中
 * 核心功能,在初始化和继承中继承
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)
  
  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

这是个核心功能,在vue的实例化和继承的时候都会用到。它接受三个参数,一个 parent对象, 一个 child对象, 一个vue实例vm。返回一个options对象。我们一行一行来看。
首先,在非生产环境下,先检查 child 对象的名称。

if (process.env.NODE_ENV !== 'production') {
   checkComponents(child)
 }

这个checkComponents()方法就在当前文件中,实现很简单,看看:

/**
* Validate component names 检查组件的名字
*/
function checkComponents (options: Object) {
 // 遍历options中的所有components, 检查它们的名称是否合法
 for (const key in options.components) {
   validateComponentName(key)
 }
}

export function validateComponentName (name: string) {
 // 从警告中我们可以看到,组件的名称必须是只能含有如下的内容:数字,短横线(-),字母
 // 并且组件名称必须以字母开头
 if (!/^[a-zA-Z][\w-]*$/.test(name)) {
   warn(
     'Invalid component name: "' + name + '". Component names ' +
     'can only contain alphanumeric characters and the hyphen, ' +
     'and must start with a letter.'
   )
 }
 // 不能是内置标签或保留字
 if (isBuiltInTag(name) || config.isReservedTag(name)) {
   warn(
     'Do not use built-in or reserved HTML elements as component ' +
     'id: ' + name
   )
 }
}

在本例中,我们的child没有components,所以这里也就不需要检查了。继续往下看:

if (typeof child === 'function') {
   child = child.options
 }

我们当前的例子也不满足,因为我们的child此时是object,所以也不需要管。
接下来是三个normalize:

 normalizeProps(child, vm)
 normalizeInject(child, vm)
 normalizeDirectives(child)

normalize是什么意思呢?规范化。为什么要规范化?因为它提供了多种配置方式供开发者使用,内部想要统一处理就必须要做规范化。这也是一种编程思想。我们先来分析这三个normalize方法。

normalizeProps

拿我们熟悉的props来举例子,下面的方式都是正确的配置:

props: ['name']
 props: {
   name: String
 }
props: {
 name: {
   type: String,
   required: true
 }
}

如果vue内部要用到这个props,那么它该如何确定props的类型呢?是的,几个if…else的确能解决问题,但是如果配置的方式多了,或者处理这个props的地方多了,对于维护来说,是非常可怕的。所以,最好的方式是将他们处理成相同的数据格式,一劳永逸。这就是要介绍的 normalize。我们先看对props的normalize:

/**
* Ensure all props option syntax are normalized into the
* Object-based format.
* 
* 确保所有的 props 选项语法都被规范化成对象格式的
*/
function normalizeProps (options: Object, vm: ?Component) {
 const props = options.props
 if (!props) return
 const res = {}
 let i, val, name
 // props 是数组的情况
 if (Array.isArray(props)) {
   i = props.length
   // 遍历数组
   while (i--) {
     val = props[i]
     // 如果数组中的某一项是字符串的话,名字转成驼峰式的
     // 接着给 res 添加该属性,默认的type=null
     // 即: ['first-name', 'last-name'] 会被转成
     // {firstName: {type: null}, lastName: {type: null}}
     if (typeof val === 'string') {
       name = camelize(val)
       res[name] = { type: null }
     // 如果数组中某项不是字符串的话会报错
     } else if (process.env.NODE_ENV !== 'production') {
       warn('props must be strings when using array syntax.')
     }
   }
 // props 是对象的情况
 } else if (isPlainObject(props)) {
   // 遍历key
   for (const key in props) {
     // 拿到值
     val = props[key]
     // 把key驼峰化
     name = camelize(key)
     // 如果值是对象的话,就用它作为值;如果不是,设置 {type: null}
     // 如上面的第二个例子:props: {name: String} 走的就是第二个条件
     // 第三个例子:props:{name: {type: String}} 就是使用自己
     res[name] = isPlainObject(val)
       ? val
       : { type: val }
   }
 // 都不满足,警告
 } else if (process.env.NODE_ENV !== 'production') {
   warn(
     `Invalid value for option "props": expected an Array or an Object, ` +
     `but got ${toRawType(props)}.`,
     vm
   )
 }
 // 把最终规范化的结果赋给 options.props 
 options.props = res
}

注释写的很详细了,对于上面的三种情况,最终会分别规范化成这样的对象格式:

props: { name: {type: null} }
props: {name: {type: String}}
props: {name: { type: String, required: true }}
normalizeInject

再来看看对inject的规范化再来看看对inject的规范化normalizeInject(关于inject/provide可以参考https://cn.vuejs.org/v2/api/#provide-inject)

/**
 * Normalize all injections into Object-based format
 */
function normalizeInject (options: Object, vm: ?Component) {
  // 先缓存下这个 inject 对象
  const inject = options.inject
  if (!inject) return
  // 再将这个 inject 对象置空,并且赋值给 normalized 变量
  // 这样做的好处是,此时 normalized 和 options.inject 都指向同一个对象,
  // 下面对 normalized 的操作实际上就是对 options.inject 的操作了
  const normalized = options.inject = {}
  // 开始对一开始缓存的 inject 对象做判断了
  if (Array.isArray(inject)) { // 数组
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] }
    }
  } else if (isPlainObject(inject)) { // 对象
    for (const key in inject) {
      const val = inject[key]
      normalized[key] = isPlainObject(val)
        ? extend({ from: key }, val) // 如果值是一个对象的话,给它添加一个from属性
        : { from: val }
    }
  } else if (process.env.NODE_ENV !== 'production') { // 如果不是数组或对象的话,报个错
    warn(
      `Invalid value for option "inject": expected an Array or an Object, ` +
      `but got ${toRawType(inject)}.`,
      vm
    )
  }
}

这里要结合 inject/provide 的文档来看了,inject的值可以是数组或者对象,跟我们的props差不多。所以两个判断分支做的事情也差不多。这里我们就直接以两个例子看看:

inject: ['foo', 'bar']
inject: {
    foo: { default: 'hello' }
}

第一个很显然走的是数组的分支,在循环遍历数组以后,给normalized弄成了这样子(正如在注释中提到的,这里的normalized对象就是inject对象),所以我们的inject对象就成了:

inject: { 'foo': {from: 'foo'}, 'bar': {from: 'bar'} }

第二个例子走的是对象的分支,就会给我们的inject变成:

inject: { foo: {from: 'foo', default: 'hello'}}

怎么样,是不是跟props差不多,没什么复杂的地方。

normalizeDirectives

最后再看看 normalizeDirectives ,规范化指令。对比上面两种,这个就相对简单些了:

/**
 * Normalize raw function directives into object format.
 */
function normalizeDirectives (options: Object) {
  const dirs = options.directives
  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key]
      if (typeof def === 'function') { // 会把函数定义的指令转成对象形式
        dirs[key] = { bind: def, update: def }
      }
    }
  }
}

可以看到,代码很简单,我们还是以一个例子来看看:

directives: {
  focus: {
    inserted(){...},
    update(){....}
  },
  remove(){....}
}

可以看到,上面的directives定义了两个指令,一个focus,一个remove。其中focus是以对象形式定义的,remove是以函数形式定义的。经过normalizeDirectives处理后,会把以函数形式定义的规范化成以对象形式定义的,也就是把这个函数作为bind钩子和update钩子的回调

directives: {
  focus: {
    inserted(){...},
    update(){....}
  },
  remove:{
    bind(){...}
    update(){...}
  }
}

Ok,三个normalize介绍完了,没有想象中那么难理解吧。在本例中,由于不想在分析的时候弄的太复杂,就没有定义props, injects, directives这些选项,但是上面的三段分析,对我们理解 Vue 在处理这些属性时的内部机制是很有帮助的,所以才花了这么多篇幅介绍。

好了,normalize完以后,接下来到了这一段代码:

// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) {
  if (child.extends) {
    parent = mergeOptions(parent, child.extends, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
}

这段代码主要是用来处理 extendsmixin 的,递归调用自身(mergeOptions方法)。没有什么好说的。而且本例中,child._base = Vue, 所以这段逻辑是不会进来的。以后分析到的时候再说。

总结:本节主要介绍了Vue在初始化的时候的一个重要的步骤,选项规范化(normalize)。它的主要作用就是将Vue暴露给开发者的props, injects, directives的配置方式,在内部做了一个统一处理——都转成对象。这样不论在内部操作这些属性的时候,就可以按照一致的标准去处理,而不用分情况来处理不同风格的配置。

(本来打算这节把merge options的内容也放进来,但是写到后面发现太多了,就放到下一节了)

你可能感兴趣的:(前端,vue,vue2源码学习)