从源码看 Vue 中的 Mixin

时间:2022-09-26 17:22:45

最近在做项目的时候碰到了一个奇怪的问题,通过 Vue.mixin 方法注入到 Vue 实例的一个方法不起作用了,后来经过仔细排查发现这个实例自己实现了一个同名方法,导致了 Vue.mixin 注入方法的失效。后来查阅资料发现 Vue.mixin 注入到实例的 methods 方法会被实例中的同名方法替换,而不会依次执行。于是我就有了查看源码的想法,进而诞生了这篇文章~

本文所用源码版本为 2.2.6

首先从 Vue.mixin 这个方法入手,打开 src 目录不难找到 mixin 所在的文件:src/core/global-api/mixin.js,其内容如下:

从源码看 Vue 中的 Mixin

可以看到这只是一层简单的封装,核心内容基本都在 mergeOptions 方法中,所以下面打开这个方法所在的文件:src/core/util/options.js。注意 mergeOptions 方法是通过 src/core/util/index.js 引入导出的,其源码在 options.js 中,直接看 options.js 就好了。

options.js 中找到 mergeOptions 方法,内容如下:

从源码看 Vue 中的 Mixin

其主流程大致如下:

  1. 如果是非生产环境下,首先调用 checkComponents 检查传入参数的合法性,后面再讲具体实现。
  2. 调用 normalizeProps 方法和 normalizeDirectives 方法对这两个属性进行规范化。
  3. 检查传入参数是否具有 extends 属性,这个属性表示扩展其它 Vue 实例,具体参考官方文档。这里为什么要检查这个属性呢?因为当传入对象具有该属性时,表示所有的 Vue 实例都要扩展它所指定的实例(Vue.mixin 的功能即是如此),那么我们在合并之前,需要先把 extends 进行合并,如果 extends 是一个 Vue 构造函数(也可能是扩展后的 Vue 构造函数),那么合并参数变为其 options 选项了;否则直接合并 extends
  4. 检查完传入参数的 extends 属性之后,我们还要检查其 mixins 属性,这个属性的功能参考官方文档。因为如果传入的 Vue 配置对象仍然指定了 mixins 的话,我们需要递归的进行 merge。
  5. 做完以上的工作之后,就可以开始合并单纯的 mixin 参数了。可以看到通过 mergeField 函数进行了合并,先遍历合并的目标对象,进行合并了;随后遍历要合并的对象,只对目标对象上不存在的属性进行合并操作。那么合并的重点就到了 mergeFiled 函数了。

继续看 mergeField 函数:

function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}

该函数通过 key 值在 strats 中选取合并的具体函数,这是一种典型的策略模式,所以我们看 strats是如何定义的。

options.js 中关于 strats 的定义如下:

/**
* Option overwriting strategies are functions that handle
* how to merge a parent option value and a child option
* value into the final value.
*/
const strats = config.optionMergeStrategies

其中 config 对象来自于 src/core/config.js,它定义了 config 的所有类型及初始值,当然初始值都还是一些空数组之类的,所以我们要在 options.js 中看具体的实现。

下面根据 Vue 的配置属性分开讲解不同的合并方式。

一、el

el 的合并方式比较简单,因为它本身

源码如下:

/**
* Options with restrictions
*/
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)
}
}

可以看到这里有个条件,只有在开发环境下才会定义 strats.el 方法以及 propsData 方法(propsData 文档),这是因为这两个属性比较特殊,尤其是 propsData 只在开发环境下才使用,方便测试而已。另外一个比较特殊的地方是这两者只能在 new 操作符调用 Vue 构造函数所构造的 Vue 实例中才能存在,所以当 vm 未传递时,会弹出一个警告。

这两个属性的合并方法都是 defaultStrat,其源码如下:

/**
* Default strategy.
*/
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}

可以看出在 childVal 已定义的时候直接替代 parentVal

这个方法在后边还会用到。

二、data

data选项的合并是重中之重,因为 data 在子组件中是一个函数,它返回的也是一个特殊的响应式对象。

其源码如下:

从源码看 Vue 中的 Mixin

