VUE源码学习第四篇--new Vue都干了啥(options合并)

一、第一部分:属性设置

第一部分比较简单,主要设置vm的两个属性。

vm._uid = uid++

定义了vm唯一标识_uid属性,每次new Vue都会递增。

 // a flag to avoid this being observed
 vm._isVue = true

定义vm的_isVue属性为true,这个属性从注释看是为了vm对象避免被observed,大家可以先认为和数据响应有关,后续再关注。

其实第一部分还有一段代码,

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

和第三部分的这段遥相呼应,主要作用是测试中间代码的执行性能,有兴趣的同学可以了解下window.performance

/* 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)
    }

好了,接下来就是本章节的重点部分了。

二、第二部分:options合并

第二部分代码是对options的合并。目的是将相关的属性和方法都放到vm.$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
      )
    }

    当满足第一个条件,即是component组件时调用initInternalComponent方法,由于当前是new Vue对象,不是组件,这部分暂时不表,等组件部分再介绍。

     那进入另一个条件分支,调用mergeOptions方法实现options的合并,该方法有三个参数,第一个是resolveConstructorOptions方法返回值(vm.constructor的options),第二是new Vue时传入的值(自定义的options),第三个是vue对象本身。mergeOptions就是通过一系列的合并策略,将Vue的构造函数以及自定义的options进行合并

1、resolveConstructorOptions

进入resolveConstructorOptions方法

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  //如果不是vue的子类(vue.extend),则undefined
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // 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)
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

  该方法的入参是vm.constructor,即构造函数Vue,如果对原型相关知识不熟悉,可以参考我的文章ES6系列教程第五篇--Class基本知识

执行完第一句,我们来看下Ctor.options(即Vue.options)

在这之前,我们没有定义任何这些属性,那这些怎么来的,我们前面介绍了Vue对象是进过层层封装的。

我们从src/platforms/web/entry-runtime-with-compiler.js,看看对options的配置。

import Vue from './runtime/index'
...
export default Vue

该层封装没有对options设置,暂时不表,继续跟踪runtime/index.js

import Vue from 'core/index'
...
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
....
export default Vue

该层在Vue.options上扩展了两个属性,分别为directives以及components

继续跟踪core/index.js,这个文件我们在上一章节分析过,其中调用了initGlobalAPI方法。

...
//Vue.options初始化
Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })
  Vue.options._base = Vue

  //扩展构建Vue.options.components
  extend(Vue.options.components, builtInComponents)
...

终于找到了源头,对Vue.options属性对象进行了初始化,

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

Vue中对象的初始化很多使用了Object.create(null)语句,这种方式创建的对象,其原型为null,对象更纯净些。

接下来将_base设置为Vue对象本身,并扩展了compontents对象。

我们来梳理下Vue.options构造过程。

再回到resolveConstructorOptions方法,继续执行,判断Ctor.super,用Vue.extend构造子类时,就会添加一个super属性,由于没有使用extend方法,Ctor.super为undefined,后面我们再做分析。

一句话总结,resolveConstructorOptions返回构造函数的options。

2、mergeOptions

mergeOptions位于core/util/options.js,总览下代码

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    //1、合法性校验检查组件名称的合法性
    checkComponents(child)
  }

  //如果传入的是类型是function,则取其options
  if (typeof child === 'function') {
    child = child.options
  }
  //2、格式规整,格式化props
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)
  //3、extends与mixins处理
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
  //4、使用不同的策略进行合并
  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
}

该方法有三个入参,parent为resolveConstructorOptions返回的值(Vue的构造函数的options),child是我们new Vue时传入的数组对象(自定义的options),vm是Vue对象本身。方便起见,下面我们就用parent和child来称呼这两种options。

(1)合法性校验

if (process.env.NODE_ENV !== 'production') {
    //检查组件名称的合法性
    checkComponents(child)
  }

这段主要是包含的组件名称的合法性校验, 包括不能与html标签冲突,名称包含字符,数字,连接符,并要以字母开头等。

var vm = new Vue({
    el:"#app",
    ...
    components:{
      childComponent,
      secondComponent
    }
})

如果创建对象时传入的参数type为function,则取其options

 //如果传入的是类型是function,则取其options
  if (typeof child === 'function') {
    child = child.options
  }

(2)输入规整 

接下代码是对下面的child属性格式化,对各种输入进行规整,为何有这步呢?由于child是自定义的,在开发时有多种写法,那么就有必要在运行前做个统一。

//格式化
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

 我们以normalizeProps为例,具体看下如何规整。先回顾下props,通过props实现对子组件的数据传递,vue对props的定义有两种实现方式。

    //数组模式
    props: ['title', 'likes', 'isPublished', 'commentIds', 'author'],
    ...
    //对象模式
    props:{
      title:{
        type:String,
        default:"this is zte phone"
      },
      likes:{
        type:String,
        default:"this like phone"
      }
    }

 normalizeProps代码:

function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  if (Array.isArray(props)) {//对数组的处理
    ....
  } else if (isPlainObject(props)) {//对对象的处理
    ...
  } 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 = res
}

 这段代码比较清晰,分别对两种不同模式的props进行处理,注意下用isPlainObject判断是否简单对象,是指用字面量或者new Object创建的对象。处理后结果如下所示,这样两种写法在实现了结构上的统一。

normalizeInject是对Inject的处理,Inject不怎么常用,大家可以参考下inject,处理方式与normalizeProps类似,大家可以自行学习下。

normalizeDirectives是对指令函数简写的处理,如果有不熟悉的可以参考指令章节,我们增加个指令

var vm = new Vue({
    el:"#app",
	data:{
        msg:'tttt'
	},
    directives:{
      color:function (el, binding) {
          el.style.backgroundColor = binding.value
        }
    }
})

处理后结构如下,将方法自动赋值给bind和updata钩子。

(3)extends与minxins处理

接下的一段代码就是对extends和mixins属性的处理。递归调用mergeOptions方法,将入参中的extends与mixins合并到parent上。

//对extends以及mixins属性的处理
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }

判断是否有extends与mixins属性,并调用mergeOptions进行递归合并,我们先来看下这两个属性的用法。

  const mixins1={
    created(){
      console.log('mixins created');
    }
  }
  const mixins2 = {
    created () {
      console.log('mixins2 created')
    }
  }

  const extend = {
    created () {
      console.log('extends created')
    }
  }

var vm = new Vue({
    el:"#app",
    mixins: [mixins1, mixins2],
    extends: extend,
    created () {
      console.log('组件 created')
    }
})

需要注意的是mixins属性接受的数组,而extends只能接受一个对象。执行这段代码的结果是:

执行的顺序,entends要优于mixins,mixins要优于Vue对象定义。所以created方法要按照这个顺序在options中保存下。

对extends以及mixins对象的遍历合并后,parent的对象最终如下:

大家可能会有疑惑,为何要将extend与mixins设置到parent上呢,这些为了后面的策略合并准备的。

(4)合并策略

到此,child与parent上的数据都已准备完毕,接下来就是对child和parent的上相同的属性的合并,以上面的created为例,Vue对象(child)定义的created要与Vue构造函数(parent)的created如何合并,是替换,还是组合?

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

对每种属性(key)有不同的合并策略strat[key],我们以钩子函数的合并为例。定义钩子函数的合并策略的处理方法为megeHook

var LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured'
];

...

LIFECYCLE_HOOKS.forEach(function (hook) {
  strats[hook] = mergeHook;
});

我们继续看下megeHook函数定义

function mergeHook (
  parentVal,
  childVal
) {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}

这是个嵌套的三元表达式,判断childVal是否值,如果没有则直接返回parentVal,否则继续;判断parentVal是否有值,如果有值则concat连接,否则继续;判断childVal是否是数组,如果是,直接返回,否则转成array。

上面例子中的created钩子函数,在parent和child上都有,所以需要concat连接。我们看下情况是否如此:

created数组增加了child定义的方法,个数由3个变成了4个。

对上面的疑惑是不是有了解答,extends与mixins中定义的各类属性设置到parent上后,就可以通过统一的过程实现与对象中的定义的属性合并。

对其他属性的合并策略我们就不一一分析代码,总体原则:parent与child两者中只有一个有值,则就直接使用该值,如果两者都有,则视情况:

beforeCreate/created/...:child的属性通过contact连接到parent后面。

watch:child的属性通过contact连接到parent后面。

props/methods/computed:child中属性值的覆盖parent的属性值。

data/provide:child中的属性值覆盖parent的属性值。

component/directive/filter:child中的属性值覆盖parent的属性值。

合并完成后返回options,赋值为vm.$options,至此,第二部分完成。

四、总结

本章节重点介绍了options的合并,合并的目的是将相关的属性和方法都放到vm.$options中,为后续的调用做准备工作。

   options的属性有两个来源,一个是Vue的构造函数(parent),通过resolveConstructorOptions获取和构建的;一个是new Vue是传入的(child)。

  准备工作完成后,通过各属性不同的合并策略,对parent和child相同属性进行合并。最终生成统一的options。

上一篇:VUE源码学习第三篇--new Vue都干了啥(概述)

发布了33 篇原创文章 · 获赞 95 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/tcy83/article/details/84800979