前言
Vue 有一个非常有趣的功能,就是我们所有传进去的 data
、methods
或者 props
,都会挂载到 Vue 实例上, 我们可以通过 this.xxx
的简单做法来进行访问。那么,这到底是怎么实现的呢?
源码实现
首先可以来看看源码部分。相关的源码实现就在 src\core\instance\state.js
文件下。
methods
由于 methods
和data
还有 props
的实现不一致,因此这里简单拉出来单独讲。
首先把目光聚焦在 initMethods
上。
function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
if (process.env.NODE_ENV !== 'production') {
if (typeof methods[key] !== 'function') {
warn(
`Method "${key}" has type "${typeof methods[key]}" 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] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
这里主要执行了这么几步骤:
- 拿到
vm.$options
上的props
。 - 遍历
methods
上的属性, 分别判断是否是函数、是否props
上已经有了相同属性名、是否和现有Vue
实例方法冲突。 - 最后,直接赋值给
vm[key]
上进行代理,并且通过bind
方法 绑定this
。
这里主要是最后一个步骤,使得我们能够直接在 Vue
实例上访问 methods
中的方法。
data 和 props
从 data 初始化开始
本章节从 data 的初始化上入手,相关方法为 initData
。
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 */)
}
简单的看一下,initData 主要执行了:
- 获取
data
。 - 把
data
赋值到vm._data
上。 - 遍历
data
中的key
,判断是否在methods
和props
中存在同样的key
, 存在则报个warnning
。 - 通过
proxy
函数代理key
。
其中,步骤三是因为 methods 和 props 的属性最终也可以直接通过 Vue 实例进行访问,因此我们需要确保 key 的唯一性。
而步骤四才是本章的核心函数,它实现了 vm.xxx
对vm._data.xxx
的访问。当然 props
也是同理。
proxy 的庐山真面目
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
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)
}
可以清楚的看到,首先定义了一个 属性描述符 sharedPropertyDefinition
,get
和 set
初始指定为一个空函数。而proxy 方法 接受三个参数,分别是:
- target:目标代理对象。
- sourceKey:代理对象的数据源所对应的key,如果代理的是
_data
,就传入"_data"
。 - key:数据源的
key
。
在 proxy
中,sharedPropertyDefinition
的 get
和 set
方法会被重写,并通过 Object.defineProperty(target, key, sharedPropertyDefinition)
来进行属性的代理。
那么结合到代码中的实现,在 initData
阶段,我们执行了 proxy(vm, '_data', key)
,那么,对于这里而言,target
事实上就是 vm
, 而 key
则是 vm._data
中的属性 key
。通过重写后的 get
和 set
方法不难看出,假设 a
是 data
中的数据,那么当我们访问 vm.a
的时候,实际上访问的是 vm._data.a
;而当我们赋值 vm.a = 1
的时候,实际上会代理到 vm._data.a = 1
上去。
至此,proxy
已经真相了,而 props
也同样通过 proxy
来代理属性的访问。
proxy(vm, `_props`, key)
这里我们不主张直接通过 vm._data.xxx
的方式来进行操作,一方面下划线从规范来讲属于私有属性,是不允许被直接访问的;另一方面, 直接访问 _data
可能会造成一些不可预见的 bug,比方说新增属性不会经过响应式处理。
自己实现个乞丐版
学过一个东西,肯定要自己造个轮子来简单验证一下。直接上代码:
// vue 如何通过 this.xxx 访问 this._data.xxx
const noop = () => {}
const sharedPropertypeDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
function proxy (target, sourceKey, key) {
sharedPropertypeDefinition.get = function proxyGet () {
return target[sourceKey][key]
}
sharedPropertypeDefinition.set = function proxySet (val) {
target[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertypeDefinition)
}
function Vue (data) {
this._data = data
Object.keys(this._data).forEach(key => {
proxy(this, '_data', key)
})
}
const vueIns = new Vue({a: 1, b: 2})
console.log(vueIns.a)
console.log(vueIns.b)
vueIns.a = 111
vueIns.b = 222
console.log(vueIns.a)
console.log(vueIns.b)
总结
- Vue 会代理
data
、methods
和props
上的属性。我们可以直接通过this.key
的方式来进行访问和操作。 - Vue 会 确保
data
、methods
和props
上的属性具有唯一性。 - proxy 通过
Object.defineProperty
的方式来进行属性代理。