本文主要探讨一下,Vue的beforeCreate和created阶段一个大致运行流程,我们知道,它的生命周期包括,beforeCreate,created,beforeMount,mounted,beforeUpdate,updated,beforeDestroy,destroyed。出于简单起见,这里暂时讨论只讨论beforeCreate,created阶段
本次探究所用的代码如下,vue版本为v2.5.17-beta.0
:
<div id="box">
<ul>
<li v-for="item in data" @click="logChange(item.NAME,item.AGE)">
{{item.NAME}}
li>
ul>
div>
//js
new Vue({
el: '#box',
data: {
data:[
{NAME:'SDF',AGE:3},
{NAME:'JIKU',AGE:6},
{NAME:'HYF',AGE:3}
]
},
methods: {
logChange: function(name,age) {
console.log(name,age);
}
}
});
一、beforeCreate阶段
这个阶段主要是完成vue中关于生成周期以及事件的一些初始化工作,在这之前它会执行一个mergeOptions
函数,得到$options
选项,并把它设置成Vue实例的一个属性。
1.1、生成$options
选项
//vm为Vue实例
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
这个函数首先是检查我们的一个组件名称是否合法,它会执行如下代码片段:
//name是传入的组件名称
if (!/^[a-zA-Z][\w-]*$/.test(name)) {
warn(
'Invalid component name: "' + name + '". Component names ' +
'can only contain alphanumeric characters and the hyphen, ' +
'and must start with a letter.'
);
}
if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
);
}
从这里我们可以看出,一个合法的组件名应该是必须是以字母开头,并且中间可以加一个或多个横线连接,但vue中内置的一些标签名如'slot,component'
以及常规的html不能用作组件名,这个标签如下;一种是标准的html,还有就是svg标签。
var isHTMLTag = makeMap(
'html,body,base,head,link,meta,style,title,' +
'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +
'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' +
'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +
's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +
'embed,object,param,source,canvas,script,noscript,del,ins,' +
'caption,col,colgroup,table,thead,tbody,td,th,tr,' +
'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +
'output,progress,select,textarea,' +
'details,dialog,menu,menuitem,summary,' +
'content,element,shadow,template,blockquote,iframe,tfoot'
);
// this map is intentionally selective, only covering SVG elements that may
// contain child elements.
var isSVG = makeMap(
'svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,' +
'foreignObject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,' +
'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view',
true
);
1.2、对传入的选项做合并处理
该过程会执行mergeField
方法。
function mergeField (key) {
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
这个方法里面,它是根据我们传入的选项对象的键名来确定一个“策略函数”,然后将相关的键值传入该函数,最后得到$options
对象的一个属性信息(即生成键值对)。其中parent值的信息如下图
以components
键为例,它对应的“策略函数”,mergeAssets
函数
function mergeAssets (
parentVal,
childVal,
vm,
key
) {
var res = Object.create(parentVal || null);
if (childVal) {
"development" !== 'production' && assertObjectType(key, childVal, vm);
return extend(res, childVal)
} else {
return res
}
}
在这个函数中,它是将父选项作为一个对象的原型对象(如果有父选项传入的话),然后将所有子选项的属性复制到该对象上(如果有子选项传入的话)。通过执行这些合并操作后,最后在本例中,得到的$option对象如下:
1.3、初始化代理对象
这一步是生成Vue实例的一个渲染代理对象,通过new Proxy(vm, handlers)
生成。
之后相关数据的动态绑定都是要依靠这个代理对象来实现的。它会执行initProxy
方法
initProxy = function initProxy (vm) {
if (hasProxy) {
// 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);
} else {
vm._renderProxy = vm;
}
};
hasProxy
用于判断浏览器是否支持Proxy
代理。如果不支持,这个渲染代理就用Vue实例本身。
1.3、给Vue实例添加与生命周期相关的属性
例如:
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;
最后再初始化与父级组件相关的事件以及添加生成虚拟节点的工具函数。
例如
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
二、created阶段
这个阶段主要是初始化与依赖注入相关的操作,以及数据的动态绑定。
会依次执行
initInjections(vm); // resolve injections before data/props
initState(vm);//涉及给数据绑定监听器,给方法绑定vue实例的执行上下文
initProvide(vm); // resolve provide after data/props
由于本文没有涉及到依赖注入,只探讨initState
方法,这个方法中涉及的流程如下:
2.1、初始化方法
这个阶段是对给方法绑定vue实例的执行上下文,它会遍历我们传入的methods选项。
function initMethods (vm, methods) {
var props = vm.$options.props;
for (var key in methods) {
{
if (methods[key] == null) {
warn(
"Method \"" + key + "\" has an undefined value in the component definition. " +
"Did you reference the function correctly?",
vm
);
}
if (props && hasOwn(props, key)) {
warn(
("Method \"" + key + "\" has already been defined as a prop."),
vm
);
}
if ((key in vm) && isReserved(key)) {
warn(
"Method \"" + key + "\" conflicts with an existing Vue instance method. " +
"Avoid defining component methods that start with _ or $."
);
}
}
vm[key] = methods[key] == null ? noop : bind(methods[key], vm);
}
}
从这里可以看出,方法选项对象的键与值必须存在;不能与props选项对象中的某个键同名;不能与vue实例自带的以$或_开关的属性同名;否则它都会给我们一个错误的警告。最后会给每个方法绑定与vue实例的执行上下文即(bind(methods[key], vm);
)并且将绑定好上下文的方法添加到vue实例的一个属性上。
2.2、初始化选项数据
function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
if (!isPlainObject(data)) {
data = {};
"development" !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
);
}
// proxy data on instance
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
{
if (methods && hasOwn(methods, key)) {
warn(
("Method \"" + key + "\" has already been defined as a data property."),
vm
);
}
}
if (props && hasOwn(props, key)) {
"development" !== 'production' && warn(
"The data property \"" + key + "\" is already declared as a prop. " +
"Use prop default value instead.",
vm
);
} else if (!isReserved(key)) {
proxy(vm, "_data", key);
}
}
// observe data
observe(data, true /* asRootData */);
}
在这里首先是var data = vm.$options.data;
从$options
里面获取数据选项,当然我们设置的数据选项可能是通过一个函数返回的对象,这就是为什么会有typeof data === 'function' ? getData(data, vm): data || {};
而getData
函数里是会执行我们传入的会返回数据选项的函数,具体如下:
function getData (data, vm) {
// #7573 disable dep collection when invoking data getters
pushTarget();
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, "data()");
return {}
} finally {
popTarget();
}
}
从这里可以看出,返回数据选项的函数也会在vue实例的上下文中执行,它的第一个参数也是vue实例。
可以看出,如果这个方法里没有返回一个对象它就会给出一个data functions should return an object
警告。
之后就是进到一个循环中遍历并检查我们的数据选项,是否与methods
或props
选项中的键同名,如果有同名情况就会给出警告。一切正常,才给相关的数据项在vue实例身上添加对应一个存取器属性,这是必须的,这是完成动态数据绑定必须完成的一步。
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
在这里执行proxy(vm, "_data", key);
key在本例中的一个值就是data
即数据选项的一个键名,它对应的值就是一个数组。target是vue实例。这里当之后我们对数据做改变时,就会执行这里的setter和getter方法。
最后就是给数据添加相应的“观察器”对象(即执行observe(data, true /* asRootData */);
方法)。
2.2.1、给数据添加“观察器”对象
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}
在这个方法里,它其实会通过间接递归的方式来给我们的数据选项逐个添加“观察器“对象,因为我们的数据可能是对象里嵌套有对象或数组,同时数组里面也可能会有对象存在。
一进来时,首先判断数据是否是一个引用类型。如果不是,就不会执行后面的操作。如果某个数据项已经存在”观察器“对象,就使用之前的,而不会重新计算出新的”观察器“对象,如果没有,它也会判断这个数据对象是否是一个对象,或数组并且处理可”观察“模式,同时不能是服务器端渲染也要求数据对象是可扩展的,只有这些条件同时满足,才会为这个数据对象计算一个”观察器“对象(即执行ob = new Observer(value);
语句)。
2.2.2、创建“观察器”对象
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment;
augment(value, arrayMethods, arrayKeys);
this.observeArray(value);
} else {
this.walk(value);
}
}
在这里会给Observer
对象依次添加value 、dep、vmCount
属性,以及__ob__
存取器属性,value就是对应数据项的值,在这里第一次调用时,value值是{data:[ {NAME:'SDF',AGE:3}, {NAME:'JIKU',AGE:6}, {NAME:'HYF',AGE:3} ]}
dep属性是一个依赖对象,这个对象里面包含了一个数组,用于存储Observer
对象。
由于这里第一次调用时是一个对象,所以会调用this.walk(value);
语句。而walk
函数是会遍历我们传入的对象的,并给对象里面的属性值又添加Observer
对象,具体如下。
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
这个函数会执行defineReactive(obj, keys[i]);
语句,为数据选项对象的每个键值依次定义与交互相关的存取器属性。
2.2.3、定义与交互相关的存取器属性
function defineReactive (obj,key, val,customSetter, shallow) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if ("development" !== 'production' && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}
调用到这里时,就会为每个数据添加一个对应的存取器属性,在本例中的数据就是[ {NAME:'SDF',AGE:3}, {NAME:'JIKU',AGE:6}, {NAME:'HYF',AGE:3} ]
,从这里我们可以了解到,我们的数据项其实也可以定义成存取器属性,当然如果你的可配置性设置成false
,那么这个数据项就不会有动态交互的行为了,原因就是。
if (property && property.configurable === false) {
return
}
再看下一步
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
这里的意思是,只有某个数据项有getter方法,后面再动态取值的时候,就会从该getter方法中返回的数据获取,否则直接取原值,表现如下。
var value = getter ? getter.call(obj) : val;
当然,在定义某个数据项的存取器方法之前,它又会为该项数据(本例中是data数据项,是一个数组),定义一个Observer
对象 ,这时是第二次调用observe
函数。
接下来会走进Observer
函数的另一个分支如下:
if (Array.isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment;
augment(value, arrayMethods, arrayKeys);
this.observeArray(value);
}
由于var hasProto = '__proto__' in {};
语句会返回true
,所以它会选取protoAugment
函数执行,它是一个数组,在protoAugment
函数中,已经把当前数组的原型链上的方法改变了,这些重写的方法中是包含用于改变数组的方法,例如添加一项,删除一项,数组倒置,数组排序。
function protoAugment (target, src, keys) {
/* eslint-disable no-proto */
target.__proto__ = src;
/* eslint-enable no-proto */
}
在本例中targe值就是[ {NAME:'SDF',AGE:3}, {NAME:'JIKU',AGE:6}, {NAME:'HYF',AGE:3} ]
数组,src就是arrayMethods
,它的原型是Array.prototype
,但它里面的'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'
方法已经被重写。具体如下:
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
var original = arrayProto[method];
def(arrayMethods, method, function mutator () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
var result = original.apply(this, args);
var ob = this.__ob__;
var inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break
case 'splice':
inserted = args.slice(2);
break
}
if (inserted) { ob.observeArray(inserted); }
// notify change
ob.dep.notify();
return result
});
});
从forEach循环中可以看出,它就是为arrayMethods
对象定义了以上的方法属性,重写这些方法就可以实现数组数据的动态绑定。例如当我们调用数据项(数组)的push方法时,它首先会执行数组的原生的push操作,然后为新加入的数据项添加一个Observer
对象if (inserted) { ob.observeArray(inserted); }
,也就是把这个数据加入vue的响应式系统吧,再执行ob.dep.notify();
语句,处理后面一系列的操作比如虚拟节点的调整,DOM的生成等。从这里可以看出,这些方法的重写是非常必要。
让我们再回到上面Observer
函数,在这里的observeArray
函数又会执行observe
函数,为数组中的每一项添加Observer
对象。
Observer.prototype.observeArray = function observeArray (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
}
如此往复,就构成了vue的响应式系统了。数据的初始化过程就是在created阶段完成的。其实说到底。vue中的数据动态绑定其实就是通过setter和getter方法实现的,以上就是beforeCreate和created阶段的大概运行流程。