如果要成长为一名高级乃至资深前端工程师,不但要熟练使用,还要明白背后的原理,达到举一反三的程度。在整个成长过程中,阅读源码是不可或缺的重要一环。
本篇文章以vue 2.6.10源码为例,为大家分享vue2.x组件实例化流程。
首先创建一个index.html文件,并且引入vue2.6.10.js
<!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>Hello Vue</title>
</head>
<body>
<div id="app">
<div>{{a}}</div>
</div>
<script type="text/javascript" src="vue_2.6.10.js"></script>
<script>
var vm = new Vue({
el: "#app",
data: {
a: 1
}
});
console.log(vm.$options);
</script>
</body>
</html>
控制台输出选项参数为

可以看到除了我们初始化时传递进去的el和data参数,还多了好多其他的参数,例如components,directives等。特别需要注意的是,传进去的data是一个对象,但是却变成了一个函数。
这是什么原因呢?
下面从Vue实例初始化流程来为大家具体分析,内容有点多,还希望大家能够耐心看完哦。
在vue2.6.10.js中首先检测宿主环境是否支持匹配条件
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.Vue = factory());
}(this, function () { 'use strict';
然后定义Vue工厂函数factory(),并且赋值给global.Vue,首先检测是不是通过new Vue()来初始化根实例,如果直接在index.html中把Vue()当作一个普通函数来调用,则会发出警告;options就是在index.html中new Vue()时传递的选项参数。
function Vue (options) {
if (!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
调用this._init(options)对选项参数做处理,通过函数initMixin(Vue)来完成。
function initMixin (Vue) {
// 定义Vue的原型属性_init
Vue.prototype._init = function (options) {
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 {
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);
}
};
}
在该函数中,定义Vue的原型属性_init,调用mergeOptions对options参数做处理
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
resolveConstructorOptions函数定义如下,传递参数vm.constructor就是Vue构造函数。所以Ctor等价于Vue,Ctor.super为false。说明该函数不是给Vue根实例用的,而是给Vue子类构造函数用的。对于Vue父类,直接返回父类的options。
function resolveConstructorOptions (Ctor) {
var options = Ctor.options;
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
}
那么Vue根实例的子组件options参数怎么处理呢?通过Vue.extend来进行处理
Vue.extend = function (extendOptions) {
extendOptions = extendOptions || {};
var Super = this;
var SuperId = Super.cid;
var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
var name = extendOptions.name || Super.options.name;
if (name) {
validateComponentName(name);
}
var Sub = function VueComponent (options) {
this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.cid = cid++;
Sub.options = mergeOptions(
Super.options,
extendOptions
);
Sub['super'] = Super;
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps$1(Sub);
}
if (Sub.options.computed) {
initComputed$1(Sub);
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend;
Sub.mixin = Super.mixin;
Sub.use = Super.use;
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type];
});
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub;
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options;
Sub.extendOptions = extendOptions;
Sub.sealedOptions = extend({}, Sub.options);
// cache constructor
cachedCtors[SuperId] = Sub;
return Sub
};
}
可以看到上述函数也调用了mergeOptions,说明mergeOptions 非常重要,根实例和子组件的options处理都会调用它。
Vue父类的options通过调用函数initGlobalAPI(Vue)来进行处理,将ASSET_TYPES数组的每一项添加进Vue.options,然后再调用mergeOptions进行合并。
Vue.options = Object.create(null);
.forEach(function (type) {
Vue.options[type + 's'] = Object.create(null);
});
ASSET_TYPES数组定义如下,至此回答了为什么控制台输出的Vue options会多出好多项。那为什么要在Vue.options的每一项key值中加's',不直接在ASSET_TYPES数组中进行添加呢?因为ASSET_TYPES在好多地方会复用。
var ASSET_TYPES = [
'component',
'directive',
'filter'
];
还有一个遗留问题:输入的data是一个对象,为什么输出的data变成了一个函数?
因为vue2.5.x中引入了选项的策略处理
接下来我们分析mergeOptions函数,其中运用了选项的策略处理,本质就是通过javascript增强配置选项的表达力。首先遍历父组件的key值,然后遍历子组件,如果父子组件中有相同的key值,则只调用mergeField处理一次。
function mergeOptions (
parent,
child,
vm
) {
{
checkComponents(child);
}
// ...
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
}
接下来看strats相关的定义
var strats = config.optionMergeStrategies;
config是一个全局的配置对象,config.optionMergeStrategies默认为一个空对象
var config = ({
/**
* Option merge strategies (used in core/util/options)
*/
// $flow-disable-line
optionMergeStrategies: Object.create(null),
// ...
});
strats.data被定义为一个函数,最终返回函数mergedInstanceDataFn,这就解释为什么我们输入一个data对象,而控制台输出函数的问题了。
strats.data = function (
parentVal,
childVal,
vm
) {
if (!vm) {
if (childVal && typeof childVal !== 'function') {
warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
);
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
};
function mergeDataOrFn (
parentVal,
childVal,
vm
) {
if (!vm) {
// in a Vue.extend merge, both should be functions
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
// when parentVal & childVal are both present,
// we need to return a function that returns the
// merged result of both functions... no need to
// check if parentVal is a function here because
// it has to be a function to pass previous merges.
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
return function mergedInstanceDataFn () {
// instance merge
var instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal;
var defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal;
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
接下来接着讲自定义策略处理, initGlobalAPI(Vue)函数除了扩展静态属性,还对Vue的全局配置config设了一个拦截,表明用户只能读取congfig的值,而不能修改它。
// config
var configDef = {};
configDef.get = function () { return config; };
{
configDef.set = function () {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
);
};
}
Object.defineProperty(Vue, 'config', configDef);
那我们怎么实现自定义策略处理呢?需要借助Vue.config.optionMergeStrategies,验证如下
<script>
Vue.config.optionMergeStrategies.count = function(partentVal, childVal, vm) {
return childVal >= 100 ? 100 : childVal;
};
var vm = new Vue({
el: "#app",
data: {
a: 1
},
count: 99
});
console.log(vm.$options);
</script>
控制台输出如下
当count赋值99时,输出99

当count赋值101时,输出100
