vue 提供了 mixins 这个 API,可以让我们将组件中的可复用功能抽取出来,放入 mixin 中,然后在组件中引入 mixin,可以让组件显得不再臃肿,提高了代码的可复用性。

如何理解 mixins 呢 ?我们可以将 mixins 理解成一个数组,数组中有单或多个 mixin,mixin 的本质就是一个 JS 对象,它可以有 data、created、methods 等等 vue 实例中拥有的所有属性,甚至可以在 mixins 中再次嵌套 mixins,It’s amazing !

举个简单的栗子:

  1. <div id="app">
  2. <h1>{{ message }}</h1>
  3. </div>
  1. const myMixin = {
  2. data() {
  3. return {
  4. message: 'this is mixin message'
  5. }
  6. },
  7. created() {
  8. console.log('mixin created')
  9. }
  10. }
  11. const vm = new Vue({
  12. el: '#app',
  13. mixins: [myMixin],
  14. data() {
  15. return {
  16. message: 'this is vue instance message'
  17. }
  18. },
  19. created() {
  20. console.log(this.message)
  21. // => Root Vue Instance
  22. console.log('vue instance created')
  23. // => created myMixin
  24. // => created Root Vue Instance
  25. }
  26. })

mixins 与 Vue Instance 合并时,会将 created 等钩子函数合并成数组,mixins 的钩子优先调用,当 data、methods 对象键值冲突时,以组件优先。

PS: 如果对 mixins 的概念还不太清的小伙伴,可以去 vue 官方文档 看一下 vue mixins 的基本概念和用法。

mixins 实现

那 mixins 是如何实现的呢 ?当 vue 在实例化的时候,会调用 mergeOptions 函数进行 options 的合并,函数申明在 vue/src/core/util/options.js 文件。

  1. export function mergeOptions(
  2. parent: Object,
  3. child: Object,
  4. vm?: Component
  5. ): Object {
  6. ...
  7. // 如果有 child.extends 递归调用 mergeOptions 实现属性拷贝
  8. const extendsFrom = child.extends
  9. if (extendsFrom) {
  10. parent = mergeOptions(parent, extendsFrom, vm)
  11. }
  12. // 如果有 child.mixins 递归调用 mergeOptions 实现属性拷贝
  13. if (child.mixins) {
  14. for (let i = 0, l = child.mixins.length; i < l; i++) {
  15. parent = mergeOptions(parent, child.mixins[i], vm)
  16. }
  17. }
  18. // 申明 options 空对象,用来保存属性拷贝结果
  19. const options = {}
  20. let key
  21. // 遍历 parent 对象,调用 mergeField 进行属性拷贝
  22. for (key in parent) {
  23. mergeField(key)
  24. }
  25. // 遍历 parent 对象,调用 mergeField 进行属性拷贝
  26. for (key in child) {
  27. if (!hasOwn(parent, key)) {
  28. mergeField(key)
  29. }
  30. }
  31. // 属性拷贝实现方法
  32. function mergeField(key) {
  33. // 穿透赋值,默认为 defaultStrat
  34. const strat = strats[key] || defaultStrat
  35. options[key] = strat(parent[key], child[key], vm, key)
  36. }
  37. return options
  38. }

为了保持代码简洁,已经将 mergeOptions 函数不重要的代码删除,剩余部分我们慢慢来看。

  1. const extendsFrom = child.extends
  2. if (extendsFrom) {
  3. parent = mergeOptions(parent, extendsFrom, vm)
  4. }

首先申明 extendsFrom 变量保存 child.extends,如果 extendsFrom 为真,递归调用 mergeOptions 进行属性拷贝,并且将 merge 结果保存到 parent 变量。

  1. if (child.mixins) {
  2. for (let i = 0, l = child.mixins.length; i < l; i++) {
  3. parent = mergeOptions(parent, child.mixins[i], vm)
  4. }
  5. }

如果 child.mixins 为真,循环 mixins 数组,递归调用 mergeOptions 实现属性拷贝,仍旧将 merge 结果保存到 parent 变量。

