博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Vue2.0源码阅读笔记(一):选项合并
阅读量:7120 次
发布时间:2019-06-28

本文共 15718 字,大约阅读时间需要 52 分钟。

  Vue本质是上来说是一个函数,在其通过new关键字构造调用时,会完成一系列初始化过程。通过Vue框架进行开发,基本上是通过向Vue函数中传入不同的参数选项来完成的。参数选项往往需要加以合并,主要有两种情况:

1、Vue函数本身拥有一些静态属性,在实例化时开发者会传入同名的属性。

2、在使用继承的方式使用Vue时,需要将父类和子类上同名属性加以合并。

  Vue函数定义在 /src/core/instance/index.js中。

function Vue (options) {  if (process.env.NODE_ENV !== 'production' &&    !(this instanceof Vue)  ) {    warn('Vue is a constructor and should be called with the `new` keyword')  }  this._init(options)}initMixin(Vue)复制代码

  在Vue实例化时会将选项集 options 传入到实例原型上的 _init 方法中加以初始化。 initMixin 函数的作用就是向Vue实例的原型对象上添加 _init 方法, initMixin 函数在 /src/core/instance/init.js 中定义。

  在 _init 函数中,会对传入的选项集进行合并处理。

// merge optionsif (options && options._isComponent) {    initInternalComponent(vm, options)} else {    vm.$options = mergeOptions(      resolveConstructorOptions(vm.constructor),      options || {},      vm    )}复制代码

  在开发过程中基本不会传入 _isComponent 选项,因此在实例化时走 else 分支。通过 mergeOptions 函数来返回合并处理之后的选项并将其赋值给实例的 $options 属性。 mergeOptions 函数接收三个参数,其中第一个参数是将生成实例的构造函数传入 resolveConstructorOptions 函数中处理之后的返回值。

export function resolveConstructorOptions (Ctor: Class
) { let options = Ctor.options 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}复制代码

  resolveConstructorOptions 函数的参数为实例的构造函数,在构造函数的没有父类时,简单的返回构造函数的 options 属性。反之,则走 if 分支,合并处理构造函数及其父类的 options 属性,如若构造函数的父类仍存在父类则递归调用该方法,最终返回唯一的 options 属性。在研究实例化合并选项时,为行文方便,将该函数返回的值统一称为选项合并的父选项集合,实例化时传入的选项集合称为子选项集合

一、Vue构造函数的静态属性options

  在合并选项时,在没有继承关系存在的情况,传入的第一个参数为Vue构造函数上的静态属性 options ,那么这个静态属性到底包含什么呢?为了弄清楚这个问题,首先要搞清楚运行 npm run dev 命令来生成 /dist/vue.js 文件的过程中发生了什么。

  在 package.json 文件中 scripts 对象中有:

"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",复制代码

  在使用rollup打包时,依据 scripts/config.js 中的配置,并将 web-full-dev 作为环境变量TARGET的值。

// Runtime+compiler development build (Browser)'web-full-dev': {    entry: resolve('web/entry-runtime-with-compiler.js'),    dest: resolve('dist/vue.js'),    format: 'umd',    env: 'development',    alias: { he: './entity-decoder' },    banner},复制代码

  上述文件路径是在 scripts/alias.js 文件中配置过别名的。由此可知,执行 npm run dev 命令时,入口文件为 src/platforms/web/entry-runtime-with-compiler.js ,生成符合 umd 规范的 vue.js 文件。依照该入口文件对Vue函数的引用,按图索骥,逐步找到Vue构造函数所在的文件。如下图所示:

  Vue构造函数定义在 /src/core/instance/index.js中。在该js文件中,通过各种Mixin向 Vue.prototype 上挂载一些属性和方法。之后在 /src/core/index.js 中,通过 initGlobalAPI 函数向Vue构造函数上添加静态属性和方法。

import Vue from './instance/index'import { initGlobalAPI } from './global-api/index'import { isServerRendering } from 'core/util/env'import { FunctionalRenderContext } from 'core/vdom/create-functional-component'initGlobalAPI(Vue)复制代码

  在initGlobalAPI 函数中有向Vue构造函数中添加 options 属性的定义。

Vue.options = Object.create(null)ASSET_TYPES.forEach(type => {    Vue.options[type + 's'] = Object.create(null)})// this is used to identify the "base" constructor to extend all plain-object// components with in Weex's multi-instance scenarios.Vue.options._base = Vueextend(Vue.options.components, builtInComponents)复制代码

  经过这段代码处理以后,Vue.options 变成这样:

