温故而知新,浅入 Vue Mixin 底层原理

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战javascript

前言

本文你将收获:html

  • 混入(mixin) 的时机。
  • 混入(mixin) 对于不一样状况的策略:
    • 函数叠加混入(data、provide)
    • 数组叠加混入(hook、watch)
    • 原型链叠加混入(components,filters,directives)
    • 对象覆盖混入(props,methods,computed,inject )
    • 替换覆盖混入(el,template,propData)

在使用 Vue 开发的时候,常用 混入(mixin) 发现真的好用,混入(mixin) 提供了一种很是灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象能够包含任意组件选项。当组件使用混入对象时,全部混入对象的选项将被“混合”进入该组件自己的选项。vue

虽然好用,可是一直停留在看文档会用,「知其然不知其因此然」,而且在以前组内分享时,有同窗也分享了关于 混入(mixin) 的问题,因此最近有兴趣去看了一下实现原理,发现仍是有点绕,是真的有点绕(菜鸡一枚 😆)。这篇文章分享一下本身的一些探索但愿对你有帮组。整体来讲其实就是探索两个问题:java

  • 何时 混入(mixin)
  • 混入(mixin) 的策略是什么?

咱们带着问题往下看。git

前置的知识

1. 何如使用

  • 全局混入
import Vue from "vue";
import App from "./App.vue";
Vue.config.productionTip = false;
Vue.mixin({
  data() {
    return {
      a: 1
    };
  }
});
new Vue({
  render: (h) => h(App)
}).$mount("#app");
复制代码

测试源码github

  • 局部混入(组件混入)
<template>
  <div class="hello"> <h1>{{ a }}</h1> </div>
</template>

<script> export default { name: "HelloWorld", props: { msg: String, }, mixins: [ { data() { return { a: 2, }; }, }, ], data() { return { // a: 2, }; }, }; </script>
复制代码

测试源码web

2. 基础全局 options 是什么?

基础 options 就是:components、directives、filters 三兄弟,这三兄弟在初始化全局 API 的时候就设置在 Vue.options 上。因此这三个是最早存在全局 options。 数组

何时混入 (mixin) ?

混入分为两种状况。markdown

1.全局 mixin 和 基础全局 options 混入

不过全局混入,须要注意的是,混入的操做应该是在初始化实例以前,而不是以后,这样混入 (mixin) 才能合并上你的自定义 options。app

2. 自定义 options 和 基础全局 options 混入

每个组件在初始化的时候都会生成一个 vm (组件实例)。在建立组件实例以前,全局注册的 options,其实会被传递引用到每一个组件中,目的是将和 全局 options组件 options 合并起来,组件便能访问到全局选项。因此的时机就是建立好组件实例以前。

对于全局注册的 options ,Vue 背后偷偷给组件都合并一个全局选项的引用。可是为保证全局 options 不被污染,又不可能每一个组件都深度克隆一份全局选项致使开销过大,因此会根据不一样的选项,作不一样的处理。下面咱们就来看看混入合并的策略是什么?

混入 (mixin) 的策略是什么?

在这以前,回到上面的两种混入,咱们发现混入合并最后都调用了 mergeOptions 这个方法。这个方法就是混入的重点。

// 合并刚才从类的继承链中获取的配置对象及你本身在代码中编写的配置对象(从第一次合并确定是new Vue(options)这个 options
export function mergeOptions ( parent: Object, child: Object, vm?: Component ): Object {
	...
  // 组件属性中的 props、inject、directive 等进行规范化
  // 验证开发者的代码是否符合规范
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  if (!child._base) {
     // 遍历mixins,parent 先和 mixins 合并,而后在和 child 合并
    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
  // 处理 parent 的 key
  // 先遍历合并 parent 中的 key,存储在 options
  // 初始化时:parent 就是全局选项
  for (key in parent) {
    mergeField(key)
  }
  // 处理 child 的 key
  // 在遍历 child,合并补上 parent 中没有的 key ,存储在 options
  // 初始化时:child 就是组件自定义选项
  for (key in child) {
    if (!hasOwn(parent, key)) { // 排除已经处理过的 parent 中的 key
      mergeField(key)
    }
  }
  // 获得类型的合并函数,进行合并字段
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}
复制代码

源码很长,关键代码在最后的函数,这函数就是 「获得类型的合并函数,进行合并字段」 ,这里的类型多是:'data'、hook、'props'、'methods'、'inject'、'computed'、'provide'等等,也就是类型的不一样,进行的合并策略也是不同的。固然若是都不存在,就走默认的处理 defaultStrat 。

1. defaultStrat

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

组件 options > 全局 options

2. data 的混入策略

data,咱们在开发时,通常使用函数来定义,固然也可使用对象(比较少)。咱们以函数为为主线来讨论混入策略。

这里简单解释一下为何通常状况下,咱们使用函数来定义 data: 在 Vue 中组件是能够复用的,一个组件被建立好以后,就能够被用在其余各个地方,而组件无论被复用了多少次,组件中的 data 数据应该是相互不影响的。基于数据不影响的理念,组件被复用一次,data 数据就应该被复制一次,data 是函数,每个函数都会有本身的存储空间,函数每次执行都会建立本身的执行上下文,相互不影响。函数相似于给每一个组件实例建立一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得全部组件实例共用了一份 data,就会形成一个变了全都会变的结果。

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)
    } else if (
      // 存在相同的属性,就合并对象
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      mergeData(toVal, fromVal)
    }
  }
  return to
}

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

