前言
因最近闲来无事,从本文开始重新学习vue源码,如果有错误或遗漏或不对的地方,请在评论区指出,非常感谢各位大佬。
正文
相信小伙伴们在面试的时候,或多或少都有碰到过被面试官问到vue初始化过程,今天我们就通过源码来解析下new Vue(options)到底发生了什么。
初始化源码入口
找源码入口最简单的办法,首先我们需要一个例子实例化一下vue,通过打断点就可以很明确的知道代码的执行流程。还有一个办法就是通过全局搜vue的构造函数在那声明的也可以找到入口,这篇文章我们以编写代码示例为基础找入口。
<!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="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
<div id="app">
message: {{ message }}
</div>
</body>
<script>
debugger
const app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
</script>
</html>
通过debugger我们可以看到vue的构造函数在/vue/src/core/instance/index.ts文件,下面我们看下构造函数都做了些什么。
源码解读
入口文件/vue/src/core/instance/index.ts
import { initMixin } from './init'
function Vue(options) {
// instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
if (__DEV__ && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
// Vue.prototype._init 方法在initMixin声明
this._init(options)
}
// 合并配置
initMixin(Vue)
//stateMixin主要定义了$data,$props,$set,$delete,$watch,并且$data,$props是只读属性。
stateMixin(Vue)
//初始化事件中心
eventsMixin(Vue)
//初始化生命周期,调用声明周期钩子函数
lifecycleMixin(Vue)
//初始化渲染
renderMixin(Vue)
通过这段代码我们可以知道初始化是_init方法,我们来看下_init方法都有什么
Vue.prototype._init
文件位置:vue/src/core/instance/init.ts
export function initMixin (Vue: Class<Component>) {
// 负责 Vue 的初始化过程
Vue.prototype._init = function (options?: Object) {
// vue 实例
const vm: Component = this
// 每个 vue 实例都有一个 _uid,并且是依次递增的
vm._uid = uid++
// 这段代码是用来检测性能,使用需要借助谷歌插件Vue Performance Devtool
let startTag, endTag
/* istanbul ignore if */
if (__DEV__ && 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
// avoid instances from being observed
vm.__v_skip = true
// effect scope
vm._scope = new EffectScope(true /* detached */)
// 处理组件配置项
if (options && options._isComponent) {
/**
* 每个子组件初始化时走这里,这里只做了一些性能优化
* 将组件配置对象上的一些深层次属性放到 vm.$options 选项中,以提高代码的执行效率
*/
initInternalComponent(vm, options)
} else {
/**
* 初始化根组件时走这里,合并 Vue 的全局配置到根组件的局部配置,比如 Vue.component 注册的全局组件会合并到 根实例的 components 选项中
* 至于每个子组件的选项合并则发生在两个地方:
* 1、Vue.component 方法注册的全局组件在注册时做了选项合并
* 2、{ components: { xx } } 方式注册的局部组件在执行编译器生成的 render 函数时做了选项合并,包括根组件中的 components 配置
*/
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// 设置代理,将 vm 实例上的属性代理到 vm._renderProxy
initProxy(vm)
} else {
vm._renderProxy = vm
}
/* istanbul ignore else */
if (__DEV__) {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化组件实例关系属性,比如 $parent、$children、$root、$refs 等
initLifecycle(vm)
/**
* 初始化自定义事件,这里需要注意一点,所以我们在 <comp @click="handleClick" /> 上注册的事件,监听者不是父组件,
* 而是子组件本身,也就是说事件的派发和监听者都是子组件本身,和父组件无关
*/
initEvents(vm)
// 解析组件的插槽信息,得到 vm.$slot,处理渲染函数,得到 vm.$createElement 方法,即 h 函数
initRender(vm)
// 调用 beforeCreate 钩子函数
callHook(vm, 'beforeCreate')
// 初始化组件的 inject 配置项,得到 result[key] = val 形式的配置对象,然后对结果数据进行响应式处理,并代理每个 key 到 vm 实例
initInjections(vm) // resolve injections before data/props
// 数据响应式的重点,处理 props、methods、data、computed、watch
initState(vm)
// 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
initProvide(vm) // resolve provide after data/props
// 调用 created 钩子函数
callHook(vm, 'created')
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 如果发现配置项上有 el 选项,则自动调用 $mount 方法,也就是说有了 el 选项,就不需要再手动调用 $mount,反之,没有 el 则必须手动调用 $mount
if (vm.$options.el) {
// 调用 $mount 方法,进入挂载阶段
vm.$mount(vm.$options.el)
}
}
}
这里体现一个面试问题,实例创建前,有事件生命周期开始,对应的el没有绑定到实例,data获取不到数据,也获取不到method方法。
initInternalComponent
/**
* @description: 性能优化 把组件传进来的一些配置赋值到vm.$options上 打平配置对象上的属性 减少运行时原型链的查找,提高执行效率
* @param {*} vm 组件实例
* @param {*} options 传递进来的配置
*/
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
//基于组件构造函数上的配置对象 创建vm.$options
const opts = vm.$options = Object.create(vm.constructor.options)
//```````````````把组件传进来的一些配置赋值到vm.$options上````````````````````∧
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
//```````````````把组件传进来的一些配置赋值到vm.$options上````````````````````∨
//如果有 render 函数, 将其赋值到vm.$options
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
resolveConstructorOptions
/**
* @description: 解析实例constructor上的options属性,并合并基类选项
* @param {*} Ctor 实例构造函数
* @return {*} options 配置选项
*/
export function resolveConstructorOptions (Ctor: Class<Component>) {
//从实例构造函数上获取配置 options
let options = Ctor.options
if (Ctor.super) {
/**
* Ctor.super是通过Vue.extend构造子类的时候。Vue.extend方法会为Ctor添加一个super属性,指向其父类构造器
* 如果构造函数上有super 说明Ctor是Vue.extend构建的子类 换句话说就是检查是否有父级组件
* 然后再用递归的方式获取基类上的配置选项,也就是获取所有上级的options合集
*/
const superOptions = resolveConstructorOptions(Ctor.super)
// Ctor.superOptions:父级组件的options Vue构造函数上的options,如directives,filters,....
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
// 如果父级组件被改变过,更新superOption
Ctor.superOptions = superOptions
// 检查 Ctor.options 上是否有任何后期修改/附加选项
const modifiedOptions = resolveModifiedOptions(Ctor)
if (modifiedOptions) {
//如果存在被修改或增加的选项,则合并两个选项
extend(Ctor.extendOptions, modifiedOptions)
}
// 选项合并,将合并结果赋值为 Ctor.options
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
//当Ctor.super不存在时,如通过new关键字来新建Vue构造函数的实例 直接返回基础构造器的options
return options
}
resolveModifiedOptions
/**
* @description: 检查是否有任何后期修改/附加选项
* @param {*} Ctor 实例构造函数
* @return {*} modified
*/
function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
// 声明修改项
let modified
// 获取构造函数选项
const latest = Ctor.options
// 密封的构造函数选项,备份
const sealed = Ctor.sealedOptions
// 对比两个选项,记录不一致的选项
for (const key in latest) {
if (latest[key] !== sealed[key]) {
if (!modified) modified = {}
modified[key] = latest[key]
}
}
//返回修改项
return modified
}
总结
现在回到开始的问题,Vue 的初始化过程new Vue(options)都做了什么?
- 处理组件配置项
- 初始化组件实例的关系属性,比如 children、refs 等
- 处理自定义事件
- 调用 beforeCreate 钩子函数
- 初始化组件的 inject 配置项,得到 ret[key]= val 形式的配置对象,然后对该配置对象进行浅层的响应式处理(只处理了对象第一层数据),并代理每个 key 到 vm 实例上
- (响应式原理的核心)数据响应式,处理 props、methods、data、computed、watch 等选项(顺序也是按照这个执行)
- 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
- 调用 created 钩子函数
- 如果发现配置项上有 el 选项,则自动调用 mount 方法,反之,没提供 el 选项则必须调用 $mount
- 进入挂载阶段