Vue 中的核心源码主要放在了 src/core 目录下,我们先来看下面这段代码

上面这段代码是我们平常创建一个 Vue 项目中 main.js 入口文件里的原始代码,这也是 Vue 创建应用的起点,接下来我们就来分析从 new Vue() 到 $mount 这一过程 Vue 都做了哪些事,也就是做了哪些初始化。
首先是 src/core/index.ts 文件

我们看到它调用 initGlobalAPI 函数,并把 Vue 作为参数传进去,我们先不着急看 initGlobalAPI 内部逻辑,因为我们连它传入的这个参数 Vue 是啥都不知道,工欲善其事,必先利其器。我们看到文件最顶上 Vue 是从同目录下 instance/index 文件中导出的。
我们打开文件看一下:

哦,原来导出的 Vue 是一个函数,而且函数名称还是大写的,所以可以当作构造函数使用,除此之外,我们还看到在函数下方还有一系列以 xxxMixin 为首的函数调用,而且也把 Vue 这个函数当作参数传入,我们可以猜测这些函数调用就是执行各种初始化操作,而且在我们 new Vue 的时候,其实下面的那些 xxxMixin 都已经执行过了,new Vue 时调用的 _init 方法实际上是在 initMixin 中的,所以我们把它归类到 initMixin 中,可以初步得到以下初始化流程图:

具体每个函数都干了哪些事,我们接下来就按照上边流程图的顺序来逐个进行分析。
initMixin
_init 方法所在文件(src/core/instance/init.ts)

它是 Vue 原型上的一个方法,接收一个 options 参数,我们主要分析它的核心逻辑:

1、首先将 this 赋值给 vm

这里的 this 就是 new Vue 时构造出来的实例对象,所以 vm 特指 Vue 实例对象。
函数外部有一个 uid 变量,默认从 0 开始

接着往 Vue 实例对象上添加一个 _uid 属性作为唯一标识,值默认是 0,赋值完后 uid 自增(这样下一个 new Vue 构造的实例,它的 _uid 就是 1,以此类推)

vm 上添加 _isVue 属性,值设为 true,这个属性用来标记当前 vm 是一个 Vue 的实例(也就是通过 new Vue 构造出来的)

后边还设置了其他的一些属性,咱们先不用管,后边有出现会提到的。
然后我们来看关键方法 mergeOptions,从名字上我们就可以知道它是用于合并选项的,调用 mergeOptions 并将返回结果赋值给 vm.$options

我们先来分析它的传参,第一个参数是调用 resolveConstructorOptions 函数的返回值
那么调用 resolveConstructor 函数传了 vm.constructor(实例对象的 constructor),这里通过原型相关的知识可以得知,实例的 constructor 指向实例化它的构造函数,那这里就是 Vue 构造函数了,所以就是:
vm.constructor === Vue // true
我们来看下 resolveConstructor 函数内部逻辑:

- 接收一个参数 Ctor,我们刚才传了 vm.constructor(相当于 Vue 构造函数)
- 取得 options 选项
- 判断 Ctor.super 有没有(super 关键字会指向其父类的构造函数,即判断有无父类),这一般在子组件通过 extend 继承父组件时会存在这种情况,我们目前没有父子继承关系,所以不进入判断内部
- 直接返回 options
mergeOptions 的第二个参数是 _init 接收的参数 options,也就是 new Vue 时传给构造函数中的对象,第三个参数是当前组件实例 vm。传给 mergeOptions 的三个参数都分析完后,来看下 mergeOptions 函数

通过官方注释得知,就是将两个选项合并成一个,parent 就是 Vue 构造函数的默认 options,child 是我们 new Vue 时传给构造函数的,vm 就是当前实例
checkComponent 用于检测组件的名称,前提是传入的 options 上的 components 不为空,枚举 options.components 上的组件名,调用 validateComponentName 函数校验
validateComponentName 接收参数 name(即组件名称),采用正则表达式校验 name 是否符合要求,不符合就调用 warn 函数,提示错误信息。
通过正则表达式校验,下边还需要校验是不是和 Vue 中一些内置名称冲突
调用 isBuiltInTag 和 isReservedAttribute 函数相当于是调用了 makeMap 返回的函数,我们看看 makeMap 函数
接收 str 字符串参数,expectsLowerCase 布尔值是否期望小写。makeMap 函数内部逻辑:
- 构建一个空 map
- 分割传入的 str 放到 list 数组
- 遍历数组,将数组中的元素放到 map 中作为键,其值默认为 true
- 返回值:如果 expectsLowerCase 为 true,返回一个函数,这个函数接口一个 val 参数,将 val 转为小写后去 map 中找到这个键对应的值,如果 expectsLowerCase 为 false,返回一个接收 val 参数的函数,函数直接返回 map 中 val 这个键对应的值。
总结:isBuiltInTag 函数对应的 makeMap 返回的函数内部的 map 为:
map: {
'slot': true,
'component': true
}
这些是 Vue 中内置的标签
isReservedAttribute 函数对应 makeMap 内的 map 也是:
map: {
'key': true,
'ref': true,
'slot': true,
'slot-scope': true,
'is': true
}
这些是 Vue 中预定义的属性
默认调用 makeMap 时第二个参数是 true,也就是我们组件名称传进去会先转为小写,然后再去 map 中匹配,说白了,我们写的组件名称中不管大小写,统一转为小写后匹配上边的 map,只要匹配上键(属性名),它们的值刚好是 true,那就进入判断抛出错误提示信息。


