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

  • 相关阅读:
    Mac OS使用brew安装memcached
    Mac OS使用brew安装memcached
    Mac OS使用brew安装memcached
    JAVA学习之路 (五) 类
    JAVA学习之路 (五) 类
    JAVA学习之路 (五) 类
    JAVA学习之路 (五) 类
    常用的CSS小技巧
    常用的CSS小技巧
    常用的CSS小技巧
  • 原文地址:https://www.cnblogs.com/lidengfeng/p/10419259.html
Copyright © 2011-2022 走看看