mixin的意思是混入,是指将事先配置的选项混入到组件中,然后与组件中的对象和方法进行合并,也就是对组件进行了扩展,也可以理解为是将一段重复的代码进行抽离,然后通过混入的形式达到复用的效果,它有两种混入形式,分别是 Vue.mixin({})全局注册和组件的 mixins选项,那么在 Vue 中,他们是怎么进行合并,具体实现是怎么样呢,这篇文章将进行讲解,相信你一定会有所收获;

首先是入口文件,在全局 api 的初始化文件中,通过调用 initMixin进行注册:

// src/core/global-api/mixin.js
import { mergeOptions } from '../util/index'export function initMixin (Vue: GlobalAPI) {Vue.mixin = function (mixin: Object) {this.options = mergeOptions(this.options, mixin)return this}
}

接着看 mergeOptions的具体实现:

// src/core/util/options.js
export function mergeOptions (parent: Object,child: Object,vm?: Component
): Object {// 校验 mixin 组件属性的规范if (process.env.NODE_ENV !== 'production') {checkComponents(child)}if (typeof child === 'function') {child = child.options}// 规范化 props / inject / directivesnormalizeProps(child, vm)normalizeInject(child, vm)normalizeDirectives(child)// _base 是标识 extends 和 mixins 是属于子选项的,确保它不是 mergeOptions 的结果// _base 在 initGlobalAPI 时给 Vue 本身注入的一个标识 Vue.options._base = Vueif (!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)}}}// 定义一个变量,存放 merge 后的结果const options = {}let key// 先遍历前者的选项,合并到 options 里for (key in parent) {// 合并的主要核心,通过策略模式来进行合并mergeField(key)}for (key in child) {// 如果后者里面还存有前者没有的选项,则进行合并if (!hasOwn(parent, key)) {mergeField(key)}}function mergeField (key) {// 根据 key 值来确认采用何种策略const strat = strats[key] || defaultStratoptions[key] = strat(parent[key], child[key], vm, key)}return options
}

mergeOptions方法中可以看出它大致分为几个步骤:

  • 校验混入对象的 components选项;
  • propsinjectdirectives进行规范化处理;
  • 判断混入对象是否有 mixinsextends选项,有则递归进行合并;
  • 定义一个 options,作为 merge的结果集;
  • 将前者的选项通过策略模式合并到 options
  • 后者中如果还存在其他的选项,则通过策略模式合并到 options
  • 返回合并的结果 options

从上面的代码来看,主要的核心点在于策略模式,也就是对象和方法之间的合并规则,我们接着一个一个看:

data 属性合并

// src/core/util/options.js
strats.data = function (parentVal: any,childVal: any,vm?: Component
): ?Function {if (!vm) {// 如果后者的 data 属性不是一个 function 的形式返回,则直接返回前者的 dataif (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}// 调用 mergeDataOrFn 进行合并return mergeDataOrFn(parentVal, childVal)}// 调用 mergeDataOrFn 进行合并return mergeDataOrFn(parentVal, childVal, vm)
}export function mergeDataOrFn (parentVal: any,childVal: any,vm?: Component
): ?Function {if (!vm) {// 后者没有 data 属性,则直接返回前者的 dataif (!childVal) {return parentVal}// 前者没有 data 属性,则直接返回后者的 dataif (!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): childValconst defaultData = typeof parentVal === 'function'? parentVal.call(vm, vm): parentVal// 如果后者存在 data 属性,则返回 merge 之后的结果,否则直接返回前者的 data if (instanceData) {return mergeData(instanceData, defaultData)} else {return defaultData}}}
}// to 表示后者的 data,from 表示前者
function mergeData (to: Object, from: ?Object): Object {// 前者没有,则直接返回后者的 data if (!from) return tolet key, toVal, fromVal// 获取 data 中的 keyconst keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from)for (let i = 0; i < keys.length; i++) {key = keys[i]// 如果该属性已经被观察,则直接下一步if (key === '__ob__') continue// 根据 key 获取对应的值toVal = to[key]fromVal = from[key]// 如果后者没有该变量,则直接加到后者的 data// 如果两个值都是对象,但是值不相等,则进行对象的合并if (!hasOwn(to, key)) {set(to, key, fromVal)} else if (toVal !== fromVal &&isPlainObject(toVal) &&isPlainObject(fromVal)) {mergeData(toVal, fromVal)}}// 返回合并的结果return to
}

data属性的合并主要是 mergeDataOrFnmergeData

  • mergeDataOrFn的实现很简单,主要是判断两者之间是否有一个没有 data属性,是则直接返回有 data属性的一方,否则返回一个两者合并结果的函数;
  • mergeData是两者合并的过程:
    • 如果是后者没有的变量,则把该变量加入到后者的 data属性;
    • 如果两个值都是对象,但是值不相等,则进行对象的合并;
    • 如果 key值相等,但是值不相等,则以后者的为准;

生命周期相关的合并

// src/shared/constants.js
// 定义一个数组存放生命周期钩子函数的 key
export const LIFECYCLE_HOOKS = ['beforeCreate','created','beforeMount','mounted','beforeUpdate','updated','beforeDestroy','destroyed','activated','deactivated','errorCaptured','serverPrefetch'
]// src/core/util/options.js
LIFECYCLE_HOOKS.forEach(hook => {strats[hook] = mergeHook
})
function mergeHook (parentVal: ?Array<Function>,childVal: ?Function | ?Array<Function>
): ?Array<Function> {// 判断后者是否存在生命周期钩子函数// 是 --> 判断前者是否有定义钩子函数//       是 --> 直接将后者的生命周期钩子函数拼接到后面//      否 --> 直接返回后者的生命周期钩子函数// 否 --> 直接返回自身的生命周期钩子函数const res = childVal? parentVal? parentVal.concat(childVal): Array.isArray(childVal)? childVal: [childVal]: parentValreturn 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
}

生命周期钩子函数的合并比较简单,先判断后者的生命周期钩子函数是否存在,是则将后者的相应生命周期钩子函数拼接到前者后面,否则以数组的形式返回后者的相应生命周期钩子;

components、directives、filters 的合并

// src/shared/constants.js
export const ASSET_TYPES = ['component','directive','filter'
]// src/core/util/options.js
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}
}