遍历数组元素,进行类型判断,数组中的元素必须是字符串类型,否则抛出错误信息,随后调用 camelize 将元素 val 传进去做处理,camelize 就是将传入的字符串转为驼峰命名的形式并返回

camelizeRE 是要匹配的正则表达式,括号内是捕获组,(\W) 就是捕获一个英文单词,对传入的字符串使用 replace 方法,就是将正则表达式匹配的部分替换为第二个参数指定的部分。举个例子,假设我们传入的 str 如下:
my-component
那么 camelizeRE 正则匹配到的就是 -c,捕获组捕获到的就是 c,replace 第二个参数是回调,回调第一个参数是匹配到的完整字符串,第二个参数是捕获到的字符,这里是小写 c,将其转为大写(调用 toUpperCase()),那这里就是将正则匹配到的 -c,替换成大写 C,替换后的字符如下:
myComponent
这个 name 就是驼峰式的了,然后放到 res 对象中作为属性,值是一个对象,对象中默认有一个 type: null 的属性,放到 res 中就是
res: {
'xxx': {
type: null
}
}
- 情况二:props 是对象形式
对象处理也很简单,枚举对象上的属性,拿到属性值 val,同样对属性名进行处理,转为驼峰命名形式,接着往 res 上放这个属性,如果 val 本身是一个对象,那就直接将这个对象作为属性值,反之就将 type 放到一个对象中,属性值设为这个 val。分别对应下边两种情况
props: {
name: String,
age: {
type: Number
}
}
name 属性值不是一个对象,将 name 属性值(String)作为新对象中 type 的属性值,放到 res 中就是
res: {
name: {
type: String
}
}
age 属性值本身是一个对象,放到 res 中是
res: {
age: {
type: Number
}
}
可以看到上边就是对用户传的 props 进行归一化,所谓归一化就是将不同的形式转为相同的形式,上边归一化后的形式就是:
props: {
key1: {
type: String
},
key2: {
type: Number
},
key3: {
type: null
}
}
- 调用 normalizeInject 规范化 inject
normalizeInject 内部处理逻辑和 normalizeProps 很相似,都是分为数组和对象两种情况处理,如果两者都不是就抛出错误信息。只是 props 中是处理 type,injects 是 from。这里对象的处理中,如果枚举的属性其属性值是一个对象,会调用 extend 进行处理,传入两个参数,第一个是一个对象,有 from 属性,属性值是这个枚举属性,第二个参数是枚举属性对应的值,这个值是个对象,看看 extend 函数。

就是往第一个参数也就是目标对象上混入属性,枚举第二个参数传入的 val 对象,往第一个参数({from: key})混入第二个参数中的属性(如果第二个参数 val 对象上也存在 from 属性,则 val 对象上的 from 属性值覆盖源对象上的 from 属性值,混入后,源对象上就不仅只有 from 属性了,还可能有其他的一些属性)。
最后归一化后的格式就是:
injects: {
key1: {
from: xxx,
..., // 其他一些属性
}
}
-
调用 normalizeDirectives 规范化 directives

- 获取 directives 选项
- 枚举选项上的属性,获取属性值
- 判断属性值是不是一个函数,是的话就将选项上当前属性的属性值重新赋值为一个对象,对象上的 bind 和 update 属性为这个源函数
-
枚举 parent(Vue 构造函数的 options 选项),调用 mergeField 将属性作为实参传入
-
枚举 child(用户传入的 options 选项),如果属性在 parent(Vue 构造函数中的 options)中不存在,调用 mergeField 函数,将 key 属性作为实参传入
接着看看这个核心的 mergeField 函数

strat 是一个函数,会先从 strats 中取,strats 在文件顶部声明

默认值为 config.optionMergeStrategies, 这个 config 在(src/core/config.ts)文件中,默认是个空对象

