欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

vue 访问对象属性_Vue 响应式系统(一)- 实例对象代理访问数据

程序员文章站 2022-07-15 22:20:38
...

关于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 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)) {
			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 */ );
}

接下来我们来了解下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 getters
	pushTarget();
	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 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)) {
		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 */ );

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 // 1
ins.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 */)

正式进入响应式之路。