componentsdirectivesfilters的合并是先拷贝一份原先的属性对象,然后对拷贝的对象进行扩展,它会遍历传入的对象,将传入对象的属性赋值给拷贝对象,如果有重复的,则后者覆盖前者;

watch 合并

// src/core/util/env.js
export const nativeWatch = ({}).watch// src/core/util/options.js
strats.watch = function (parentVal: ?Object,childVal: ?Object,vm?: Component,key: string
): ?Object {if (parentVal === nativeWatch) parentVal = undefinedif (childVal === nativeWatch) childVal = undefined// 如果后者没有 watch 属性,则返回一个创建的对象if (!childVal) return Object.create(parentVal || null)if (process.env.NODE_ENV !== 'production') {assertObjectType(key, childVal, vm)}// 如果前者没有 watch 属性,则返回后者的 watchif (!parentVal) return childValconst ret = {}// 将前者的 watch 赋值给 retextend(ret, parentVal)for (const key in childVal) {let parent = ret[key]const child = childVal[key]if (parent && !Array.isArray(parent)) {parent = [parent]}// 将后者的 watch 拼接到前者的 watchret[key] = parent? parent.concat(child): Array.isArray(child) ? child : [child]}return ret
}

watch的合并和生命周期钩子函数有点相似,都是把后者的属性拼接到前者属性的后面;

props、methods、inject、computed、provide 的合并

// src/core/util/options.js
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 childValconst ret = Object.create(null)extend(ret, parentVal)if (childVal) extend(ret, childVal)return ret
}
strats.provide = mergeDataOrFn

propsmethodsinjectcomputed的合并都是先定义一个对象 ret,先遍历前者的属性或方法,对 ret进行扩展,如果后者有相应的propsmethodsinjectcomputed等属性,则将后者的覆盖前者的属性或方法;

provide则是跟 data合并一样,调用 mergeDataOrFn进行合并;

默认策略合并

// src/core/util/options.js
const defaultStrat = function (parentVal: any, childVal: any): any {return childVal === undefined ? parentVal : childVal
}

如果存在策略对象 strats没定义的策略,则采用默认策略,默认策略是指如果后者有值,则直接返回后者,否则返回前者;

总结

mixin是平常开发中常见的一种代码复用手段,它的作用类似于 react中的高阶组件,mixin具体实现是通过采用策略模式来将数据进行合并:

  • data、provide:后者的值将对前者的值进行扩展,相同属性名(非对象)则以后者的属性值为准,如果两者的值是对象,但值不相等,则继续进行合并,
  • 生命周期钩子函数:将后者的生命周期钩子函数拼接到前者的生命周期钩子函数,调用时依次执行;
  • components、filters、directives:对前者的属性进行拷贝扩展,属性相同则后者覆盖前者;
  • watch:与生命周期钩子函数类似,将后者的 watch拼接到前者的 watch后面;
  • props、methods、inject、computed:定义一个对象 ret,遍历前者的属性或方法,对 ret进行扩展,再遍历后者的属性或方法,后者将覆盖前者的属性或方法;
  • 默认策略:策略中没有定义的策略,后者有则返回后者,否则返回前者;