这里分了两种情况,一种是传递了 vm 参数,一种是没传递。

当没传递 vm 参数的时候,需要校验 childVal 是否是函数,而 parentVal 不需要校验,因为它必须是函数才能通过之前的 merge 校验,到达现在这一步。确定都是函数之后,就调用这两个函数,再然后对返回的两个 data 对象通过 mergeData 做处理,这里后面再讲。

当传递了 vm 参数的时候,需要用其他方式处理,当是函数的时候,使用返回值做下一步合并;当是其他值的时候,直接使用其值进行下一步合并。

这一步要校验 childVal parentVal 是否为函数。正是因为这一步校验了,所以前面所讲的情况就不再需要校验,为什么呢?

我们可以回头看 mergeOptions 的源码,发现其第三个参数 vm 是可选的,在递归的时候它会把 vm 传递给自身,这就导致当我们一开始调用 mergeOptions 的时候传递了 vm,则其后所有递归都会传递 vm;当我们一开始未传递 vm 值的时候,其后所有的递归也不会传递 vm 参数。那么是否有 vm 就取决于我们最开始调用该函数时所传递的参数是否包含 vm 了。

全局查找 mergeOptions 函数的调用,可以看到有两处:

  1. 第一处位于 src/core/instance/init.js,该文件也定义了 initMixin 方法,用于初始化 Vue 把传递给 Vue 构造函数的配置对象合并到 vm.$options 中。这种情况下会传递 vm,其值为当前正在构造的 Vue 实例。
  2. 第二处位于之前一直在讲的 src/core/global-api/mixin.js,这处才是定义的全局 API。

简而言之,Vue 构造函数构造 Vue 实例时,会调用 mergeOptions 并且传递 vm 实例作为第三个参数;当我们调用 Vue.mixin 进行全局混淆时是不会传递 vm 的。前者对应第二种情况,后者对应第一种情况。

当我们先构造 Vue 实例的时候,vm 被传递进而执行第二种情况,parentVal 会被校验,所以之后再调用 Vue.mixin 时第一种情况不再需要校验。

当我们先不实例化 Vue 而先调用 Vue.mixin 时,会先执行第一种情况的代码,那么会导致 bug 出现吗?答案肯定是不会,因为此时 parentValundefined,因为 Vue.mixin 调用时 parentVal 的初始值为 Vue.options,这个对象根本不包含 data 属性。

那么 data 合并的任务主要在 mergeData 函数中了,查看其源码:

从源码看 Vue 中的 Mixin

可以看到这里遍历了要合并的 data 的所有属性,然后根据不同情况进行合并:

  1. 当目标 data 对象不包含当前属性时,调用 set 方法进行合并,后面讲 set
  2. 当目标 data 对象包含当前属性并且当前值为纯对象时,递归合并当前对象值,这样做是为了防止对象存在新增属性。

继续看 set 函数:

从源码看 Vue 中的 Mixin

可以看到 set 也对 target 分了两种情况进行处理。首先判断了 target 是数组的情况,然后如果 target 包含当前属性,那么就直接赋值。接下来判断了 target 是否是响应式对象,如果是的话就会在开发环境下弹出警告,最好不要让 data 函数返回一个响应式对象,因为会造成性能浪费。如果不是响应式对象也可以直接赋值返回,其他情况下就会进一步转化 target 为响应式对象,并收集依赖。

以上大概就是 data 的合并方式,可以看出来如果实例指定了与 mixins 相同名称的 data 值,那么以实例中的为准,mixin 中执行的 data 会失效,如果都是对象但是 mixin 中新增了属性的话,还是会被添加到实例 data 中去的。

三、生命周期钩子(Hooks)

Hooks 的合并函数定义为 mergeHook 钩子,其源码如下:

/**
* Hooks and props are merged as arrays.
*/
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}

这个比较简单,代码注释也写得很清楚了,Vue 实例的生命周期钩子被合并为一个数组。具体有哪些钩子可以被合并被写在 src/core/config.js 中:

/**
* List of lifecycle hooks.
*/
_lifecycleHooks: [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated'
],

合并 assets (components、filters、directives)的方法也比较简单,下面跳过了。

