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 构造函数了,所以就是:
js
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
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 为:
js
map: {
'slot': true,
'component': true
}
这些是 Vue 中内置的标签
isReservedAttribute 函数对应 makeMap 内的 map 也是:
js
map: {
'key': true,
'ref': true,
'slot': true,
'slot-scope': true,
'is': true
}
这些是 Vue 中预定义的属性
默认调用 makeMap 时第二个参数是 true,也就是我们组件名称传进去会先转为小写,然后再去 map 中匹配,说白了,我们写的组件名称中不管大小写,统一转为小写后匹配上边的 map,只要匹配上键(属性名),它们的值刚好是 true,那就进入判断抛出错误提示信息。

-
校验完组件名后,接着调用 normalizeProps 规范化 props
接收 options 选项以及 vm 实例对象-
获取 options 上的 props,为空直接返回
-
情况一:props 如果是数组形式
-

遍历数组元素,进行类型判断,数组中的元素必须是字符串类型,否则抛出错误信息,随后调用 camelize 将元素 val 传进去做处理,camelize 就是将传入的字符串转为驼峰命名的形式并返回
camelizeRE 是要匹配的正则表达式,括号内是捕获组,(\W) 就是捕获一个英文单词,对传入的字符串使用 replace 方法,就是将正则表达式匹配的部分替换为第二个参数指定的部分。举个例子,假设我们传入的 str 如下:
js
my-component
那么 camelizeRE 正则匹配到的就是 -c,捕获组捕获到的就是 c,replace 第二个参数是回调,回调第一个参数是匹配到的完整字符串,第二个参数是捕获到的字符,这里是小写 c,将其转为大写(调用 toUpperCase()),那这里就是将正则匹配到的 -c,替换成大写 C,替换后的字符如下:
js
myComponent
这个 name 就是驼峰式的了,然后放到 res 对象中作为属性,值是一个对象,对象中默认有一个 type: null 的属性,放到 res 中就是
js
res: {
'xxx': {
type: null
}
}
- 情况二:props 是对象形式
对象处理也很简单,枚举对象上的属性,拿到属性值 val,同样对属性名进行处理,转为驼峰命名形式,接着往 res 上放这个属性,如果 val 本身是一个对象,那就直接将这个对象作为属性值,反之就将 type 放到一个对象中,属性值设为这个 val。分别对应下边两种情况
js
props: {
name: String,
age: {
type: Number
}
}
name 属性值不是一个对象,将 name 属性值(String)作为新对象中 type 的属性值,放到 res 中就是
js
res: {
name: {
type: String
}
}
age 属性值本身是一个对象,放到 res 中是
js
res: {
age: {
type: Number
}
}
-
props 不是数组也不是对象
抛出错误信息,提示 props 必须是一个数组或对象 -
处理好的 res 覆盖 options.props
可以看到上边就是对用户传的 props 进行归一化,所谓归一化就是将不同的形式转为相同的形式,上边归一化后的形式就是:
js
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 属性了,还可能有其他的一些属性)。
最后归一化后的格式就是:
js
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 上没有的属性了。
- 最后返回合并好的终极 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 操作符的,比如我们这里判断一个属性:
js
name in obj
这就会触发代理对象 obj 的 has 拦截方法,同理上边代理对象是 vm,使用 in 操作符判断某个属性是否在 vm 上时就会触发 has 函数拦截。
has 常量取值取决于 key 属性在不在 target(vm)上
isAllowed 常量取值满足以下其中一种就是 true,都不满足就为 false:
js
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
initLifecycle 的初始化,就是往 vm 实例上添加一些属性,并赋予默认值,比如我们熟悉且常用的 $refs、$parent、$root、$children 对象, 
- initEvents 事件初始化

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

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

- initRender
内部定义了 $slots、$createElement、$attrs、$listeners
- beforeCreate
调用了 beforeCreate 生命周期钩子
- initInjections
处理 injects 信息,将 injects 对象中的每个属性转为响应式的,这样就能和在 data 中声明的属性一样使用了,这里的关键点就是 injects 比 data 和 props 先初始化。 
- initState
初始化 props、setup(vue3 语法糖)、methods、data、computed、watch(这边的初始化逻辑留到响应式系统篇章再来分析) 
- initProvide
处理 provide 信息,将 provide 对象内的每个属性转为响应式,provide 的初始化在 data、methods 初始化之后 
- created
调用 created 生命周期钩子
- $mount
判断选项中如果有 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 原型添加两个方法:
$nextTick_render

调用 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 上就有如下属性:
js
Vue.options = {
components: {},
directives: {},
filters: {}
}
接着往 Vue.options 追加 _base 属性,属性值是 Vue 本身
现在 Vue.options 就多了一个属性
js
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 属性(组件)

混合后就是
js
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 的响应式系统~