zoukankan      html  css  js  c++  java
  • Vue2.0源码阅读笔记(一):选项合并

      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 options
    if (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<Component>) {
      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 = Vue
    
    extend(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 & components
    extend(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` 时候继承 CompA
    var 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<Function>,
      childVal: ?Function | ?Array<Function>
    ): ?Array<Function> {
      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 = undefined
    if (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 childVal
    const 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 childVal
    const 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://www.cnblogs.com/lidengfeng/p/10419259.html

  • 相关阅读:
    EBS SQL > Form & Report
    oracle sql 优化分析点
    MRP 物料需求计划
    MRPII 制造资源计划
    Barcode128 应用实务
    Oracle SQL语句优化技术分析
    APPSQLAP10710 Online accounting could not be created. AP Invoice 无法创建会计分录
    Oracle数据完整性和锁机制
    ORACLE Responsibility Menu Reference to Other User
    EBS 常用 SQL
  • 原文地址:https://www.cnblogs.com/lidengfeng/p/10419259.html
Copyright © 2011-2022 走看看