四、watch

合并 watch 的函数源码如下:

从源码看 Vue 中的 Mixin

这一段源码也很简单,注释也很明了,跟生命周期的钩子一样,Vue.mixin 会把所有同名的 watch 合并到一个数组中去,在触发的时候依次执行就好了。

五、props、methods、computed

这三项的合并都使用了相同的策略,源代码如下:

从源码看 Vue 中的 Mixin

这里的处理也比较简单,可以看出来当多次调用 Vue.mixin 混淆时,同名的 props、methods、computed 会被后来者替代;但是当 Vue 构造函数传递了同名的属性时,会以构造函数所接受的配置对象为准。因为 Vue 实例化时也会调用 mergeOptions 第二个参数即为 Vue 构造函数所接受的配置对象,正如前文所述。

六、一些辅助函数

前文有讲到几个辅助函数,比如:checkComponentsnormalizePropsnormalizeDirectives。这里简单贴一下源码:

checkComponents

从源码看 Vue 中的 Mixin

这个函数是为了检查 components 属性是否符合要求的,主要是防止自定义组件使用 HTML 内置标签。

normalizeProps

从源码看 Vue 中的 Mixin

这个函数主要是对 props 属性进行整理。包括把字符串数组形式的 props 转换为对象形式,对所有形式的 props 进行格式化整理。

normalizeDirectives

从源码看 Vue 中的 Mixin

这个函数也主要是对 directives 属性进行格式化整理的,把原来的对象整理成一个新的符合标准格式的对象。

七、自定义合并策略

看到 Vue 的官方文档:自定义选项合并策略,它允许我们自定义合并策略,具体方式就是替换 Vue.config.optionsMergeStrategies,也就是前文所提到的那个定义在 src/core/config.js 中的属性。我们也可以看一下源代码,这一功能在 src/core/global-api/index.js 文件中的 initGlobalAPI 定义。

const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)

可以看到最后一句给 Vue 函数定义了一个 config 属性,其 property 定义为 configDef。在生产环境下不允许设置其值,但是在开发环境下,我们可以直接设置 Vue.config。那么通过设置 Vue.config.optionsMergeStrategies,我们可以改变合并策略,在后面再进行合并操作时,都会读取 config 对象中的属性,这时就可以使用我们自定义的合并策略进行合并了。

八、总结

看了这些属性的合并方式以后,对 Vue.mixin 的工作方式也有了一定的了解了。个人认为基本上可以把 Vue.mixin 合并属性的方式分为三类,一类是替换式、一类是合并式、还有一类是队列式。

替换式的有 elpropsmethodscomputed,这一类的行为是新的参数替代旧的参数。

合并式的有 data,这一类的行为是新传入的参数会被合并到旧的参数中。

队列式合并的有 watch、所有的生命周期钩子(hooks),这一类的行为是所有的参数会被合并到一个数组中,必要时再依次取出。

所以对于 Vue.mixin 的使用我们也需要小心,尤其是替换式合并的属性,当你在 mixins 里面指定了以后,就不要再实例中再指定同名属性了,那样的话你的 mixins 中的属性会被替代导致失效。

作者水平有限,文章难免存在纰漏,敬请大家指正。