接下来是关于 parent、child 的属性赋值:

  1. const options = {}
  2. let key
  3. for (key in parent) {
  4. mergeField(key)
  5. }
  6. for (key in child) {
  7. if (!hasOwn(parent, key)) {
  8. mergeField(key)
  9. }
  10. }

申明 options 空对象,用来保存属性拷贝的结果,也作为递归调用 mergeOptions 的返回值。这里首先会调用 for…in 对 parent 进行循环,在循环中不断调用 mergeField 函数。

接着调用 for…in 对 child 进行循环,这里有点不太一样,会调用 hasOwn 判断 parent 上是否有这个 key,如果没有再调用 mergeField 函数,这样避免了重复调用。

那么这个 mergeField 函数到底是用来做什么的呢?

  1. function mergeField(key) {
  2. // 穿透赋值,默认为 defaultStrat
  3. const strat = strats[key] || defaultStrat
  4. options[key] = strat(parent[key], child[key], vm, key)
  5. }

mergeField 函数接收一个 key,首先会申明 strat 变量,如果 strats[key] 为真,就将 strats[key] 赋值给 strat。

  1. const strats = config.optionMergeStrategies
  2. ...
  3. optionMergeStrategies: Object.create(null),
  4. ...

strats 其实就是 Object.create(null),Object.create 用来创建一个新对象,strats 默认是调用 Object.create(null) 生成的空对象。

顺便说一句,vue 也向外暴露了 Vue.config.optionMergeStrategies,可以实现自定义选项合并策略。

如果 strats[key] 为假,这里会用 || 做穿透赋值,将 defaultStrat 默认函数赋值给 strat。

  1. const defaultStrat = function(parentVal: any, childVal: any): any {
  2. return childVal === undefined ? parentVal : childVal
  3. }

defaultStrat 函数返回一个三元表达式,如果 childVal 为 undefined,返回 parentVal,否则返回 childVal,这里主要以 childVal 优先,这也是为什么有 component > mixins > extends 这样的优先级。

mergeField 函数最后会将调用 strat 的结果赋值给 options[key]。

mergeOptions 函数最后会 merge 所有 options、 mixins、 extends,并将 options 对象返回,然后再去实例化 vue。

钩子函数的合并

我们来看看钩子函数是怎么进行合并的。

  1. function mergeHook(
  2. parentVal: ?Array<Function>,
  3. childVal: ?Function | ?Array<Function>
  4. ): ?Array<Function> {
  5. return childVal
  6. ? parentVal
  7. ? parentVal.concat(childVal)
  8. : Array.isArray(childVal)
  9. ? childVal
  10. : [childVal]
  11. : parentVal
  12. }
  13. LIFECYCLE_HOOKS.forEach(hook => {
  14. strats[hook] = mergeHook
  15. })

循环 LIFECYCLE_HOOKS 数组,不断调用 mergeHook 函数,将返回值赋值给 strats[hook]。

  1. export const LIFECYCLE_HOOKS = [
  2. 'beforeCreate',
  3. 'created',
  4. 'beforeMount',
  5. 'mounted',
  6. 'beforeUpdate',
  7. 'updated',
  8. 'beforeDestroy',
  9. 'destroyed',
  10. 'activated',
  11. 'deactivated',
  12. 'errorCaptured'
  13. ]

LIFECYCLE_HOOKS 就是申明的 vue 所有的钩子函数字符串。

mergeHook 函数会返回 3 层嵌套的三元表达式。

  1. return childVal
  2. ? parentVal
  3. ? parentVal.concat(childVal)
  4. : Array.isArray(childVal)
  5. ? childVal
  6. : [childVal]
  7. : parentVal

第一层,如果 childVal 为真,返回第二层三元表达式,如果为假,返回 parentVal。
第二层,如果 parentVal 为真,返回 parentVal 和 childVal 合并后的数组,如果 parentVal 为假,返回第三层三元表达式。
第三层,如果 childVal 是数组,返回 childVal,否则将 childVal 包装成数组返回。

  1. new Vue({
  2. created: [
  3. function() {
  4. console.log('冲冲冲!')
  5. },
  6. function() {
  7. console.log('鸭鸭鸭!')
  8. }
  9. ]
  10. })
  11. // => 冲冲冲!
  12. // => 鸭鸭鸭!

