vue2源码分析-data、props、methods、computed属性可以重名吗
我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情
简介
在我们面试的过程中,可能经常会碰到这样一个面试题:
在vue中,我们定义在data、props、methods、computed中的属性可以重名吗?
听到这有些小伙伴可能有点茫然,别着急,今天笔者通过分析源码的方式带大家彻底弄懂这个问题。
本文vue版本为2.6.14
简单测试
在分析源码前,我们来做个简单测试
我们先创建一个组件,然后分别使用相同的属性定义在data、props、methods、computed
中,看看vue
会给我们提示什么。
<template>
<div>
<div>{{ message }}</div>
</div>
</template>
<script>
export default {
props: {
message: {
type: Number,
default: 123,
},
},
data() {
return {
message: "randy",
};
},
};
</script>
我们首先在props和data
中定义了一个相同的message
属性,我们启动下vue
服务看下会给我们提示什么?
可以看到,给我们的提示就是 message
属性已经在props
中定义了,不能再作为data
使用了。
接下来我们再定义一个message
方法
<template>
<div>
<div>{{ message }}</div>
</div>
</template>
<script>
export default {
data() {
return {
message: "randy",
};
},
methods: {
message() {
console.log("message");
}
}
};
</script>
可以看到,给我们的提示就是 message
属性已经在data
中定义了,不能再当做方法使用了。
接下来我们再定义一个message
计算属性
<template>
<div>
<div>{{ message }}</div>
</div>
</template>
<script>
export default {
data() {
return {
message: "randy",
};
},
computed: {
message() {
return 666;
},
},
};
</script>
可以看到,给我们的提示就是 message
属性已经在data
中定义了,不能再当做计算属性使用了。
通过这个简单测试就可以解答我们上面的问题了,data、props、methods、computed属性不可以重名
为什么呢?下面我们通过分析源码的方式,来看看为什么不可以
为了方便小伙伴们的理解,笔者将不重要的代码进行了删除,如果想看完整源码的可以点击此链接自行查看
源码分析
vue实例化
首先我们来看看我们new Vue()
进行实例化的时候发生了什么。
// src/core/instance/index.js
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);
}
通过源码我们可以看到,在实例化的时候有调用一个_init
方法,我们来看看这个方法
// src/core/instance/init.js
// ...
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) // 今天分析的重点
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// ...
这个方法里面做了很多初始化的工作,这里我们着重看initState(vm)
方法,这里才是我们今天的关键。
// src/core/instance/state.js
export function initState(vm: Component) {
vm._watchers = [];
const opts = vm.$options;
// 初始化props
if (opts.props) initProps(vm, opts.props);
// 初始化方法
if (opts.methods) initMethods(vm, opts.methods);
// 初始化data
if (opts.data) {
initData(vm);
} else {
observe((vm._data = {}), true /* asRootData */);
}
// 初始化computed
if (opts.computed) initComputed(vm, opts.computed);
// 初始化watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
在initState
方法中,分别对props、methods、data、computed、watch
做了初始化。下面我们将依次分析这几个初始化方法。
不过在分析之前我们先来看一个非常核心的proxy
方法,这里先混个脸熟,后面会多次用到。
这个方法也是我们能实现this
直接访问data、props
对象里面属性的关键所在。
proxy
// src/core/instance/state.js
// 定义了一个对象
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop,
};
// 代理方法 将target[key]的访问代理到target[sourceKey][key]上
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);
}
好了,下面我们进入具体的初始化方法进行分析
initProps
// src/core/instance/state.js
function initProps(vm: Component, propsOptions: Object) {
// ...
// 将props和vm.props赋值同一个对象
const props = (vm._props = {});
// ...
// 遍历我们的props对象得到每个key
for (const key in propsOptions) {
// ...
// 为 props 的每个 key 设置数据响应式
// 因为props和vm.props指向同一个对象,所以给props赋值属性的同时也是在给vm._props赋值
defineReactive(props, key, value);
// 如果vue实例上没有该属性,则进行代理
if (!(key in vm)) {
// 也就是我们this.xxx能直接访问到属性的原因,其实访问的是this._props.xxx
proxy(vm, `_props`, key);
}
}
// ...
}
详细分析:
- 将
props
和vm._props
赋初始值为{}
,即指向同一个对象。 - 遍历我们的
props
对象得到每个key
,并利用defineReactive
方法进行响应式,其次在该方法中对props
赋值,其实也就是对vm._props
赋值。(这里笔者没贴出defineReactive
代码,感兴趣的小伙伴可以自行查看) - 使用
in
进行判断,如果vue实例
上没有该属性,则进行代理。proxy
的核心就是将target[key]的访问代理到target[sourceKey][key]上
,也就是我们this.xxx实际上访问的是this._props.xxx
。(为什么this._props.xxx
能拿到值,因为在defineReactive
的时候对vm._props
赋了值)
initMethods
// src/core/instance/state.js
function initMethods(vm: Component, methods: Object) {
// 因为props先初始化,所以这里先拿到props
const props = vm.$options.props;
// 遍历methods,得到每个方法名
for (const key in methods) {
if (process.env.NODE_ENV !== "production") {
// ...
// 如果props已经有该key了,就报警告
if (props && hasOwn(props, key)) {
warn(`Method "${key}" has already been defined as a prop.`, vm);
}
// ...
}
}
// 判断有没有方法的实现,有的话就进行bind,这样我们使用this.xxx就能直接调用methods里面的方法了
vm[key] = methods[key] == null ? noop : bind(methods[key], vm);
}
}
bind
很重要,我们来看看bind
方法的实现
// 分析当前浏览器环境是否支持bind,支持就使用原生的bind方法,否则使用polyfillBind,也就是使用apply和call来模拟bind
export const bind = Function.prototype.bind
? nativeBind
: polyfillBind
// 支持的话,很简单,就是调用bind
function nativeBind (fn: Function, ctx: Object): Function {
return fn.bind(ctx)
}
// 模拟bind方法
function polyfillBind (fn: Function, ctx: Object): Function {
function boundFn (a) {
const l = arguments.length
return l
? l > 1
? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}
boundFn._length = fn.length
return boundFn
}
详细分析:
- 因为
props
先初始化,所以这里先拿到所有的props
,方便后面的key
比较。 - 遍历
methods
,得到每个方法名,然后判断该方法名是否已经在props
定义了,如果定义了就抛出警告。 - 使用
bind
方法,将原本需要this.methods.xxx
调用的方法能直接通过this.xxx
调用。(这里的bind做了兼容处理,会判断当前浏览器环境是否支持bind,支持就使用原生的bind方法,否则使用polyfillBind,也就是使用apply和call来模拟bind)
initData
// src/core/instance/state.js
function initData(vm: Component) {
// 拿到选项data
let data = vm.$options.data;
// 将data和vm._data指向同一引用
data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
// ...
// 拿到data所有的key
const keys = Object.keys(data);
// 因为props和methods先初始化,所有需要拿到props和methods方便后面比较是否重名
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") {
// 如果key已经在methods定义好了,就抛出警告
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
);
}
}
// 如果key已经在props定义好了,就抛出警告
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)) {
// 进行代理
// 也就是我们this.xxx能直接访问到属性的原因,其实访问的是this._data.xxx
proxy(vm, `_data`, key);
}
}
// ...
}
详细分析:
- 将
data
和vm._data
指向同一地址,即指向同一个对象。 - 拿到前面初始化好的
props和methods
,这是为了方便后面key
重名的比较。 - 遍历我们的
data
对象得到每个key
,分别在props和methods
进行判断,key
是否重复,重复的话抛出警告。 - 使用
proxy
进行代理,proxy
的核心就是将target[key]的访问代理到target[sourceKey][key]上
,也就是我们this.xxx实际上访问的是this._data.xxx
。
initComputed
// src/core/instance/state.js
function initComputed(vm: Component, computed: Object) {
// ...
// 遍历computed
for (const key in computed) {
// 拿到具体的值,可能是方法也可能是对象
const userDef = computed[key];
// 看是函数写法还是对象写法,函数写法直接将函数作为get方法,对象写法就取get方法
const getter = typeof userDef === "function" ? userDef : userDef.get;
// ...
// 如果vue实例上没有该属性就使用defineComputed进行定义
if (!(key in vm)) {
defineComputed(vm, key, userDef);
} else if (process.env.NODE_ENV !== "production") {
// 如果key已经在data定义好了,就抛出警告
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm);
// 如果key已经在props定义好了,就抛出警告
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm);
// 如果key已经在methods定义好了,就抛出警告
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(`The computed property "${key}" is already defined as a method.`, vm);
}
}
}
}
下面我们来看看defineComputed
方法
export function defineComputed(
target: any,
key: string,
userDef: Object | Function
) {
// ...
// 根据参数,定义get、set方法
if (typeof userDef === "function") {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef;
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop;
sharedPropertyDefinition.set = userDef.set ? userDef.set : noop;
}
// ...
// 在vue实例上直接定义属性,get、set方法就是我们在computed里面定义的
Object.defineProperty(target, key, sharedPropertyDefinition);
}
详细分析:
- 遍历
computed
,得到每个键值。 - 拿到
key
,然后判断是否在data、props、methods
中已经定义好,如果定义好了就抛出警告。 - 如果
vue
实例上没有该属性,就使用defineComputed
方法定义到vue
实例上。 - 定义使用的是
defineProperty
方法,将computed
里面的内容直接定义到了vue
实例上,对于get
和set
方法,使用的就是我们在computed
里面定义的。
总结
因为,data、props、methods、computed
最终都会挂载到vue
实例上,因此各自在初始化的时候都会有属性重名的判断,所以这四者属性名不能重复。
data
和props
首先会以私有属性_data、_props
的形式定义到vue
实例上,然后利用defineProperty
方法重写里面的get、set
方法,将vm.xxx
的访问代理到vm._data.xxx或者vm.props.xxx
上。这也就是我们能通过this
直接访问data和props
对象里属性的原因。
methods
主要借助bind
方法改变this
指向。将原本需要this.methods.xxx
调用的方法能直接通过this.xxx
调用。并且对bind
的方法做了兼容处理。
computed
也借助了defineProperty
方法,将computed
里面的内容直接定义到了vue
实例上,对于get
和set
方法,使用的就是我们在computed
里面定义的。
系列文章
vue2源码分析-data、props、methods、computed属性可以重名吗
后记
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!
转载自:https://juejin.cn/post/7139788012925222919