关于Vue的响应式系统在近几年已经被无数的人提及过。 为了避免炒剩饭从本章开始我们将以源码级的角度分析Vue响应式更新过程每个细节点。
定位initState函数
function initState(vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) {
initProps(vm, opts.props);
}
if (opts.methods) {
initMethods(vm, opts.methods);
}
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */ );
}
if (opts.computed) {
initComputed(vm, opts.computed);
}
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
initState 函数是很多选项初始化的汇总,在 initState 函数内部使用 initProps 函数初始化props 属性;使用 initMethods 函数初始化 methods 属性;使用 initData 函数初始化 data选项;使用 initComputed 函数和 initWatch 函数初始化 computed 和 watch 选项。 为了内容更加符合标题,接下来以 initData 为切入点为大家讲解 Vue 的响应系统。
如下是 initState 函数中用于初始化 data 选项的代码:
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */ );
}
在此判断 opts.data 是否存在,即 data 选项是否存在,如果存在则调用 initData(vm) 函数初始化 data 选项,否则通过 observe 函数观测一个空的对象,并且 vm._data 引用了该空对象。其中 observe 函数是将 data 转换成响应式数据的核心入口。
由于没有先讲Vue中的选项合并处理,在此还是给大家解释下。 opts.data是否有值取决你是否有定义data 选项。
如下:
var vm = new Vue({
el: "app",
data: {
message: "this is test code "
}
})
此时 vm.$options.data 就有值并且最终被处理成了一个函数,且该函数的执行结果才是真正的数据。后续会开个章节讲解下Vue选项处理合并策略。
initData
function initData(vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function' ?
getData(data, vm) :
data || {};
if (!isPlainObject(data)) {
data = {};
warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
);
}
// proxy data on instancevar 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)) {
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 dataobserve(data, true /* asRootData */ );
}
接下来我们来了解下initData 函数看下它做了什么,首先定义 data 变量,它是vm.$options.data的引用。在刚刚我们讲到 vm.$options.data其实是在选项合并阶段被处理成了一个函数,且该函数的执行结果才是真正的数据。在上面的代码中依然存在一个使用 typeof 语句判断data数据类型的操作,那么这里的判断还有必要吗 ?
答案是有,这是因为 beforeCreate 生命周期钩子函数是在 选项合并阶段之后 initData 之前被调用的,如果在 beforeCreate 生命周期钩子函数中修改了 vm.$options.data 的值,那么在 initData 函数中对于 vm.$options.data 类型的判断就是有存在的必要了。
在回归到源码,如果 vm.$options.data 的类型为函数,则调用 getData 函数获取真正的数据。
function getData(data, vm) {
// #7573 disable dep collection when invoking data getterspushTarget();
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, "data()");
return {}
} finally {
popTarget();
}
}
可以看到 getData 函数的作用其实就是通过调用 data 函数获取真正的数据对象并返回,即:data.call(vm, vm),而且我们注意到 data.call(vm, vm) 被包裹在 try...catch 语句块中,这是为了捕获 data 函数中可能出现的错误。如果有错误发生那么则返回一个空对象作为数据对象。pushTarget、popTarget 函数暂时不解释在依赖收集阶段再来介绍。
再回到 initData 函数中:
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {};
当通过getData拿到最终的数据对象后,将该对象赋值给 vm._data 属性,同时重写了 data 变量,此时 data 变量已经不是函数了,而是最终的数据对象。
接下来是个if 判断:
if (!isPlainObject(data)) {
data = {};
warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
);
}
上面的代码中使用 isPlainObject 函数判断变量 data 是不是一个纯对象,如果不是纯对象那么在非生产环境会打印警告信息。
接下来代码:
// proxy data on instancevar 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)) {
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 dataobserve(data, true /* asRootData */ );
Object.keys 、 while 循环这些就略过了直接挑重点讲。
if (methods && hasOwn(methods, key)) {
warn(
("Method \"" + key + "\" has already been defined as a data property."),
vm
);
}
这是在做什么?这段代码的意思是在非生产环境下如果发现在 methods 对象上定义了同样的key,也就是说 data 数据的 key 与 methods 对象中定义的函数名称相同,那么会打印一个警告,提示开发者:你定义在 methods 对象中的函数名称已经被作为 data 对象中某个数据字段的key了,需要换个名字。
为什么要这么做?如下示例代码:
const vm= new Vue({
data: {
count: 1
},
methods: {
unique: function(){}
}
})
ins.count // 1ins.unique // function
可以看到不管是定义在 data 中的数据对象,还是定义在 methods 对象中的函数,都可以通过实例对象代理访问。为了避免产生覆盖掉的现象这么做是必然之举。
接下来代码:
if (props && hasOwn(props, key)) {
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);
}
在处理 props 对象中的key字段时与上同理。当上面的代码中当if语句的条件不成立,则会判断 else if 语句中的条件:!isReserved(key),该条件的意思是判断定义在 data 中的 key 是否是保留键。isReserved 函数通过判断一个字符串的第一个字符是不是 $ 或_来决定其是否是保留的,Vue 是不会代理那些键名以 $ 或 _ 开头的字段的,因为Vue自身的属性和方法都是以 $ 或 _ 开头的,所以这么做是为了避免与 Vue 自身的属性和方法相冲突。 了解更多 isReserved 详情
如果 key 既不是以 $ 开头,又不是以 _ 开头,那么将执行 proxy 函数,实现实例对象的代理访问:
proxy(vm, "_data", key);
proxy 源码如下:
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 函数的原理是通过 Object.defineProperty 函数在实例对象 vm 上定义与 data 数据字段同名的访问器属性,并且这些属性代理的值是 vm._data 上对应属性的值。
const vm = new Vue ({
data: {
count: 1
}
})
如上示例代码当我们访问 vm.count 时实际访问的是 vm._data.count 。而 vm._data 才是真正的数据对象。
最后一句:
observe(data, true /* asRootData */)
正式进入响应式之路。