概要
本文通过分析Vue 2.0 源码,探讨一下为什么在Vue 2.0中,我们可以在method,filter,计算属性和自定义事件的方法中,通过this指针访问data方法的返回值。最后我们将关键的代码抽取出来,模拟出具体的实现过程。
本文使用的Vue源码版本是2.6.14。Github地址是https://github.com/vuejs/vue.git。
代码流程分析
该功能是Vue 2.0整个初始化过程的的一小部分。具体函数调用关系如下:
- 调用初始化方法_init, 将指向Vue实例的指针this备份到全局变量vm中,方便后面的方法使用。
- 调用initState方法,判断实例化Vue时候的实参对象是否包含data字段。
- 如果包含data字段,调用initData方法,执行data字段对应的方法,将方法返回值放到的全局变量vm._data中。
- 在vm中为data方法返回值的所有key生成代理,从而实现使得Vue实例可以访问data方法的返回数据。
1. 调用初始化方法_init
在/src/core/instance/index.js 中,调用_init方法
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
- 如果在测试环境,如果不是通过new关键字创建Vue实例,则抛出警告。
- 调用_init(options),参数就是我们实例化Vue对象时候的实参。
- _init方法定义参看/src/core/instance/init.js,源码较长,不再逐一列出。
- 该方法一开始将指向Vue实例的this指针进行备份,避免JS中的this指针丢失的问题,将其作为实参供下面的其它初始化方法使用。
- 该方法会调用initState方法去获取data方法返回值。
2. 调用initState方法
该方法的定义请参考在/src/core/instance/state.js ,具体如下:
export function initState (vm: Component) {
vm._watchers = []
const 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)
}
}
- 调用该方法的实参vm是当前指向Vue对象的this指针。
- 通过用户在实例化Vue对象时的实参,获取用户自定义属性,方法。
- 判断是否定义名为data的key,如果定义,调用initData方法。
3. 调用initData方法
该方法的定义请参考在/src/core/instance/state.js ,具体如下:
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== '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
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== '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 */)
}
- 获取Vue的构造方法中传入的参数。
- 在传入参数中,如果data是一个方法,则调用getData方法。
- 在getData方法中执行data方法,该方法请见附录。
- 将getData返回值赋值给局部变量data 和 vm._data,其中,vm._data作为Vue实例的私有属性使用。
- 如果data方法的返回值不是一个通过{}或new Object方式创建的Plain对象,isPlainObject 方法(具体定义见附录)返回false,局部变量data 和 vm._data将被赋值为{}。
- 获取data方法返回值的所有key值(不包含原型链的)。
- 获取所有用户自定义属性和方法。
- 如果data返回值对象的key和用户的自定义方法和属性重名,抛出警告。
- 如果data返回值对象的key不是$或_,调用proxy方法, 生成代理。
4. 调用proxy方法
在/src/core/instance/state.js 中,调用proxy方法
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
- 该方法第一个参数是指向Vue实例的this指针
- 该方法第二个参数是字符串"_data"
- 第三个参数是具体key值
- 在Vue内部,通过创建从target[key]到this[’_data’][key]的代理,让我们可以通过this访问到data方法的返回值。
关键点答疑
为什么在initData方法中,需要通过实参vm访问Vue实例中的内容,而在proxy方法中,可以直接通过this访问Vue实例中的内容?
首先,vm变量来自_init方法中,备份的指向Vue实例的this指针。
其次,根据当前调用栈_init->initState->initData->proxy,存在函数的嵌套调用,this指针在initData方法中已经指向window,所以需要使用备份值vm才能访问Vue实例的内容。
再次,在proxy方法中,虽然执行该方法时,该方法中的this也是指向window,但是我们的句柄中只是定义了get和set方法,两个方法并没有执行。
当我们在代码中真正使用this.xxx时候,get或set方法才会被调用。此时根this运行时绑定规则,get或set方法是指向Vue实例的。这也充分体现了JS中 this的运行时绑定的特点。
在实际开发中,data方法是否可以返回一个数组?
不可以。
在initData方法中,当通过getData方法获取了data方法的返回值后,是要通过isPlainObject方法进行检查的。
该方法实际是通过原型方法Object.prototype.toString.call(obj)检查对象类型,如果是数组,则Object.prototype.toString返回的是"[object Array]",所以isPlainObject返回为false。
Vue只会把数组当做一个空对象{}处理,因此data方法不能直接返回数组。
代码模拟
function Vue(optioanl){
if (!(this instanceof Vue)){
console.error('Vue is constructor and should be called with new keyword');
}
const vm = this;
const initData = function(optioanl) {
let data = vm._data = typeof optioanl.data === 'function'
? optioanl.data.call(this) : {};
if (!isPlainObject(data)){
data = {};
console.error('data function should return an object.');
}
var keys = Object.keys(data);
for(let key of keys){
proxy(vm, '_data', key);
}
};
const proxy = function(target, sourceKey, key){
const handler = {
get: function(){
return this[sourceKey][key];
},
set:function(val){
this[sourceKey][key] = val;
}
}
Object.defineProperty(target, key, handler);
} ;
const isPlainObject = function (obj){
return (Object.prototype.toString.call(obj) === "[object Object]");
}
initData(optioanl);
}
var v = new Vue({
data(){
return {
name: "ly",
age:21
}
}
});
代码执行后, 我们可以通过v.age和v.name 访问到data方法的返回值。
附录
getData方法
export function getData (data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
isPlainObject 方法
const _toString = Object.prototype.toString
export function isPlainObject (obj: any): boolean {
return _toString.call(obj) === '[object Object]'
}