strats.data = function ( parentVal: any, childVal: any, vm?: Component ): ?Function {
  if (!vm) {
    if (childVal && typeof childVal !== 'function') {
      return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }

  return mergeDataOrFn(parentVal, childVal, vm)
}
复制代码

源码很长,若是你一行一行去看,真的难受。抽象一下其实就是:两个 data 函数合并成一个函数返回,data 函数执行返回的数据对象也进行合并。

  • 函数合并为一个
  • 函数返回数据合并,优先级高的被应用

可是注意这里的合并数据也是有优先级的。咱们经过一个例子来看看。

// 全局配置
Vue.mixin({
  data() {
    return {
      a: 1
    };
  }
});
// 子组件
<template> <div class="child"> <h1>{{ a }}</h1> </div> </template>
<script> export default { name: "Child", mixins: [ { data() { return { a: 5, }; }, mixins: [ { data() { return { a: 4, }; }, }, ], }, ], data() { return { a: 6, }; }, }; </script>
复制代码

测试源码

这例子中,设置 4 类 data option 函数:

  • 组件本身的 data 函数,A
  • 组件 mixin data 函数 ,B
  • 组件 mixin,在 mixin data 函数,C
  • 全局 mixin data 函数,D 其实不管这里 嵌套 mixin 多少个 data 函数,最后都只会返回一个合并函数,合并函数返回一个合并的对象,合并对象的合并数据优先级, 组件 data > 组件 mixin data > 组件 mixin -mixin data > ... > 全局 mixin data

3. provide 的混入策略

provide 的混入策略和 data 的混入策略一致。底层都是调用 mergeDataOrFn 函数实现。

4. hook 的混入策略

// Vue 中全部的 hook 函数
export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]

// 为全部的 hook 注册回调,回调都是 mergeHook
LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

// mergeHook 协同 dedupeHooks 的做用就是将 hook 函数存入数组
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
}

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

hook 的混入,相对 data 的混入来讲,要简单一些,就是把全部的钩子函数保存进数组,虽然顺序执行。

// 全局
Vue.mixin({
  created() {
    console.log(1);
  }
});
// 子组件
<template> <div class="child"> </div> </template>
<script> export default { name: "Child", mixins: [ { created() { console.log(3); }, mixins: [ { created() { console.log(4); }, }, ], }, ], created() { console.log(2); }, }; </script>
复制代码

测试源码 hook 混入是存放在数组中,最后就变成了:

[
  全局 mixin hook,
  ... ,
  组件mixin-mixin hook,
  组件mixin hook,
  组件 hook
],
复制代码

执行的时候,按照这个数组 顺序执行

5. watch 的混入策略

watch 的混入策略和 hook 的混入策略思想是一致的,都是按照