Vue.options = {	components: {		KeepAlive	},	directives: Object.create(null),	filters: Object.create(null),  _base: Vue}复制代码

  在 /src/platforms/web/runtime/index.js 中,通过如下代码向 Vue.options 属性上添加平台化指令以及内置组件。

import platformDirectives from './directives/index'import platformComponents from './components/index'// install platform runtime directives & componentsextend(Vue.options.directives, platformDirectives)extend(Vue.options.components, platformComponents)复制代码

  最终 Vue.options 属性内容如下所示:

Vue.options = {    components: {        KeepAlive,        Transition,        TransitionGroup    },    directives: {        model,        show     },    filters: Object.create(null),    _base: Vue}复制代码

二、选项合并函数mergeOptions

  合并选项的函数 mergeOptions/src/core/util/options.js 中定义。

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)  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}复制代码

1、组件命名规则

  合并选项时,在非生产环境下首先检测声明的组件名称是否合乎标准:

if (process.env.NODE_ENV !== 'production') {  checkComponents(child)}复制代码

   checkComponents 函数是 对子选项集合components 属性中每个属性使用 validateComponentName 函数进行命名有效性检测。

function checkComponents (options: Object) {  for (const key in options.components) {    validateComponentName(key)  }}复制代码

  validateComponentName 函数定义了组件命名的规则:

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    )  }}复制代码

  由上述代码可知,有效性命名规则有两条:

1、组件名称可以使用字母、数字、符号 _、符号 - ,且必须以字母为开头。

2、组件名称不能是Vue内置标签 slotcomponent;不能是 html内置标签;不能使用部分SVG标签。

2、选项规范化

  传入Vue的选项形式往往有多种,这给开发者提供了便利。在Vue内部合并选项时却要把各种形式进行标准化,最终转化成一种形式加以合并。

normalizeProps(child, vm)normalizeInject(child, vm)normalizeDirectives(child)复制代码

  上述三条函数调用分别标准化选项 propsinjectdirectives

(一)、props选项的标准化

  props 选项有两种形式:数组、对象,最终都会转化成对象的形式。

  如果props 选项是数组,则数组中的值必须都为字符串。如果字符串拥有连字符则转成驼峰命名的形式。比如:

props: ['propOne', 'prop-two']复制代码

  该props将被规范成:

props: {  propOne:{    type: null  },  propTwo:{    type: null  }}复制代码

  如果props 选项是对象,其属性有两种形式:字符串、对象。属性名有连字符则转成驼峰命名的形式。如果属性是对象,则不变;如果属性是字符串则转变成对象,属性值变成新对象的 type 属性。比如:

props: {  propOne: Number,  "prop-two": Object,  propThree: {    type: String,    default: ''  }}复制代码

  该props将被规范成:

props: {  propOne: {    type: Number  },  propTwo: {    type: Object  },  propThree: {    type: String,    default: ''  }}复制代码

  props对象的属性值为对象时,该对象的属性值有效的有四种:

1、type:基础的类型检查。

2、required: 是否为必须传入的属性。
3、default:默认值。
4、validator:自定义验证函数。

(二)、inject选项的标准化

  inject 选项有两种形式:数组、对象,最终都会转化成对象的形式。

  如果inject 选项是数组,则转化为对象,对象的属性名为数组的值,属性的值为仅拥有 from 属性的对象, from 属性的值为与数组对应的值相同。比如:

inject: ['test']复制代码

  该 inject 将被规范成:

inject: {  test: {    from: 'test'  }}复制代码

  如果inject 选项是对象,其属性有三种形式:字符串、symbol、对象。如果是对象,则添加属性 from ,其值与属性名相等。如果是字符串或者symbol,则转化为对象,对象拥有属性 from ,其值等于该字符串或symbol。比如:

inject: {  a: 'value1',  b: {    default: 'value2'  }}复制代码

  该 inject 将被规范成:

inject: {  a: {    from: 'value1'  },  b: {    from: 'b',    default: 'value2'  }}复制代码
(三)、directives选项的标准化

  自定义指令选项 directives 只接受对象类型。一般具体的自定义指令是一个对象。 directives 选项的写法较为统一,那么为什么还会有这个规范化的步骤呢?那是因为具体的自定义指令对象的属性一般是各个钩子函数。但是Vue提供了一种简写的形式:在 bindupdate 时触发相同行为,而不关心其它的钩子时,可以直接定义自定义指令为一个函数,而不是对象。

  Vue内部合并 directives 选项时,要将这种函数简写,转化成对象的形式。如下:

directive:{  'color':function (el, binding) {    el.style.backgroundColor = binding.value  })}复制代码

  该 directive 将被规范成:

directive:{  'color':{    bind:function (el, binding) {      el.style.backgroundColor = binding.value    }),    update: function (el, binding) {      el.style.backgroundColor = binding.value    })  }}复制代码

3、选项extends、mixins的处理

  mixins 选项接受一个混入对象的数组。这些混入实例对象可以像正常的实例对象一样包含选项。如下所示:

var mixin = {  created: function () { console.log(1) }}var vm = new Vue({  created: function () { console.log(2) },  mixins: [mixin]})// => 1// => 2复制代码

  extends 选项允许声明扩展另一个组件,可以是一个简单的选项对象或构造函数。如下所示:

var CompA = { ... }// 在没有调用 `Vue.extend` 时候继承 CompAvar CompB = {  extends: CompA,  ...}复制代码

  Vue内部在处理选项extends或mixins时,会先通过递归调用 mergeOptions 函数,将extends对象或mixins数组中的对象作为子选项集合父选项集合中合并。这就是选项extends和mixins中的内容与并列的其他选项有冲突时的合并规则的依据。

4、使用策略模式合并选项

  选项的数量比较多,合并规则也不尽相同。Vue内部采用策略模式来合并选项。各种策略方法mergeOptions 函数外实现,环境对象strats 对象。

  strats 对象是在 /src/core/config.js 文件中的 optionMergeStrategies 对象的基础上,进行一系列策略函数添加而得到的对象。环境对象接受请求,来决定委托哪一个策略来处理。这也是用户可以通过全局配置 optionMergeStrategies 来自定义选项合并规则的原因。

三、选项合并策略

  环境对象 strats 上拥有的属性以及属性对应的函数如下图所示:

1、选项el、propsData以及strats对象不包括的属性对象的合并策略

  选项 elpropsData以及图中没有的选项都采用默认策略函数 defaultStrat 进行合并。

const defaultStrat = function (parentVal: any, childVal: any): any {  return childVal === undefined    ? parentVal    : childVal}复制代码

  默认策略比较简单:如果子选项集合中有相应的选项,则直接使用子选项的值;否则使用父选项的值。

2、选项data、provide的合并策略

  选项 dataprovide 的策略函数虽然都是 mergeDataOrFn,但是选项 provide 合并时是向 mergeDataOrFn函数中传入三个参数:父选项、子选项、实例。选项 data 的合并分两种情况:通过Vue.extends()处理子组件选项时、正常实例化时。前一种情况没有实例 vm,向 mergeDataOrFn函数传入两个参数:父选项和子选项;后一种情况则跟选项 provide 传入的参数一样。

  mergeDataOrFn函数代码如下所示,只有在合并 data 选项,且是通过Vue.extends()处理子组件选项时,才会走 if 分支。处理正常的实例化选项 dataprovide 时,都是走 else 分支。

export function mergeDataOrFn (parentVal: any,childVal: any,vm?: Component): ?Function {  if (!vm) {    if (!childVal) {      return parentVal    }    if (!parentVal) {      return childVal    }    return function mergedDataFn () {      return mergeData(        typeof childVal === 'function' ? childVal.call(this, this) : childVal,        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal      )    }  } else {    return function mergedInstanceDataFn () {      const instanceData = typeof childVal === 'function'        ? childVal.call(vm, vm)        : childVal      const defaultData = typeof parentVal === 'function'        ? parentVal.call(vm, vm)        : parentVal      if (instanceData) {        return mergeData(instanceData, defaultData)      } else {        return defaultData      }    }  }}复制代码

  在实例 vm 不存在的情况下,有三种情况:

1、子选项不存在,则返回父选项。

