前言
距离本小白接触前端开发也已经快将近一年了,从开始的连闭包的原理都需要了解一个晚上(虽然目前深入其原理我也说不明白)到现在能够自己独立完成一个简单的单页webapp,回顾近一年的时间,也是按照前人留下的足迹,一步一个脚印的走,从html到css再到js再到jQuery库,再到webpack,直到Vue。一路走来较为顺利,单也有磕绊,也有疑惑。开始接触Vue的时候,甚为惊艳!竟然有如此之写法,简洁,方便。但是使用过程当然不免充满了疑惑,为什么是这样的?为什么能这样写。在经过近一年的打磨之后,本小白也决定开始研究、研读Vue源码,希望在Vue3.0来临之际,将2.6.X源码研究一遍,为往后3.0打基础,并且将这一年来心中的不少疑惑悉数解开!也希望本文对同样在Vue道路上的朋友有所帮助~
一、准备工作
磨刀不误砍柴工,再开始研读之前,我们需要了解一下Vue的文件的架构以及Vue源码编写使用到的静态类型检查器FLOW。
文件架构
我们从github上面下载最新的vue的源码后,可以看到vue的源码项目文件还是非常庞大的,和一般的webpack类似,我们Vue源码也存在src文件当中。

core文件就是我们这一篇要讲得的核心也是Vue实例初始化的核心所在。
platform文件夹存放的是我们的vue如何进入到运行时的,区分是通过cli打包的文件还是直接在html中引用的情况等。
server文件中存放生成render模板编译的相关代码,例如我们的template是如何编译成render函数,逻辑在这个文件夹中。
FLOW
FLOW是一个静态类型检查器,由于Vue中存在大量的类,引入flow静态检查器来确保类属性不出错,用法类似于TypeScript,这里就不多展开介绍了(其实是不会)。
二、new Vue()
1、Class Vue
正文开始了!首先,我们找到Vue类定义的位置src/core/instance/index.js。
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
在这里看到Vue类的定义非常"简单"= =;做了一个判断防止调用Vue函数,然后就是一个初始化this._init(options)。而_init
就是在initMixin(Vue)是混入到类中的。options就是我们经常main.js写的那个传入的对象:
new Vue({
render: h => h(App),
}).$mount('#app')
那么,接下来 我们就要看一下initMixin到底对Vue做了什么,给这个类添加了什么功能,以及_init()函数到底做了哪些事情。
2、initMixin()
从上面代码可以看到initMixin在同级目录下init.js文件中,我们来看一下initMixin到底干了什么。
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
//some codes
}
}
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
//some codes
}
export function resolveConstructorOptions (Ctor: Class<Component>) {
//some codes
}
function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
//some codes
}
好吧整个文件去掉逻辑部分还是很清晰的,暴露四个方法,可以看到initMixin就是只为Vue添加了一个_init方法,那么接下来我们就重点来看一下_init方法,在Vue实例化的过程中,做了什么吧~
_init()
首先,先把源码贴上~
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
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
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
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
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
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)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el) //实例的首次挂载
}
}
1、mergeOptions
我们一点点来讲,首先判断我们传入的options,根据是否存在options._isComponent来判断如何处理options。Vue的初始化逻辑当然是走下面的,通过mergeOptions()合并了父类(这里是指Vue)options和当前options。那么好奇的童鞋肯定要问了,Vue哪来的options??!!这里卖个关子,后面会讲到的~还是比较重要的一块,所以留个悬念,也算是加深印象~
2、一系列的initXXX()
可以看到接下来就是一系列的init初始化,我们本章大致介绍一下每个init都是干啥的,具体展开讨论会放到后面专门的文中介绍~首先是initLifecycle:它主要是针对生命周期做一些初始化;initEvents:对vue的监听事件的初始化;initRender:初始化Vue的render方法,本文会花一定篇幅展开;callhook:beforeCreate:调起预先设定的beforeCreate钩子函数(没错生命周期钩子函数就在这里慢慢开始了);initInjections(vm):后代组件injection相关初始化;initState(vm):初始化data、props、methods等属性,数据双向绑定的核心所在,之后文章会详细展开。initProvide(vm):后代组件initProvide相关初始化;callHook(vm, 'created'):调起预先设定的created钩子函数。
3、vm.$mount(vm.$options.el)
最后一步就是调用vm.$mount()方法进行实例的挂载。我们可以看到,如果我们传入的属性如果有el属性,才会调用,不然是不会调用的。所以我们在new Vue是会有两种情况:
new Vue({
render: h => h(App),
}).$mount('#app')//不走_init()的mount自己调用
new Vue({
el:'#app'//_init直接调用mount
render: h => h(App),
})
到这里,_init(options)的流程也就结束了,虽然后面的文章我们还会无数次提到他。。。接下来我们就来一起看一下vm.$mount()到底干了些什么,以及具体他是如何挂载我们的Vue实例的。
3、vm.$mount(el)
vm.$mount(el)定义在src/platforms/web/index.js我们一起来看一下代码。
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
看来只是一个封装,我们继续去找mountComponent(this, el, hydrating),代码在src/core/instance/lifecycle.js里面。我们看一下代码:
export function mountComponent ( //vue实例挂载的方法
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating) //挂载实例,调用
}
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
我们一步步看他的逻辑来看他的逻辑。1、!vm.$options.render判断如果实例不存在render函数,则创建一个空的vnode。2、callHook(vm, 'beforeMount')原来这个钩子函数在这儿~3、调用了_update()里面两个参数,vm._render()创建了一个node节点,接下来我们会重点展开。
let updateComponent () => {
vm._update(vm._render(), hydrating)
}
4、new Watcher()添加了一个观察者,这是一个render Watcher监听数据变化后展开节点渲染。(后面介绍watcher的时候会重点讲)目前我们只要知道watcher在初始化会调用传入的updateComponent函数,从而触发渲染和实例挂载。5、callHook(vm, 'mounted'):mounted钩子函数都不陌生。到这里vm.$mount函数也就走完了!
三、总结
本来打算把vm._update(vm._render(), hydrating)也放在这里面讲的,但是一想,如果仓促展开,可能有些细节回顾及不到而且vm._update()和vm._render()也是比较重要的两块一块涉及patch(肯定就会涉及到diff算法)另一块涉及到Vnode创建,现在本小白也处于比较没有思绪的情况,不知如何展开大家会比较容易接受,需要再整理整理大纲在接着往下来写~所以就先到这儿吧!在这里也贴一下目前本小白学习整理的大纲:Vue源码解读
最后的最后打个小小的广告~
有大佬们需要短信业务,或者号码认证的能力的话,可以看看这里!中国联通创新能力平台 运营商官方平台!没有中间商赚差价~