本文仅对Vue主体做一个流程化的简单解析,其他详细功能的代码分析过程是相同的。阅读源码之前,首先要确保对Vue.js的使用较为熟悉。先知道怎么用,再研究怎么实现。
首先去github下载官方源码:
https://github.com/vuejs/vue
当前版本为2.5.17。
解压缩。为了方便查看,导入到webstrom中:
源码必然在src文件夹。
查看src文件夹:
从文件夹名看,compiler应该是编译相关;core应该是内核相关;platforms应该是平台相关;server应该是服务端相关;sfc无法直观猜测;shared是共享的意思,从名称也无法直接猜测出作用。要查看vue的主体实现,那这几个文件夹,必然应该选core。
打开core,列出了几个文件夹,以及两个js文件:config.js和index.js。
config.js很明显是配置文件。index.js,这必然就是入口了。于是打开index.js:
可以看到做了几个事情:
1. import了一些对象进来。
2. 调用了initGlobalAPI()。
3. 调用了Object.defineProperty()。
4. 为Vue对象设置version变量。
5. 将Vue对象默认导出。也就是说,虽然Vue是导入的,但经过一系列操作后,又将Vue进行了导出。相当于是Vue对象在该文件中进行了加工。
imoprt进来的对象,必然是后面用到的。所以可以先不管,等用到的时候再回来查。先来看执行的第一个函数:
initGlobalAPI(Vue)
面对这行代码,需要搞清楚两个问题:
① initGlobalAPI()是在哪定义的,具体实现是什么。
② 参数Vue明显是个对象,且跟Vue库同名,所以Vue必然是最重要的主对象。这个Vue是在哪定义的,具体实现是什么。
这两个问题,明显②比较重要。所以先看Vue的实现。
查看最上方的import,有这么一行:
import Vue from './instance/index'
也就是说,Vue是在与index.js同目录下的instance文件夹下的index.js中定义的。
打开该index.js:
可以看到在这个index.js中做了几个事情:
1. import了一些对象进来。
2. 定义了一个名叫Vue的function,且传入了一个options参数。
3. 调用了许多函数,且都将Vue作为参数传入。
4. 将Vue默认导出。
同样不管import,先来分析Vue这个function的定义。
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) }
这个function的几个要点:
① 名称是Vue,js常用的扩展方式是实现一个function,然后再new。这个函数就是这样的一个构造函数。回想我们在用Vue的时候,也都是采用
var vm = new Vue({
// 选项
})
这样的形式。
② 函数内首先是一个if。判断条件暂且不看,if若成立,会有一个warn:
warn('Vue is a constructor and should be called with the `new` keyword')
大意是,该Vue是一个构造函数,必须使用new来调用。
于是就可以猜测if的判断条件,必然是:若不使用new来调用了Vue,则提示warn。
③ 函数最后是一个函数调用:
this._init(options)
this就是new出的Vue对象了。_init()必然是Vue的一个成员函数,从函数名看,其作用是初始化。options是我们传入的参数。结合Vue的具体使用,可知传入的options就是那一堆data,props,methods等。
那么,现在的一个疑问就是,Vue对象是在什么时候定义了这个_init()成员函数?
继续往下看代码,此时执行了5个函数:
initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue)
根据import,可以找到这5个函数的js文件。先来看第一个:
import { initMixin } from './init'
打开同级目录下的init.js,可以看到:
首先imoprt了一堆对象进来。
然后定义了一个uid,并赋值为0。
然后导出了一个function,名为initMixin(),接受一个class类型参数,参数名为Vue。
进入initMixin(),第一行代码就是:
Vue.prototype._init = function (options?: Object) {
这里直接修改了传入参数Vue的prototype,为其设置了一个名为_init的函数,该函数接收一个Object类型参数,参数名为options。
于是上面的疑问得以解答:Vue对象的_init()成员函数是在initMixin()中创建并挂载到Vue对象上的,并且对Vue对象而言是个prototype全局函数。
然后继续来看_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) } }
首先保存this对象到vm变量中:
const vm: Component = this
用const定义,所以对于vm中的变量不会被修改,当然对象变量和数组变量的内容除外。
然后保存uid,设置_isVue变量等,都没有涉及到感兴趣的操作,忽略。
接着是一段代码:
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging ispretty slow, and none of the
// internal component options needsspecial treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
最上方的注释意思是合并options,所以下面的if..else主要作用的合并传入的options。
先看if条件。当options存在并且options._isComponent为true时,就会调用initInternalComponent(vm, options)。
_isComponent从变量名看,其含义应该是一个标记,用来记录当前options是否属于组件。而initInternalComponent()从函数名看,其作用应当是初始化内部组件。
所以推断,当options属于一个组件时,就会进入if,对内部组件进行初始化。
而对于else,将传入的options与vm本身的属性进行了合并,并重新赋值给vm.$options。
所以经过这一步,Vue实例会将传入的用户自定义options合并到自身属性中。
接着是:
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
proxy是代理的意思。所以这一步是初始化代理。
然后是一堆函数调用:
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolveinjections before data/props
initState(vm)
initProvide(vm) // resolve provide afterdata/props
callHook(vm, 'created')
注释expose real self的意思是暴露自身。因此猜测这一堆函数应该是为Vue实例添加各种接口,事件,以及可调用的属性。
从import找到initLifecycle(),其实现很短:
export function initLifecycle(vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
可以看出主要是做了几件事情:
① 从当前Vue实例vm开始向上查找,找到最近的一级abstract为false的parent。将vm push给它的$children。
② 将vm的parent修改为①中找到的parent。
③ 修改vm的根$root。若vm的parent存在,则与parent的$root统一;否则$root就是vm自身。
④ 修改vm的各种变量。
于是,经过initLifecycle(),vm的parent,以及parent的children,都得到了更新。同时为vm新增了各种变量。
实际上,lifecycle是生命周期的意思,所以initLifecycle从字面即可清晰理解意图:为vm的生命周期做准备,初始化各种变量。
同理,
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolveinjections before data/props
initState(vm)
initProvide(vm) // resolve provide afterdata/props
callHook(vm, 'created')
这些,从名称就可以看出,是为vm实例装配事件、渲染器、状态,data等一系列属性或函数。
注意在这个过程中,插入了两个callHook():
callHook(vm, 'beforeCreate')
…
callHook(vm, 'created')
也就是说,执行完initLifecycle(),initEvents (),initRender()之后,会callHook(vm, 'beforeCreate'),即调用'beforeCreate'钩子函数。
然后继续执行initInjections(),initState(),initProvide(),接着callHook(vm, 'ceated'),即调用'created'钩子函数。
在这里体现出了Vue生命周期中的前两步。
最后:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
当el存在时,将其挂载到vm实例上。
这样,initMixin()就执行完成了。综上可知,该函数为vm添加了各种变量与函数,就像函数名一样:init是初始化的意思,mixin是混入的意思,initMixin()的主要作用就是将vm所需的各种初始化变量与函数混入到vm对象中。
然后返回到instance/index.js中来。
同理:
stateMixin(Vue) :混入props,methods,data,computed,watch。
eventsMixin(Vue) :混入_events,并更新组件的listeners。
lifecycleMixin(Vue) :混入_vnode,$el,$parent相关。
renderMixin(Vue) :混入渲染相关的内容,如$slots等。
经过这些混入操作,Vue由一个什么属性都没有的空对象,变为拥有一堆变量与函数的丰满对象。
现在回到core/index.js。
上面已经解决了最初的两个问题中的第②个,即Vue对象是在哪里定义的,具体实现是什么。
然后来看问题①:initGlobalAPI()是在哪定义的,具体实现是什么。
同理,根据imoprt,打开core/global-api/index.js,查看initGlobalAPI()的具体实现,会发现该函数对Vue对象进行了以下几个操作:
① 为Vue实例设置了一个空的config属性。
② 为Vue的util设置了几个成员,包含 :warn,extend,mergeOptions,defineReactive。
③ 为Vue设置了set,delete,nextTick,以及一个空的options。其中options按类型填充默认空对象。
④ 将自身赋给options._base。
⑤ 将options.components与builtInComponents合并。
⑥ 初始化use,mixin,extend,assetRegisters功能。
于是,经过initGlobalAPI(),Vue实例增加了更多的变量和功能,主要是全局化相关的内容。
现在回到core/index.js。
经过三个属性的设置,以及version修改,最后将功能非常完善的Vue实例对象默认返回。
这个Vue本质上依然是一个构造函数。于是,我们在前端导入Vue.js后,调用:
var vm = new Vue({
// 选项
})
得到的vm对象就是一个包含了所有功能的Vue实例。