2、父选项不存在,则返回子选项。
3、如果父子选项都存在,则返回函数 mergedDataFn

  函数 mergedDataFn将分别提取父子选项函数的返回值,将该纯对象传入 mergeData 函数,最终返回 mergeData 函数的返回值。如果父子选项都不存在,则不会走到这个函数中,因此不加以考虑。

  为什么前面说在 if 分支中的父子选项都为函数呢?因为走该分支,只能是通过Vue.extends()处理子组件 data 选项时。而当一个组件被定义时, data 必须声明为返回一个纯对象的函数,这样能防止多个组件实例共享一个数据对象。定义组件时, data 选项是一个纯对象,在非生产环境下,Vue会有错误警告。
  在 else 分支中,返回函数 mergedInstanceDataFn ,在该函数中,如果子选项存在则分别提取父子选项函数的返回值,将该纯对象传入 mergeData 函数;否则,将返回纯对象形式的父选项。
  在该场景下 mergeData 函数的作用是将父选项对象中有而子选项对象没有的属性,通过 set 方法将该属性添加到子选项对象上并改成响应式数据属性。
  分析完各种情况,发现选项 dataprovide 策略函数是一个高阶函数,返回值是一个返回合并对象的函数。这是为什么呢?这个原因前面说过,是为了保证各组件实例有唯一的数据副本,防止组件实例共享同一数据对象。
  选项 dataprovide选项合并处理的结果是一个函数,而且该函数在合并阶段并没有执行,而是在初始化的时候执行的,这又是为什么呢?在 /src/core/instance/init.js 进行初始化时有如下代码:

initInjections(vm)initState(vm)initProvide(vm) 复制代码

   函数 initState 有如下代码:

if (opts.props) initProps(vm, opts.props)if (opts.methods) initMethods(vm, opts.methods)if (opts.data) {  initData(vm)} else {  observe(vm._data = {}, true /* asRootData */)}复制代码

  由上述代码可知: dataprovide 的初始化是在 injectprops 之后进行的。在初始化时执行合并函数的返回函数,能够使用 injectprops 的值来初始化 dataprovide 的值。

3、生命周期钩子选项的合并策略

  生命周期钩子选项使用 mergeHook 函数合并。

function mergeHook (  parentVal: ?Array
, childVal: ?Function | ?Array
): ?Array
{ const res = childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal return res ? dedupeHooks(res) : res}复制代码

  Vue官方API文档上说生命周期钩子选项只能是函数类型的,从这段源码中可以看出,开发者可以传入函数数组类型的生命周期选项。因为可以将数组中各函数加以合并,因此传入函数数组实用性不大。

  还有一个点比较有意思:如果父选项存在,必定是一个数组。虽然生命周期选项可以是数组,但是开发者一般传入的都是函数,那么为什么这里父选项必定是数组呢?
  这是因为生命周期父选项存在的情况有两种:Vue.extends()、Mixins。在上面 选项extends、mixins的处理 部分已经说过,处理这两种情况时,会将其中的选项作为子选项递归调用 mergeOptions 函数进行合并。也就说声明周期父选项都是经过 mergeHook 函数处理之后的返回值,所以如果生命周期父选项存在,必定是函数数组。
  函数 mergeHook 返回值如果存在,会将返回值传入 dedupeHooks 函数进行处理,目的是为了剔除选项合并数组中的重复值。

function dedupeHooks (hooks) {  const res = []  for (let i = 0; i < hooks.length; i++) {    if (res.indexOf(hooks[i]) === -1) {      res.push(hooks[i])    }  }  return res}复制代码

  生命周期钩子数组按顺序执行,因此先执行父选项中的钩子函数,后执行子选项中的钩子函数。

4、资源选项(components、directives、filters)的合并策略

  组件 components ,指令 directives ,过滤器 filters ,被称为资源,因为这些都可以作为第三方应用来提供。

  资源选项通过 mergeAssets 函数进行合并,逻辑比较简单。

function mergeAssets (  parentVal: ?Object,  childVal: ?Object,  vm?: Component,  key: string): Object {  const res = Object.create(parentVal || null)  if (childVal) {    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)    return extend(res, childVal)  } else {    return res  }}复制代码

  先定义合并后选项为空对象。如果父选项存在,则以父选项为原型,否则没有原型。如果子选项为纯对象,则将子选项上的各属性复制到合并后的选项对象上。

  前面说过 Vue.options 属性内容如下所示:

Vue.options = {    components: {        KeepAlive,        Transition,        TransitionGroup    },    directives: {        model,        show     },    filters: Object.create(null),    _base: Vue}复制代码

  KeepAliveTransitionTransitionGroup 为内置组件,modelshow 为内置指令,不用注册就可以直接使用。

5、选项watch的合并策略

  选项 watch 是一个对象,但是对象的属性却可以是多种形式:字符串、函数、对象以及数组。