Vue 源码之 mixin 原理相关推荐

  1. 【Vue原理】Vue源码阅读总结大会

    专注 Vue 源码分享,为了方便大家理解,分为了白话版和 源码版,白话版可以轻松理解工作原理和设计思想,源码版可以更清楚内部操作和 Vue的美,喜欢我就关注我的公众号,好吧兄弟,不会让你失望的 阅读源 ...

  2. 【Vue原理】Vue源码阅读总结大会 - 序

    [Vue原理]Vue源码阅读总结大会 - 序 阅读源码准备了什么 1.掌握 Vue 所有API 2.JavaScript 扎实基础 3.看完 JavaScript 设计模式 4.学会调试 Vue 源码 ...

  3. [Vue源码分析]自定义事件原理及事件总线的实现

    最近小组有个关于vue源码分析的分享会,提前准备一下- 前言: 我们都知道Vue中父组件可以通过 props 向下传数据给子组件:子组件可以通过向$emit触发一个事件,在父组件中执行回调函数,从而实 ...

  4. [Vue源码分析] v-model实现原理

    最近小组有个关于vue源码分析的分享会,提前准备一下- 前言: 我们都知道使用v-model可以实现数据的双向绑定,及实现数据的变化驱动dom的更新,dom的更新影响数据的变化.那么v-model是怎 ...

  5. vue 源码自问自答-响应式原理

    vue 源码自问自答-响应式原理 最近看了 Vue 源码和源码分析类的文章,感觉明白了很多,但是仔细想想却说不出个所以然. 所以打算把自己掌握的知识,试着组织成自己的语言表达出来 不打算平铺直叙的写清 ...

  6. Vue响应式原理 vue源码(十一)

    前言 看过很多讲响应式的文章,大多都是告诉你们,有Observer,Dep,Wathcer类,Object.definePorperty,先会触发get中的dep.depend收集依赖,然后数据改变时 ...

  7. Vue源码--解读vue响应式原理

    原文链接:https://geniuspeng.github.io/2018/01/05/vue-reactivity/ Vue的官方说明里有深入响应式原理这一节.在此官方也提到过: 当你把一个普通的 ...

  8. Vue源码学习之Computed与Watcher原理

    前言  computed与watch是我们在vue中常用的操作,computed是一个惰性求值观察者,具有缓存性,只有当依赖发生变化,第一次访问computed属性,才会计算新的值.而watch则是当 ...

  9. Vue源码探究-全局API

    Vue源码探究-全局API 本篇代码位于vue/src/core/global-api/ Vue暴露了一些全局API来强化功能开发,API的使用示例官网上都有说明,无需多言.这里主要来看一下全局API ...

最新文章

  1. 剑指offer十九之顺时针打印矩阵
  2. 计算机/ARM 系统
  3. 在Windows Server 2012中如何快速开关桌面上经典的“计算机、我的文档”等通用图标...
  4. 生成树(STP)学习笔记
  5. tensorflow项目构建流程
  6. JS面向对象方法(二) 面向对象方法实现橱窗式图面预览以及放大功能
  7. JDK各版本下载官网链接
  8. jenkins 版本升级
  9. Android项目之利用手机传感器做惯性导航
  10. Cloudera Manager安装教程
  11. BurpSuite 安装配置(License Key)
  12. python-shixian考拉兹猜想
  13. 杨世忠:“周转”知识终圆航天梦
  14. Android fstab学习
  15. 3D游戏建模师看不看学历?现在转行还能行吗?
  16. GBase 8s HAC集群简介
  17. Device /dev/sdb1 excluded by a filter
  18. 2022-2028年全球与中国涡轮轴发动机行业产销需求与投资预测分析
  19. Android MD5 RSA DES等几种加密算法
  20. 呵呵年薪十万都干什么工作

热门文章

  1. 统计学习导论 - 基于R的应用 学习笔记1
  2. 车内静谧性超越埃尔法?走进腾势D9身价上亿的NVH实验室
  3. 设计一个程序,帮助小学生练习10以内的加法题目
  4. 众恒微拓科技:品质退款率怎么优化
  5. ASEMI快恢复二极管FR207参数,FR207图片,FR207应用
  6. 与领导喝酒的18个应紧记的诀窍
  7. linux电脑接电视,Ubuntu下如何给通过HDMI连接电视机的计算机强制设置1920*1080分辨率...
  8. Python fitter包:拟合数据样本的分布
  9. Kindle如何带封面传书
  10. 【英语】英语写作——三段式开头