在 Vue.js 中,props 和 data 是组件的两个重要概念。props 是父组件向子组件传递数据的方式,而 data 是组件自身的状态数据。这两者都会被代理到 Vue 实例上,也就是说我们可以通过 this 访问到它们。那么,这个代理是如何实现的呢?
Vue.js 使用了 ES5 提供的 Object.defineProperty 方法来实现数据的响应式绑定。这个方法可以让我们在读取或者修改一个对象的属性时,执行一些额外的操作。Vue.js 利用这个方法在读取或者修改 props 和 data 时,进行依赖收集和派发更新。
当我们在组件中访问 this.message 时,实际上是在访问 this._data.message 或者 this._props.message。Vue.js 在初始化实例时,会遍历 data 和 props 对象,使用 Object.defineProperty 为每一个属性设置 getter 和 setter,然后将这些属性代理到 Vue 实例上。
当我们访问 this.message 时,实际上触发了 getter,返回了 this._data.message 或者 this._props.message 的值。当我们修改 this.message 时,实际上触发了 setter,修改了 this._data.message 或者 this._props.message 的值,并且触发视图的更新。
这就是 Vue.js 如何将 props 和 data 上的属性代理到 vm 实例上的基本原理。
在 Vue.js 的源码中,props 和 data 的代理实现是在初始化实例时进行的。以下是一个简化的示例,展示了如何使用 Object.defineProperty 来实现这个代理:
function Vue (options) {
this._data = options.data;
this._props = options.props;
var self = this;
// 遍历 data 对象
Object.keys(this._data).forEach(function(key) {
self.proxyData(key);
});
// 遍历 props 对象
Object.keys(this._props).forEach(function(key) {
self.proxyProps(key);
});
}
Vue.prototype.proxyData = function(key) {
var self = this;
Object.defineProperty(self, key, {
enumerable: true,
configurable: true,
get: function proxyGetter() {
return self._data[key];
},
set: function proxySetter(newVal) {
self._data[key] = newVal;
}
});
};
Vue.prototype.proxyProps = function(key) {
var self = this;
Object.defineProperty(self, key, {
enumerable: true,
configurable: true,
get: function proxyGetter() {
return self._props[key];
},
set: function proxySetter(newVal) {
console.warn('You are trying to modify a read only value');
}
});
};
在这个示例中,我们在 Vue 的构造函数中遍历 data 和 props 对象,然后使用 Object.defineProperty 为每一个属性设置 getter 和 setter。当我们访问 this.message 时,实际上触发了 getter,返回了 this._data.message 或者 this._props.message 的值。当我们修改 this.message 时,实际上触发了 setter,修改了 this._data.message 的值,并且触发视图的更新。对于 props,由于它是只读的,所以我们在 setter 中打印了一个警告消息。
源码中 Vue 的初始化阶段,_init 方法执行的时候,会执行 initState(vm) 方法,方法主要是对 props、methods、data、computed 和 wathcer 等属性做了初始化操作。
data 的初始化主要过程也是做两件事,一个是对定义 data 函数返回对象的遍历,通过 proxy 把每一个值 vm._data.xxx 都代理到 vm.xxx 上;另一个是调用 observe 方法观测整个 data 的变化。
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 */)
}