从上边的接口类型声明来看,这个对象上的属性是字符串类型,属性值是个函数

那么如果 strats 上没有这个属性对应值,就会使用默认的策略 defaultStrat
默认策略接收父属性值和子属性值作为参数,如果子属性值不为 undefined 的话优先返回它,否则再取父属性值,这个返回值就作为最后合并好的 options 上该属性对应的值。
那么上边先枚举 Vue 构造函数的 options 上的属性,如果用户传的 options 也存在该属性,优先使用用户传的,这样后边枚举用户传的 options 时就仅需要处理 Vue 构造函数 options 上没有的属性了。
至此,mergeOptions 函数就分析完了,主要流程就是组件名校验、归一化 props、injects、directives,然后将 Vue 构造函数及用户的 options 进行一个合并,最后返回。
继续往下看:

- initProxy
所在文件(src/core/instance/proxy.ts),在文件顶部声明了一个 initProxy 变量,然后赋值为一个函数:
接收 vm 实例作为参数,首先判断 hasProxy 值,hasProxy 判断浏览器是否支持 Proxy 这个 API,即不为 undefined,且调用 isNative 要返回 true,isNative 会判断传入参数是一个函数,且是 JavaScript 内置的函数(内置函数调用 toString 方法会返回包含 [native code] 的字符串)

进入判断后先获取 vm 上的 options,然后定义 handlers 配置项,然后创建一个代理实例赋值给 vm 上的 _renderProxy 属性,如果 hasProxy 为 false, _renderProxy 属性就赋值为 vm 实例本身,接着我们看下代理配置项 handlers 取值,有 getHandler 和 hasHandler 两种:
getHandler 中定义了一个 get 函数,参数是 target(这里是 vm 实例) 和 key(属性)

也就是我们读取 vm 上的某个属性时,会触发 get 函数拦截,首先判断这个 key 属性是字符串且不在 target(vm 实例)上,接着继续判断 key 属性在不在 vm 实例的 $data(也就是我们组件中写的 data 对象里)上,如果在就调用 warnReservedPrefix 函数抛出错误提示,如果不在 vm 实例上也不在 vm.$data 属性上,调用 warnNonPresent 函数抛出另一个错误信息
因为属性 key 它不在 vm 上,但在 vm.$data 上,所以这里提示信息意思就是这个 key 属性必须访问 $data.key 上的
这种情况就是 key 属性即不在 vm 上也不在 vm.$data 上,抛出错误信息表示属性或方法在实例上未定义但是存在引用
再看下 hasHandler 函数的 has 函数,has 代理方法是针对 in 操作符的,比如我们这里判断一个属性:
name in obj
这就会触发代理对象 obj 的 has 拦截方法,同理上边代理对象是 vm,使用 in 操作符判断某个属性是否在 vm 上时就会触发 has 函数拦截。
has 常量取值取决于 key 属性在不在 target(vm)上
isAllowed 常量取值满足以下其中一种就是 true,都不满足就为 false:
allowedGlobals(key) ||
typeof key === 'string' &&
key.charAt(0) === '_' &&
!(key in target.$data)
allowGlobals 函数也很简单,有我们熟悉的 makeMap 函数,第二个参数不传,就是内部匹配名称时不会转为小写去匹配,内部 map 的组成由每个逗号分割的关键字作为属性,其属性值默认为 true,调用这个函数时,如果当前 key 和 map 中某个属性名称匹配,那么取值就是 true
下面就是判断 has 取值为 false(即属性不在 vm 上) 且 isAllowed 也为 false,内部逻辑和 getHandler 中的 get 函数相似。最后返回 has || !isAllowed 逻辑或的取值。

在 initLifecycle 调用前,还有一句赋值语句,将自身引用保持在了自身的 _self 属性上

initLifecycle 的初始化,就是往 vm 实例上添加一些属性,并赋予默认值,比如我们熟悉且常用的 $refs、$parent、$root、$children 对象,


核心就是 updateComponentListeners 方法,这个方法内部又调用了 updateListeners 方法

updateListeners 方法内部就是对新老事件进行处理(更新事件 on 监听,包括 add 新增事件和 remove 移除事件)

内部定义了 $slots、$createElement、$attrs、$listeners
调用了 beforeCreate 生命周期钩子
处理 injects 信息,将 injects 对象中的每个属性转为响应式的,这样就能和在 data 中声明的属性一样使用了,这里的关键点就是 injects 比 data 和 props 先初始化。

初始化 props、setup(vue3 语法糖)、methods、data、computed、watch(这边的初始化逻辑留到响应式系统篇章再来分析)

