Mixin解析

链接:https://juejin.cn/post/6993115621160386590
本文你将收获:

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

在使用 Vue 开发的时候,经常使用 混入(mixin) 发现真的好用,混入(mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

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

我们带着问题往下看。

前置的知识

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");
  • 局部混入(组件混入)
<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>

2. 基础全局 options 是什么?

基础 options 就是:components、directives、filters 三兄弟,这三兄弟在初始化全局 API 的时候就设置在 Vue.options 上。所以这三个是最先存在全局 options。 [图片上传失败...(image-a3dc9c-1628235953841)]

什么时候混入 (mixin) ?

混入分为两种情况。

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

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

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 [图片上传失败...(image-a664bf-1628235953840)]

    其实无论这里 嵌套 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>

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。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容