本文仅对Vue主体做一个流程化的简单解析,其他详细功能的代码分析过程是相同的。阅读源码之前,首先要确保对Vue.js的使用较为熟悉。先知道怎么用,再研究怎么实现。
首先去github下载官方源码:
当前版本为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:</span></strong>${<span style="color:#458383;">vm</span>._uid}<strong><span style="color:#008000;">
endTag = vue-perf-end:</span></strong>${<span style="color:#458383;">vm</span>._uid}<strong><span style="color:#008000;">
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 </span></strong>${<span style="color:#458383;">vm</span>._name}<strong><span style="color:#008000;"> init
, startTag, endTag)
}
if (vm.KaTeX parse error: Expected '}', got '&' at position 15: options.el) { &̲nbsp; &nbs…mount(vm.KaTeX parse error: Expected 'EOF', got '&' at position 13: options.el) &̲nbsp; } }</pre>…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.KaTeX parse error: Expected 'EOF', got '#' at position 91: …le="background:#̲FFFFFF;"><em><s…options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.KaTeX parse error: Expected 'EOF', got '#' at position 43: …n style="color:#̲660E7A;">abstra…parent) {
parent = parent.KaTeX parse error: Expected 'EOF', got '&' at position 11: parent<br>&̲nbsp; &nbs…children.push(vm)
}
vm.KaTeX parse error: Expected 'EOF', got '#' at position 36: …n style="color:#̲458383;">parent…root = parent ? parent.KaTeX parse error: Expected 'EOF', got '&' at position 18: …ot : vm<br><br>&̲nbsp; vm.children = []
vm.KaTeX parse error: Expected 'EOF', got '&' at position 18: …fs = {}<br><br>&̲nbsp; vm._watch…children。
② 将vm的parent修改为①中找到的parent。
③ 修改vm的根
r
o
o
t
。
若
v
m
的
p
a
r
e
n
t
存
在
,
则
与
p
a
r
e
n
t
的
root。若vm的parent存在,则与parent的
root。若vm的parent存在,则与parent的root统一;否则KaTeX parse error: Expected 'EOF', got '&' at position 20: …就是vm自身。</p><p>④&̲nbsp; 修改v…options.el) {
vm.KaTeX parse error: Expected 'EOF', got '#' at position 33: …n style="color:#̲458383;">vm</sp…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,
e
l
,
el,
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实例。