处理 provide 信息,将 provide 对象内的每个属性转为响应式,provide 的初始化在 data、methods 初始化之后

调用 created 生命周期钩子
判断选项中如果有 el 节点,那就作为实参传入 vm.$mount 函数中

这里的 el 就是我们常说的挂载的根容器 app

stateMixin
所在文件:src/core/instance/state.ts,内部逻辑也很简单

- 拦截 Vue 原型上的
$data 和 $props
- Vue 原型上添加
$set 方法
- Vue 原型上添加
$delete 方法
- Vue 原型上添加
$watch 方法
看看是怎么拦截的

访问 Vue.prototype.$data 时实际上是这样访问 vm._data(当前 vm 实例上的 _data 对象)
访问 Vue.prototype.$props 时实际上是这样访问 vm._props(当前 vm 实例上的 _props 对象)
如果是修改 Vue.prototype.$data 或者 Vue.prototype.$props,会走 set 拦截方法抛出错误信息

eventsMixin
eventsMixin 函数接收 Vue 构造函数作为参数,往构造函数原型上添加四个方法:
$on
$once
$off
-
$emit
lifecycleMixin
lifecycleMixin 函数接收 Vue 构造函数作为参数,往 Vue 原型添加三个方法:
_update
$forceUpdate
-
$destroy
renderMixin
renderMixin 函数接收 Vue 构造函数作为参数,往 Vue 原型添加两个方法:

调用 installRenderHelpers 函数时将 Vue.prototype 作为实参传入

target 就是 Vue.prototype,往 Vue 原型上添加各种以 _ 开头的方法
initGlobalAPI
接收 Vue 构造函数作为形参
先代理 Vue 上的 config 属性(是个对象),属性描述符项是 configDef,提供 get 和 set 函数,当你尝试修改 Vue.config 时会被 configDef 的 set 函数拦截,抛出错误信息,意思就是不能替换 Vue.config 对象。如果是读取 Vue.config,就被 configDef 的 get 函数拦截,直接返回了 config,这个 config 是个对象(所在文件:core/config.ts)中,就是暴露了一堆全局属性(比如 async、devtools 等)

接着往 Vue.util 对象上添加一些方法
Vue 官方也提供了注释,意思就是这些不被认为是公共的 API,虽然暴露出去你能用,但是不建议用,因为这几个 API 其实是 Vue 内部自己在用的。
再下边就往 Vue 上添加了几个方法,在之后文章会详细介绍

下边初始化 Vue.options 为一个空对象,遍历 ASSETS_TYPE 数组中的元素然后追加到 options 上

ASSETS_TYPE 所在文件(src/shared/constants.ts)
数组中每个元素就是遍历时的 type,是字符串类型,往 type 对应元素名称后边拼上 s 后作为属性名,属性值默认是空对象,遍历追加完后 Vue.options 上就有如下属性:
Vue.options = {
components: {},
directives: {},
filters: {}
}
接着往 Vue.options 追加 _base 属性,属性值是 Vue 本身
现在 Vue.options 就多了一个属性
Vue.options = {
_base: Vue,
components: {},
directives: {},
filters: {}
}
然后是 extend 函数,将 Vue.options.components 作为第一个参数(是一个对象),builtInComponent 作为第二个参数

builtInComponent(所在文件:src/core/components/index.ts),其实就是暴露了一个 KeepAlive 组件

那看下 extend 函数(所在文件:src/shared/utils.ts)
其实很简单,就是往目标对象混合属性,目标对象就是传的第一个参数(Vue.options.components),混合的属性在第二个参数里,刚才看了是一个对象,对象里边有 KeepAlive 属性(组件)

混合后就是
Vue.options = {
_base: Vue,
components: {
KeepAlive
},
directives: {},
filters: {}
}
接着下边又往 Vue 上添加了一系列方法:
initUse,往 Vue 上添加了 use 方法

initMixin,往 Vue 上添加 mixin 方法

initExtend,往 Vue 上 添加 extend 方法

initAssetRegisters,往 Vue 上添加 component、directive、filter 方法,ASSET_TYPES 刚才看过了是个数组,数组中每个元素作为函数名,在 Vue 上注册成了函数。

至此,从 new Vue() 到 $mount 期间的一系列初始化操作我们就看完了,上边为了先分析传给 initGlobalAPI 的参数 Vue,所以就先分析了 initMixin 等函数,实际上 initGlobalAPI 作为核心入口文件是最先执行的。下边再来看看流程图,这会就清晰多了

Vue 初始化流程,每一步都做了哪些初始化,现在看就一目了然了,下篇文章我们就进击 Vue 的响应式系统~