extends 和 mixins的实现方式

414 阅读3分钟

使用方式

  • extends的使用方式

    • 允许扩展另一个组件,可以是一个简单的对象或是构造函数,而无需使用extend,其主要是为了便于扩展单文件组件,这和 mixins 类似。

    • 主要功能适合组件相同的,只不过是用在单页面上,而不是全局去使用的

    • var CompA = { ... }
      
      // 在没有调用 `Vue.extend` 时候继承 CompA
      var CompB = {
        extends: CompA,
      }
      
  • Mixins的使用方式

    • 接收个混入对象的数组,这些混入对象可以像正常的实例对象一样包含实例对象,这些选项将会被合并到最终的选项中。

    • Mixin钩子按照传入的循序依次调用,并在调用组件自身的钩子之前被调用

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

实现原理

  • 源码分析
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
   ...格式化一些属性
  if (!child._base) {
    if (child.extends) { // 如果存在extends方法将组件进行合并 child.extends 可以是个构造函数和对象
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) { // child.mixins[i]  是个对象 ,将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
}

这个合并函数的最主要的部分就是mergeField函数中的strats[key]具体的合并方式需要查看option.js来进行查看具体属性的具体方法,下面这些数据

// strats
const strats = config.optionMergeStrategies
  • el的合并方法

    • if (process.env.NODE_ENV !== 'production') {
        strats.el = strats.propsData = function (parent, child, vm, key) {
          if (!vm) {
            warn(
              `option "${key}" can only be used during instance ` +
              'creation with the `new` keyword.'
            )
          }
          return defaultStrat(parent, child)
        }
      }
      
      
      • 这个只是在开发环境之下进行操作,如果没有传入vm就提示信息,并通过defaultStrat方法来将child上的数据直接替换了parent中的数据
  • data的合并方法

    • strats.data = function ( // 将两个对象数据进行合并
        parentVal: any,
        childVal: any,
        vm?: Component
      ): ?Function {
        if (!vm) {
          if (childVal && typeof childVal !== 'function') {
            process.env.NODE_ENV !== 'production' && warn(
              'The "data" option should be a function ' +
              'that returns a per-instance value in component ' +
              'definitions.',
              vm
            )
            return parentVal
          }
          return mergeDataOrFn(parentVal, childVal)
        }
      
        return mergeDataOrFn(parentVal, childVal, vm)
      }
      
      • 如果未传入vm对象:如果子元素不是一个函数,在开发环境中就将提示这个data属性应该是一个函数....,并返回父元素;或则是将父子元素进行合并
      • 如果传入vm对象:将父子元素进行合并
    • //mergeDataOrFn 合并方法
      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 () {
            // instance merge
            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对象:父子元素哪个参数是空的就将返回另一个元素;或者将父子元素的方法指向完成之后对象进行合并返回
      • 有传入vm对象的时候:将父子元素的函数执行之后的结果合并返回
    • // mergeData 具体的对象合并方法
      function mergeData (to: Object, from: ?Object): Object {
        if (!from) return to
        let key, toVal, fromVal
        const keys = hasSymbol
          ? Reflect.ownKeys(from)
          : Object.keys(from)
        for (let i = 0; i < keys.length; i++) {
          key = keys[i]
          // in case the object is already observed...
          if (key === '__ob__') continue
          toVal = to[key]
          fromVal = from[key]
          if (!hasOwn(to, key)) {
            set(to, key, fromVal) // 将to中不存在的from设置到to对象中
          } else if (
            toVal !== fromVal && // 如果两个数据不相同且都是对象的话,然后将元素中的所有进行递归的绑定
            isPlainObject(toVal) &&
            isPlainObject(fromVal)
          ) {
            mergeData(toVal, fromVal)
          }
        }
        return to
      }
      
      • 获取到from中对象的所有属性,并对该属性进行循环操作。如果to对象中不存在该属性,就将直接绑定到该to对象中;如果to对象中 已存在默认是不绑定该属性,如果两对象的属性值是不一样并且都是对象的时候,就将对该对象属性进行循环合并。
  • 生命周期的合并方法

    • export const LIFECYCLE_HOOKS = [
        'beforeCreate',
        'created',
        'beforeMount',
        'mounted',
        'beforeUpdate',
        'updated',
        'beforeDestroy',
        'destroyed',
        'activated',
        'deactivated',
        'errorCaptured',
        'serverPrefetch'
      ]
      
      LIFECYCLE_HOOKS.forEach(hook => {
        strats[hook] = mergeHook
      })
      
    • // 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
      }
      
      • 主要是将父子函数放置到数组中返回,来实现合并
  • component |directive|filter的合并方法

    • export const ASSET_TYPES = [
        'component',
        'directive',
        'filter'
      ]
      
      ASSET_TYPES.forEach(function (type) {
        strats[type + 's'] = 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
        }
      }
      
    • export function extend (to: Object, _from: ?Object): Object {
        for (const key in _from) {
          to[key] = _from[key]
        }
        return to
      }
      
      • 如果childVal中有数据的话,就将parentVal中的绑定所有childVal中的参数,但对于相同属性的数据,childVal将会覆盖parentVal中的数据
  • watch的合并方法

    • strats.watch = function (
        parentVal: ?Object,
        childVal: ?Object,
        vm?: Component,
        key: string
      ): ?Object {
      	...格式验证等
        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
      }
      
      • childVal中的属性会循环绑定到parentVal中,如果parentVal中是存在相同的属性,就将parentValchildVal`中对应的属性值放置进入数组中然后作为属性值
  • props|methods|inject|computed的合并方法

    • strats.props =
      strats.methods =
      strats.inject =
      strats.computed = function (
        parentVal: ?Object,
        childVal: ?Object,
        vm?: Component,
        key: string
      ): ?Object {
        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
      }
      
      • 其合并方式就是后者直接覆盖前者相同属性的数据,不相同的就直接绑定上去既可以
  • provide的合并方法

    • strats.provide = mergeDataOrFn
      
      • data的合并方式一样
// config.optionMergeStrategies
export type Config = {
  optionMergeStrategies: { [key: string]: Function };
};
export default ({
  optionMergeStrategies: Object.create(null), 
}: Config)
// defaultStrat 直接覆盖
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

总结

dataprovide:在合并的时候,如果目标对象不存在该属性将该属性直接绑定到目标对象上面; 但是如果目标对象已经存在了就不绑定,但是如果该属性对象的属性值两个都是对象的话,就需要对这个属性值对象进行递归的合并绑定操作。

生命周期:在合并的时候,将目标对象的钩子函数的属性值替换成 存放着目标对象对应的函数和要绑定对象对应的函数 的数组

component |directive|filter,props|methods|inject|computed :在合并的时候,都是将要绑定对象的属性进行循环,目标对象不存在就直接绑定,存在就直接覆盖

watch:在合并的时候,如果目标对象和合并对象有相同的属性,就将相同属性进行合并,对于不相同的并不会进行绑定。合并的方式主要是将属性值替换成 包含目标对象对象方法和要绑定对象对应函数的 数组