// work around Firefox's Object.prototype.watch...if (parentVal === nativeWatch) parentVal = undefinedif (childVal === nativeWatch) childVal = undefined/* istanbul ignore if */if (!childVal) return Object.create(parentVal || null)if (process.env.NODE_ENV !== 'production') {  assertObjectType(key, childVal, vm)}if (!parentVal) return childValconst ret = {}extend(ret, parentVal)for (const key in childVal) {  let parent = ret[key]  const child = childVal[key]  if (parent && !Array.isArray(parent)) {    parent = [parent]  }  ret[key] = parent    ? parent.concat(child)    : Array.isArray(child) ? child : [child]}return ret复制代码

  因为火狐浏览器 Object 原型对象上拥有watch属性,因此在合并前需要检查选项集合 options 上是否有开发者添加的 watch属性,如果没有,不做合并处理。

  如果子选项不存在,则返回以父选项为原型的空对象。
  如果父选项不存在,先检查子选项是否为纯对象,再返回子选项。
  如果父子选项都存在,则先将父选项各属性复制到合并对象上,然后检查子选项上的各个属性。
  在子选项上而不在父选项上的属性,是数组则直接添加到合并对象上。如果不是数组,则填充到新数组中,将该数组添加到合并对象上。
  父子选项上都存在的属性,将父选项上该属性变成数组格式,再向数组中添加子选项上的对应属性。

6、选项props、methods、inject、computed的合并策略

  选项 propsmethodsinjectcomputed 采用相同的合并策略。选项 methodscomputed 传入时只接受对象形式,而选项 propsinject 经过前面的标准化之后也是纯对象的形式。

if (childVal && process.env.NODE_ENV !== 'production') {  assertObjectType(key, childVal, vm)}if (!parentVal) return childValconst ret = Object.create(null)extend(ret, parentVal)if (childVal) extend(ret, childVal)return ret复制代码

  首先检查子选项是否为纯对象,如果不是纯对象,在非生产环境报错。

  如果父选项不存在,则直接返回子选项。
  如果父选项存在,先创建一个没有原型的空对象作为合并选项对象,将父选项上的各属性复制到合并选项对象上。如果子选项存在,则将子选项对象上的全部属性复制到合并对象上,因此父子选项上有相同属性则以取子选项上该属性的值。最后返回合并选项对象。

7、选项合并策略总结

1、elpropsData 以及采用默认策略合并的选项:有子选项就选用子选项的值,否则选用父选项的值。

2、选项 dataprovide :返回一个函数,该函数的返回值是合并之后的对象。以子选项对象为基础,如果存在子选项上没有而父选项上有的属性,则将该属性转变成响应式属性后加入到子选项对象上。
3、生命周期钩子选项:合并成函数数组,父选项排在子选项之前,按顺序执行。
4、资源选项(components、directives、filters):定义一个没有原型的空合并对象,子选项存在,则将子选项上的属性复制到合并对象;父选项存在,则以父选项对象为原型。
5、选项 watch :子选项不存在,则返回以父选项为原型的空对象;父选项不存在,返回子选项;父子选项都存在,则和生命周期合并策略类似,以子选项属性为主,转化成数组形式,父选项也存在该属性,则推入数组中。
6、选项props、methods、inject、computed:将父子选项上的属性添加到一个没有原型的空对象上,父子选项上有相同属性的则取子选项的值。
7、子选项中 extendsmixins :将这两项的值作为子选项与父选项合并,合并规则依照上述规则合并,最后再分项与子选项的同名属性按上述规则合并。

四、总结

  在合并选项前,先对选项 injectpropsdirectives 进行标准化处理。然后将子选项集合中的extends、mixins作为子选项递归调用合并函数与父选项合并。最后使用策略模式合并各个选项。

如需转载,烦请注明出处:

转载于:https://juejin.im/post/5c8f60f8e51d45768a6e95f0

你可能感兴趣的文章
那些年,我在西安的“遇见”(二)
查看>>
登陆窗体(二)
查看>>
ListCtrl表格的用法
查看>>
CTO对话:云端融合下的移动技术创新
查看>>
bobo说shell:shell脚本编程概述(一)
查看>>
我的友情链接
查看>>
IP地址、子网掩码、网络地址和广播地址的换算
查看>>
Mongodb的启动和停止
查看>>
ActiveDirect批理操作
查看>>
快速为专题添加分享功能
查看>>
查看 MySQL 表使用的存储引擎
查看>>
我的友情链接
查看>>
Java状态模式(State模式)
查看>>
【心路历程】微职位报名华为HCNP认证,妥妥过。。。(嗯,最后有彩蛋)
查看>>
2018.11.07-dtoj-4032-equation
查看>>
我的友情链接
查看>>
HNUSTOJ-1696 简单验证码识别(模拟)
查看>>
Java模拟Delegate
查看>>
我的友情链接
查看>>
linux安装oracle数据库无法启动安装界面的问题
查看>>