项目实践

使用 vue 的小伙伴们,当然也少不了在项目中使用 element-ui。比如使用 Table 表格的时候,免不了申明 tableData、total、pageSize 一些 Table 表格、Pagination 分页需要的参数。

我们可以将重复的 data、methods 写在一个 tableMixin 中。

  1. export default {
  2. data() {
  3. return {
  4. total: 0,
  5. pageNo: 1,
  6. pageSize: 10,
  7. tableData: [],
  8. loading: false
  9. }
  10. },
  11. created() {
  12. this.searchData()
  13. },
  14. methods: {
  15. // 预申明,防止报错
  16. searchData() {},
  17. handleSizeChange(size) {
  18. this.pageSize = size
  19. this.searchData()
  20. },
  21. handleCurrentChange(page) {
  22. this.pageNo = page
  23. this.searchData()
  24. },
  25. handleSearchData() {
  26. this.pageNo = 1
  27. this.searchData()
  28. }
  29. }
  30. }

当我们需要使用时直接引入即可:

  1. import tableMixin from './tableMixin'
  2. export default {
  3. ...
  4. mixins: [tableMixin],
  5. methods: {
  6. searchData() {
  7. ...
  8. }
  9. }
  10. }

我们在组件内会重新申明 searchData 方法,类似这种 methods 对象形式的 key,如果 key 相同,组件内的 key 会覆盖 tableMixin 中的 key。

当然我们也可以在 mixins 中嵌套 mixins,申明 axiosMixin:

  1. import tableMixin from './tableMixin'
  2. export default {
  3. mixins: [tableMixin],
  4. methods: {
  5. handleFetch(url) {
  6. const { pageNo, pageSize } = this
  7. this.loading = true
  8. this.axios({
  9. method: 'post',
  10. url,
  11. data: {
  12. ...this.params,
  13. pageNo,
  14. pageSize
  15. }
  16. })
  17. .then(({ data = [] }) => {
  18. this.tableData = data
  19. this.loading = false
  20. })
  21. .catch(error => {
  22. this.loading = false
  23. })
  24. }
  25. }
  26. }

引入 axiosMixin:

  1. import axiosMixin from './axiosMixin'
  2. export default {
  3. ...
  4. mixins: [axiosMixin],
  5. created() {
  6. this.handleFetch('/user/12345')
  7. }
  8. }

在 axios 中,我们可以预先处理 axios 的 success、error 的后续调用,是不是少写了很多代码。

extend

顺便讲一下 extend,与 mixins 相似,只能传入一个 options 对象,并且 mixins 的优先级比较高,会覆盖 extend 同名 key 值。

  1. // 如果有 child.extends 递归调用 mergeOptions 实现属性拷贝
  2. const extendsFrom = child.extends
  3. if (extendsFrom) {
  4. parent = mergeOptions(parent, extendsFrom, vm)
  5. }
  6. // 如果有 child.mixins 递归调用 mergeOptions 实现属性拷贝
  7. if (child.mixins) {
  8. for (let i = 0, l = child.mixins.length; i < l; i++) {
  9. parent = mergeOptions(parent, child.mixins[i], vm)
  10. }
  11. }

在 mergeOptions 函数中,会先对 extends 进行属性拷贝,然后再对 mixin 进行拷贝,在调用 mergeField 函数的时候会优先取 child 的 key。

虽然 extends 的同名 key 会被 mixins 的覆盖,但是 extends 是优先执行的。

总结

注意一下 vue 中 mixins 的优先级,component > mixins > extends。

我们暂且将 mixins 称作是组件模块化,灵活运用组件模块化,可以将组件内的重复代码提取出来,实现代码复用,也使我们的代码更加清晰,效率也大大提高。

当然,mixins 还有更加神奇的操作等你去探索。