【Vue 原理】Vue 是如何代理 data、methods 和 props 的?
前言
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
的方式来进行属性代理。
线上博客
共同学习,写下你的评论
评论加载中...
作者其他优质文章