从源码看 Vue 中的 Mixin的更多相关文章

  1. 从源码看Android中sqlite是怎么通过cursorwindow读DB的

    更多内容在这里查看 https://ahangchen.gitbooks.io/windy-afternoon/content/ 执行query 执行SQLiteDatabase类中query系列函数 ...

  2. 从源码看Android中sqlite是怎么读DB的(转)

    执行query 执行SQLiteDatabase类中query系列函数时,只会构造查询信息,不会执行查询. (query的源码追踪路径) 执行move(里面的fillwindow是真正打开文件句柄并分 ...

  3. 从vue源码看Vue&period;set&lpar;&rpar;和this&period;&dollar;set&lpar;&rpar;

    前言 最近死磕了一段时间vue源码,想想觉得还是要输出点东西,我们先来从Vue提供的Vue.set()和this.$set()这两个api看看它内部是怎么实现的. Vue.set()和this.$se ...

  4. 从源码看java中Integer的缓存问题

    在开始详细的说明问题之前,我们先看一段代码 public static void compare1(){ Integer i1 = 127, i2 = 127, i3 = 128, i4 = 128; ...

  5. 从 php 源码看 php 中的对象

    从一个简单的例子说起: class Person { public $name; public $age; public function __construct($name, $age) { $th ...

  6. 从微信小程序开发者工具源码看实现原理(一)- - 小程序架构设计

    使用微信小程序开发已经很长时间了,对小程序开发已经相当熟练了:但是作为一名对技术有追求的前端开发,仅仅熟练掌握小程序的开发感觉还是不够的,我们应该更进一步的去理解其背后实现的原理以及对应的考量,这可能 ...

  7. 基于源码分析Vue的nextTick

    摘要:本文通过结合官方文档.源码和其他文章整理后,对Vue的nextTick做深入解析.理解本文最好有浏览器事件循环的基础,建议先阅读上文<事件循环Event loop到底是什么>. 一. ...

  8. 从源码看Azkaban作业流下发过程

    上一篇零散地罗列了看源码时记录的一些类的信息,这篇完整介绍一个作业流在Azkaban中的执行过程,希望可以帮助刚刚接手Azkaban相关工作的开发.测试. 一.Azkaban简介 Azkaban作为开 ...

  9. mysql-5&period;5&period;28源码安装过程中错误总结

    介绍一下关于mysql-5.5.28源码安装过程中几大错误总结,希望此文章对各位同学有所帮助.系统centOS 6.3 mini (没有任何编译环境)预编译环境首先装了众所周知的 cmake(yum ...

随机推荐

  1. 使用jsvc启动tomcat

    1.在/usr/local/apache-tomcat-7.0.68/bin中有commons-daemon-native.tar.gz  压缩包 2.解压commons-daemon-native. ...

  2. POI 设置EXCEL单元格格式(日期数字文本等)

    HSSFCellStyle style0 = workbook2003.createCellStyle(); style0.setBorderBottom(HSSFCellStyle.BORDER_T ...

  3. http&colon;&sol;&sol;love3400wind&period;blog&period;163&period;com&sol;blog&sol;static&sol;7963080120132794359703&sol;

    http://love3400wind.blog.163.com/blog/static/7963080120132794359703/

  4. shell编程之数组和关联数组

    一.数组类似c语言的数组 1.两种赋值方式 可以整体定义数组:ARRAY_NAME=(value0 value1 value2 value3 ...) 此时数组的下标默认是从0开始的 还可以单独定义数 ...

  5. Android(java)学习笔记160:Framework运行环境之 Android进程产生过程

    1.前面Android(java)学习笔记159提到Dalvik虚拟机启动初始化过程,就下来就是启动zygote进程: zygote进程是所有APK应用进程的父进程:每当执行一个Android应用程序 ...

  6. 17、SQL Server 备份和还原

    SQL Server 备份 恢复模式 SQL Server 数据恢复模式分为三种:完整恢复模式.大容量日志恢复模式.简单恢复模式. 完整恢复模式 默认的恢复模式,它会完整记录下操作数据库的每一个步骤, ...

  7. BZOJ 1023 &lbrack;SCOI2009&rsqb;生日快乐

    1024: [SCOI2009]生日快乐 Time Limit: 1 Sec  Memory Limit: 162 MBSubmit: 1729  Solved: 1219[Submit][Statu ...

  8. background-position 使用方法具体介绍

    语法: background-position : length || length background-position : position || position 取值: length  : ...

  9. 跟着刚哥学习Spring框架--通过注解方式配置Bean(四)

    组件扫描:Spring能够从classpath下自动扫描,侦测和实例化具有特定注解的组件. 特定组件包括: 1.@Component:基本注解,识别一个受Spring管理的组件 2.@Resposit ...

  10. ruby的sort方法的重新认识

    ruby中的sort方法,这个方法可以加一个两个参数的block,这个block可以返回1 0 -1来表示这两个参数大于 等于 小于示例: str = ["192.160.175" ...