一、前言
Vue的项目也已经做了四五个了,虽然已经能够基本的做项目,但是要知其然,知其所以然,所以今天来研究一下源码,虽然自己前面也断续了看了一下源码, 但是你懂得,看源码真的看得头痛,所以就放弃了很多次,今天,就开始写这一部分,一则是为了记录自己的学习,以供自己的成长,以及后续的回顾,避免走弯路,二则则是表明自己的决心,相当于立一个flag,督促自己研究下去,好啦,闲话少叙,开工。
二、准备
这里只是下一个打包后的vue.js文件,而没有去下载vue的源码文件,因为vue的源码文件太复杂,我们先拿打包后的vue的js文件来分析,我们先了解vue的基本原理之后,我再下载他的源码文件,一步步来,步子太大,容易扯着…蛋(咳咳) ,当前vue.js的版本为v2.6.10,整体如下:
从上面图可以看出:vue.js整体为一个立即执行函数,学名IIFE,不知道的可以自己去百度一下,这里先不过多叙述,这个立即执行函数最大的作用是防止变量的污染,第一个参数将当前环境的this传入,第二个factory,可以理解为工厂函数,里面的typeof检验exports,module,module.exports在当前环境是否可用,不知道这三个东西的可以去看ES6阮一峰,总之就是各种校验之后调用工厂函数。
三、调试
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>vue-sync</title>
<!-- <script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script> -->
<script src="./vue.js"></script>
</head>
<body>
<div id="app">
{{msg}}
</div>
</body>
<script>
var vm = new Vue({
el: '#app',
data: {
msg: 'hello vue'
}
})
</script>
</html>
从上面可以看出,创建一个Vue实例,然后在vue.js找到构造函数
function Vue(options) {
if (!(this instanceof Vue) // 如果没有使用new Vue创建实例的方式使用Vue,会报下面的警告
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
找到this._init
Vue.prototype._init = function (options) {
debugger
var vm = this;
// a uid
vm._uid = uid$3++;
var startTag, endTag;
/* istanbul ignore if */
if (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 { // 合并options,因为Vue自身有option,与用户传入的option合并
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
/* istanbul ignore else */
{
initProxy(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 (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);
}
};
可以看出,_init是放在Vue的原型链上,在上面的_init方法里已经写上了debugger,调试已经开始了,浏览器打开index.html 按F12
上面的config为全局配置,你可以查找var config = 就可以定位到相应的位置,先不说,具体作用请看官方API文档全局配置,而config.performance表示
设置为 true 以在浏览器开发工具的性能/时间线面板中启用对组件初始化、编译、渲染和打补丁的性能追踪。只适用于开发模式和支持 performance.mark API 的浏览器上。 所以config.performance下方的代码是性能相关,先不去了解,不是说我们每行真的要懂,不可能的,一个框架,是经过了很多时间来磨合,要解决很多问题,所以有些我们不懂很正常,不可能一下就全懂一个框架源码,所以我们要学会去蚕食,去分解,最后掌握大部分,即可。
// 这段代码是合并options,将前面我们传入的options与框架自身的options合并,
// 并将合并options放入到Vue的$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.constructor传入,即将当前实例的构造函数传入
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
function resolveConstructorOptions(Ctor) { // Ctor为Vue方法
var options = Ctor.options;
// 有super属性,说明Ctor是Vue.extend构建的子类 这里就涉及到api extend,我们这里先不说extend
//extend后续再讲
// 这里就可以看出 当new Vue时是没有super属性的,当调用Vue.extend生成子类继承Vue,会设置super属性
if (Ctor.super) {
var superOptions = resolveConstructorOptions(Ctor.super);
var cachedSuperOptions = Ctor.superOptions;
if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions;
// check if there are any late-modified/attached options (#4976)
var modifiedOptions = resolveModifiedOptions(Ctor);
// update base extend options
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions);
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);
if (options.name) {
options.components[options.name] = Ctor;
}
}
}
return options
}
// 合并options
function mergeOptions(
parent,
child,
vm
) {
{
//检测用户传入的options里面的component属性是否符合html5规范,不符合会警告
checkComponents(child);
}
// 如果child为方法,让child等于child里面的options
if (typeof child === 'function') {
child = child.options;
}
// 以下三个是验证pros,inject directive,这里先没看,先不看
normalizeProps(child, vm);
normalizeInject(child, vm);
normalizeDirectives(child);
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm);
}
if (child.mixins) {
for (var i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm);
}
}
}
var options = {};
var key;
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
function mergeField(key) {
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options
}
合并之后获得的options
可以看出parent是Vue自带的option,而child是我们自己定义的,vm为this,主要是合并的代码,这里应该不难,一步步 调试,就可以看出变化。将options合并到vue的$options上之后。
下面进来代理拦截initProxy,而proxy是ES6的语法,语法详情可以看阮一峰ES6 proxy,对this对象进行拦截,其中hasProxy验证当前环境是否可以使用Proxy
initProxy = function initProxy(vm) {
if (hasProxy) { // 当环境能使用proxy 为true
// determine which proxy handler to use
var options = vm.$options;
var handlers = options.render && options.render._withStripped
? getHandler
: hasHandler;
vm._renderProxy = new Proxy(vm, handlers); // 当访问vm的时候,会先进入handlers拦截过滤
} else {
vm._renderProxy = vm;
}
};
}
// 当获取的时候触发,判断key是否是字符串 并且是否属于对象属性,如果不通过 则警告
var getHandler = {
get: function get(target, key) {
if (typeof key === 'string' && !(key in target)) {
if (key in target.$data) { warnReservedPrefix(target, key); }
else { warnNonPresent(target, key); }
}
return target[key]
}
};
// 判断key是否是保留字 第一个字符是下划线为保留字,如果是则警告, 不是则通过
var hasHandler = { // 在Proxt中,has在 for...in中会使用到,意思就是说循环遍历的时候会触发
has: function has(target, key) { // target为目标对象
var has = key in target;
var isAllowed = allowedGlobals(key) ||
(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data));
if (!has && !isAllowed) {
if (key in target.$data) { warnReservedPrefix(target, key); }
else { warnNonPresent(target, key); }
}
return has || !isAllowed
}
};
就是说在使用vm的使用,会先调用initProxy设置的代理过滤,如果不满足条件的话,会警告你的,后面会单独开一章来举列子说明吧,现在将的比较笼统,只要知道这个是设置拦截过滤了,不满足条件的话会警告的,我们继续调试。
然后就到了关键部分了,如下如所示,这就不得不讲到Vue的生命周期了。
相信大家对这张图片不陌生吧。下面的图片就是对我们上面截图图片的流程化的介绍,我们一一分析
开始我们new Vue对应上图头部
... //前面代码省略
vm._self = vm;
initLifecycle(vm); // 初始化生命周期 其实就是一些变量的定义,详情可以自己看,不难
initEvents(vm); // 初始化事件
initRender(vm); // 初始化渲染 里面代码看不懂,以后研究
callHook(vm, 'beforeCreate'); // 执行beforeCreate钩子函数
initInjections(vm); //初始化注入 涉及到inject的用法
initState(vm); // vue双向绑定相关
initProvide(vm); // 初始化 provide 与上面的inject相关
callHook(vm, 'created'); // 执行created钩子,这个时候data数据已经完成绑定,所以这就是为什么我们
//一般在created调用方法的原因了。
/* istanbul ignore if */
if (config.performance && mark) { // 设置为 true 以在浏览器开发工具的性能/时间线面板中
//启用对组件初始化、编译、渲染和打补丁的性能追踪。只适用于开发模式和支持
// performance.mark API 的浏览器上。
vm._name = formatComponentName(vm, false);
mark(endTag);
measure(("vue " + (vm._name) + " init"), startTag, endTag);
}
if (vm.$options.el) { // 判断是否有el选项,没有的话 自行调用vm.$mount也可以渲染
vm.$mount(vm.$options.el);
}
下方为$mount方法
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && query(el);
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
warn(
"Do not mount Vue to <html> or <body> - mount to normal elements instead."
);
return this
}
var options = this.$options;
// resolve template/el and convert to render function
if (!options.render) { // 判断是否有render
var template = options.template;
if (template) {// 判断是否有template
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template);
/* istanbul ignore if */
if (!template) {
warn(
("Template element not found or is empty: " + (options.template)),
this
);
}
}
} else if (template.nodeType) {
template = template.innerHTML;
} else {
{
warn('invalid template option:' + template, this);
}
return this
}
} else if (el) {// 既没有render也没有template 使用外部模板
template = getOuterHTML(el);
}
if (template) {
/* istanbul ignore if */
if (config.performance && mark) {
mark('compile');
}
var ref = compileToFunctions(template, { // 与AST虚拟语法树有关 后续将
outputSourceRange: "development" !== 'production',
shouldDecodeNewlines: shouldDecodeNewlines,
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this);
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
options.render = render;
options.staticRenderFns = staticRenderFns;
/* istanbul ignore if */
if (config.performance && mark) {
mark('compile end');
measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
}
}
}
return mount.call(this, el, hydrating)
};
mount.call(this, el, hydrating)调用如下:
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
mountComponen如下:
function mountComponent (
vm,
el,
hydrating
) {
vm.$el = el;
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode;
{
/* 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'); // 执行beforeMount
var updateComponent;
/* istanbul ignore if */
if (config.performance && mark) {
updateComponent = function () {
var name = vm._name;
var id = vm._uid;
var startTag = "vue-perf-start:" + id;
var endTag = "vue-perf-end:" + id;
mark(startTag);
var 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 = function () {
vm._update(vm._render(), hydrating);
};
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before: function 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'); // 执行mounted
}
return vm
}
上面的挖掘就到此告一段落 你现在不需要知道他们干了什么,实际上上面一大部分都是为了将模板渲染成AST,然后转换成VNode,在转换成html,大致是这个样子,你现在只要知道流程,如上面贴的生命周期流程,现在你应该懂了官方的生命周期是什么意思吧,以及他们相应的钩子,会直接触发的原理的吧。
四、总结
看了上面的分析,你也应该了解很多了,总结一下吧,new Vue -> mergeOptions合并options -> initProxy设置代理 ->初始化生命周期initLifecycle->初始化事件initEvents ->初始化渲染initRender->执行beforeCreate生命钩子->初始化注入initInjections->绑定数据initState->
初始化provide initProvide ->执行created生命钩子->询问是否有el ->没有退出,有则继续询问有没有render->没有询问有没有template->没有使用外部模板->执行beforeMount生命钩子->执行mounted生命钩子
没有画流程图,简单的用箭头表示,偷点懒,拿着官方的生命周期图,对着代码一步步走,一目了然,我偷不偷懒无所谓,下篇文章:Vue源码一行行分析(二)双向绑定原理