本篇文章分享来自小伙伴「liuxin」的一次学习总结分享。 为了进一步熟悉 Vue 框架,对其源码进行解析,记录此文档,方便以后回顾、达到加强记忆和强化学习效果。源码不是看一遍两遍就会了,需要多看才能熟练掌握甚至精通。
收益
- 阅读 Vue 源码,可以让自己更加熟悉框架,能更快速的解决工作中遇到的问题;
- 阅读 Vue 源码,学习大佬的思路,在项目遇到问题时可以有更多的思路,也提升自己编码的思路,培养“造轮子”的能力;
- 阅读 Vue 源码,可以学习怎么写出规范又好维护的代码;
- 提升自己解读源码的能力,读源码本身就是一个很好的学习方式,掌握了如何阅读源码,将来在学习其他框架或者是接手新项目的时候,都可以通过阅读源码的方式快速上手。
准备
Vue2 最新稳定版本:2.7.10
下载 Vue2 源码
git clone https://github.com/vuejs/vue.git
安装依赖
npm i
添加 sourcemap
在 package.json - scripts 中的 dev,添加 sourcemap 方便调试时查看当前行在源码中的位置
开发调试
npm run dev
找初始化的位置
编写调试代码,打断点,快速查找 Vue 构造函数声明位置。
- 在 /examples 目录下创建 text.html,引入 vue.js,并创建实例。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue 初始化过程解析</title>
<script src="../dist/vue.js"></script>
</head>
<body>
<div id="app">
<h2>{{ title }}</h2>
</div>
<script>
debugger
const vm = new Vue({
el: '#app',
data: {
title: 'Vue 初始化过程解析'
}
});
</script>
</body>
</html>
- 在浏览器打开 text.html,debugger 进入 Vue 构造函数,然后如下图,找到 Vue 构造函数所在文件。
得到 Vue 构造函数在文件 src/core/instance/index.js 中,下面就开始进入源码解读,刚开始解读肯定会有不理解的地方,不理解的地方直接注释好,继续往下看,可能看到某一块就突然明白之前为啥这样写了。
初始化过程
index.js
- src/core/instance/index.js
import { initMixin } from './init'
...
// Vue 构造函数
function Vue (options) {
// 在非生产环境时,如果不是 new 创建实例给予警告
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
// 调用 Vue.prototype._init 方法,该方法在 initMixin 中定义
this._init(options)
}
// 定义 Vue.prototype._init 方法,进行 初始化混合
initMixin(Vue)
...
export default Vue
initMixin-Vue.prototype._init
- src/core/instance/init.js
...
// 每个 Vue 实例的 uid,从 0 开始递增
let uid = 0
// 初始化混入,定义 Vue.prototype._init 方法。
export function initMixin (Vue: Class<Component>) {
// Vue 初始化方法,会在 new Vue 时自动调用
Vue.prototype._init = function (options?: Object) {
// this: vue 实例,在 Vue 构造函数那有判断 this instanceof Vue
const vm: Component = this
// 每个 Vue 实例的 uid,从 0 开始递增
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// 处理内部组件配置项
if (options && options._isComponent) {
// 优化内部组件实例化,每个子组件初始化时走着,这里做了一些性能优化,
// 将组件配置项上的一些属性放到 vm.$options 选项中,提高代码的执行效率
initInternalComponent(vm, options)
} else {
// 初始化根组件时走这
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// 初始化代理
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化实例相关属性,如:$options、$parent、$children、$refs
initLifecycle(vm)
// 初始化父组件附加给子组件的事件,事件的派发和监听者都是子组件本身
initEvents(vm)
// 初始化组件插槽信息,得到 $slots、处理渲染函数,得到 $createElement 方法
// 给 vm 定义 $attrs、$listeners 反应属性
initRender(vm)
// 调用 beforeCreate 钩子函数
callHook(vm, 'beforeCreate')
// 初始化组件的 inject 配置项,首先通过 resolveInject 方法得到形如 result[key] = value 的对象,
// 然后使用 defineReactive 方法把得到的数据结果对象进行响应式处理,使每个 key 都代理到 vm 实例。
initInjections(vm) // resolve injections before data/props
// 初始化状态,是响应式的重点函数,处理 props、methods、data、computed、watch
initState(vm)
// 初始化组件的 provide 配置项,并将其挂载到 vm._provided 属性上,
// provide 和 inject 成对出现,作用是允许一个组件向其所有子孙后台注入一个依赖。
initProvide(vm) // resolve provide after data/props
// 调用 created 钩子函数
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 配置项上如果有 el 属性,Vue 会自动调用 $mount 方法。
// 所以如果有 el 属性就不需要自己调用 $mount,如果没有就需要自己手动调用 $mount
if (vm.$options.el) {
// 调用 $mount 进入挂载
vm.$mount(vm.$options.el)
}
}
}
...
resolveConstructorOptions
- src/core/instance/init.js
// 从组件的构造函数中获取配置对象 options,并合并到父类配置 options 中
export function resolveConstructorOptions (Ctor: Class<Component>) {
// 获取组件构造函数的选项
let options = Ctor.options
// 判断是否存在父类
if (Ctor.super) {
// 存在父类,就递归解析父类构造函数的配置选项
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
// 查看父类的构造函数选项是否改变
if (superOptions !== cachedSuperOptions) {
// 父类的构造函数选项发生改变,需要重新设置
Ctor.superOptions = superOptions
// 检查 Ctor.options 上是否有任何后期修改/增加的选项 (#4967)
const modifiedOptions = resolveModifiedOptions(Ctor)
// update base extend options
// 如果存在修改或者增加的选项,则更新 extendOptions
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
// 选项合并,并将合并结果赋值给 Ctor.options
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
// 如果存在组件名,则在 options.components 组件列表中添加这个组件的构造函数,并且 key 为组件名
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
resolveModifiedOptions
- src/core/instance/init.js
// 解析 Ctor.options 上任何后期修改/增加的选项
function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
// 修改的选项对象
let modified
// 构造函数选项
const latest = Ctor.options
// 密封的构造函数选项,也是 Vue 备份的选项
const sealed = Ctor.sealedOptions
// 对比两个选项,并把不一致的选项记录下
for (const key in latest) {
if (latest[key] !== sealed[key]) {
if (!modified) modified = {}
modified[key] = latest[key]
}
}
return modified
}
mergeOptions
- src/core/util/options.js
// 将两个选项对象合并为一个新对象,相同配置选项子配置会覆盖父配置
// 用于实例化和继承的核心实用程序。
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
// 标准化 props、inject、directive 语法,方便后续使用
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// 处理 child 对象上没有 _base 的 extends 和 mixins,分别调用 mergeOptions 合并到 parent
if (!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)
}
}
}
const options = {}
let key
// 遍历父选项
for (key in parent) {
mergeField(key)
}
// 遍历子选项,如果父选项不存在该配置,则合并,否则跳过处理,因为如果配置相同,说明上一步已经处理过了
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
// 合并选项配置,把 child 的配置内容优先级高于 parent 的
function mergeField (key) {
// 获取合并方法
const strat = strats[key] || defaultStrat
// 如果 child[key] 有配置内容,则返回 child[key],没有的话使用 parent[key]
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
initInjections
- src/core/instance/inject.js
// 初始化组件的 inject 配置项,首先通过 resolveInject 方法得到形如 result[key] = value 的对象,
// 然后使用 defineReactive 方法把得到的数据结果对象进行响应式处理,使每个 key 都代理到 vm 实例。
export function initInjections (vm: Component) {
// 解析组件的 inject 配置项,得到形如 result[key] = value 的对象
const result = resolveInject(vm.$options.inject, vm)
// 如果 result 存在,就把 result 做响应式处理,把 result 中每一个 key 都代理到 vm 实例上
if (result) {
// 不观察
toggleObserving(false)
// 对 result 中的每一个 key 都代理到 vm 实例上
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// 会给出警告,inject 不能改变,因为每个改变都会导致组件中的更改被覆盖
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
// 开启观察
toggleObserving(true)
}
}
resolveInject
- src/core/instance/inject.js
// 解析 inject,生成形如 result[key] = val 的对象
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
// 创建一个空的对象,用来记录解析后的结果
const result = Object.create(null)
// 获取 inject 的所有 key
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
// 遍历所有 key
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// #6574 in case the inject object is observed...
// 跳过 __ob__ 对象
if (key === '__ob__') continue
// 获取 injectKey,详见 flow/options.js
const provideKey = inject[key].from
let source = vm
// 遍历所有组件直到根组件,获取所有 injectKey 对应的 value,生成 result[key] = val 形式的对象。
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
// 如果上一步没有找到 _provided,就使用 inject[key].default,还没有就报警告
if (!source) {
if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault
} else if (process.env.NODE_ENV !== 'production') {
warn(`Injection "${key}" not found`, vm)
}
}
}
return result
}
}
initProvide
- src/core/instance/inject.js
// 初始化组件的 provide 配置项,并将其挂载到 vm._provided 属性上
export function initProvide (vm: Component) {
// 获取组件的 provide 配置项
const provide = vm.$options.provide
// 如果存在 provide,判断其是函数吗,是函数就执行,不是就直接返回
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
总结
说明
initLifecycle、initEvents、initRender、initState、callHook 不在本篇解析,后续解析
Vue 初始化过程中 (new Vue(options)) 都做了什么?
- 处理组件配置项
-
每个子组件走到初始化配置项时,都做一些性能优化
-
初始化根组件时,从组件的构造函数中获取配置对象 options,并合并配置
-
初始化实例相关属性,如:options、parent、children、refs 等
-
初始化父组件附加给子组件的事件,事件的派发和监听者都是子组件本身
-
初始化组件的插槽信息得到 createElement 方法
-
调用 beforeCreate 钩子函数
-
初始化组件的 inject 配置项,得到形如 result[key] = val 的对象,并把 result 每个 key 都代理到 vm 实例上
-
初始化状态,里面是响应式的核心内容
-
初始化组件的 provide 配置项,并将其挂载到 vm._provided 属性上
-
调用 create 钩子函数
-
判断配置上是否有 el 属性,如果有自动调用 vm.$mount 方法
-
接下来进入挂载阶段