[
    全局 mixin watch,
    ... ,
    组件 mixin-mixin watch,
    组件 mixin watch,
    组件 watch
]
复制代码

这个顺序混入合并 watch, 最后执行的时候顺序执行(注意:虽然混入测试和 hook 同样,可是底层实现仍是不同的,这里就不贴源码了)。

6. component、directives、filters 的混入策略

component、directives、filters 这三者是放在一块儿来说哈,主要是这三者合并测试同样,而且这三者最开始初始化全局 API 的时候就设置在 Vue.options 上。

// 中转函数
function mergeAssets ( parentVal: ?Object, childVal: ?Object, vm?: Component, key: string ): Object {
  const res = Object.create(parentVal || null)
  if (childVal) {
    return extend(res, childVal)
  } else {
    return res
  }
}

// 为 component、directives、filters 绑定回调
ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

//
export function extend (to: Object, _from: ?Object): Object {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}
复制代码

这里最重要的就是 「const res = Object.create(parentVal || null) 」 这一行代码,component、directives、filters 混入策略的精髓。

Object.create()方法建立一个新对象,使用现有的对象来提供新建立的对象的proto

什么意思了,简单来讲就是经过使用 Object.create 来建立对象,而且实现继承,两个对象继承混入,经过原型链的方式不会相互覆盖,而是 权重小 被放到 权重大 的原型上 (大佬的实现,就是牛逼)。

<script>
    // 全局 filter
    Vue.filter("g_filter",function (params) {})
    // mixin 的 mixin
    var mixin_mixin_filter={
        filters:{
            mixin_mixin_filter(){}
        }
    }
    // mixin filter
    var mixins_filter={
        mixins:[mixin_mixin_filter],
        filters:{
            mixins_filter(){}
        }
    }
    // 组件 filter
    var vue = new Vue({
        mixins:[mixins_filter],
        filters:{
            self_filter(){}
        }
    })
    console.log(vue.$options);
</script>
复制代码

在实现这个例子演示时,发生了一个小插曲。本想在 codesandbox 实现这个例子的,发如今 codesandbox 上实现的例子,发现和实际不太同样,若是有兴趣能够研究一下,codesandbox.io/s/vue-mixin…

7. props、computed、methods、inject 的混入策略

这四者的混入策略也是同样的,因此放在一块儿来讲。并且它们的混入策略,也相对来讲比较简单。

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

简单的对象合并,key 值相同,优先级高的覆盖优先级低的。组件 对象 > 组件 mixin 对象 > 组件 mixin -mixin 对象 > ... > 全局 mixin 对象。

以 methods 为例:

// 全局配置
Vue.mixin({
  methods: {
    test() {
      console.log(1);
    }
  }
});
// 子组件
<template> <div class="hello"></div> </template>

<script> export default { name: "HelloWorld", props: { msg: String, }, mixins: [ { methods: { test() { console.log(3); }, }, mixins: [ { methods: { test() { console.log(4); }, }, }, ], }, ], methods: { test() { console.log(2); }, }, created() { this.test(); }, }; </script>
复制代码

测试源码

8. el、template、propData 混入策略

这是默认的处理方式,也至关于一种兜底的方案,当上面全部的混入策略不存在的时候,就会用这种兜底方式,如 el,template,propData。他们的混入策略就是权重大的覆盖权重小的。组件 > 组件 mixin > 组件 mixin -mixin > ... > 全局 mixin。

总结

本文带你们一块儿探索了 Vue mixin 的策略,在不一样场景有不一样的混入策略,涉及到 data、provide、钩子函数、watch、component、directives、filters、props、computed、methods、inject、el、template、propData 。从混入的方式来讲,咱们能够总结为 5 个大的方向:

  • 函数叠加混入(data、provide)
  • 数组叠加混入(hook、watch)
  • 原型链叠加混入(components,filters,directives)
  • 对象覆盖混入(props,methods,computed,inject )
  • 替换覆盖混入(el,template,propData)

「知其然知其因此然」,抓着源码研究了好久,也看了不少文章,总结出来,但愿对你有帮助。

若是以为写得还行,帮忙点个赞吧。

参考

相关文章
相关标签/搜索