Vue2中为什么this能获取methods和data?解析methods和data的初始化
本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是源码共读的第23期 | 为什么 Vue2 this 能够直接获取到 data 和 methods
前言
为什么Vue2中的methods中 能直接使用this去 获得data中的属性, 当我们new一个Vue的时候 实际干了什么? 阅读本文你将了解其中的原理!
阅读准备
本地跑一个Server,通过CDN引入Vue生产文件,进行调试
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
<script>
const Harexs = new Vue({
data:{
name: 'Harexs'
},
methods: {
sayName() {
console.log(this.name)
}
}
})
Harexs.sayName()
</script>
</body>
</html>
可以使用http-server, npm i http-server -g
本地运行 http-server -c-1
调试

在入口处打下断点,如图中所示 15行处断下一个断点然后重新运行页面
快捷键
F8继续运行代码直至遇到下一个断点或代码结束F9单步调试,遇到函数会进入到函数内部继续执行F10单步调试,但是会跳过函数的内部执行,代码继续往下走F11进入下一个函数的调用shift + F11跳出当前函数的执行
入口
在刚刚断点的位置,我们按下F11 进行初始化函数的调用(5087-5093行)
function Vue (options) {
if (!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
instanceof运算符用于检测构造函数的Prototype属性是否出现在实例对象的原型链上,也就是判断这个对象的__proto__的查找最后是否能找到这个构造函数的Prottoype,即这个对象是不是这个函数New出来的
instanceof
剖一个代码例子
function Harexs(){}
let haxs = {}
haxs.__proto__ = Harexs.prototype
console.log(haxs instaceof Harexs) //true
如果你了解过new关键字的原理,它的实现过程 其实也是将创造的实例对象的__proto__指向了被new函数的Prototype
初始化
接下来,我们F9 单步下去,进入_init 初始化函数
Vue.prototype._init = function (options) {
var vm = this;
// a uid
vm._uid = uid$3++;
var startTag, endTag;
/* istanbul ignore if */
if (config.performance && mark) {
startTag = "vue-perf-start:" + (vm._uid);
endTag = "vue-perf-end:" + (vm._uid);
mark(startTag);
}
// a flag to avoid this being observed
vm._isVue = true;
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
/* istanbul ignore else */
{
initProxy(vm);
}
// expose real self
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');
/* istanbul ignore if */
if (config.performance && mark) {
vm._name = formatComponentName(vm, false);
mark(endTag);
measure(("vue " + (vm._name) + " init"), startTag, endTag);
}
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
这里是和初始化相关调用,我们F10单步往下走,到initState的位置,按下F11进入initState函数内部,它和我们的props methods data watch computed初始化有关
function initState (vm) {
vm._watchers = [];
var 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);
}
}
我们重点关注 initMethods 以及 initData, 先单步进入initMethods, 它比initData更早
initMethods
function initMethods (vm, methods) {
var props = vm.$options.props;
for (var key in methods) {
{
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);
}
}
函数内部主要是一个For...in的遍历, 它对三种特殊情况做了处理
- 先判断传进来的对象下的methods,即每一个key对应的value值的类型是否都是
function - 判断每个key是否已经出现在了props中,这里的hasOwn是
Object.prototype.hasOwnProperty(), 判断某个属性是否存在这个对象下 - 判断每个key是否已经在
vm下存在过,vm是我们的this, 即被实例化的对象,并且key和内部预留的关键字不起冲突,key开头不包含_和$字符
最后,在vm下,挂载一个相同的key的函数, 并且这个函数 是通过 bind方法返回的一个高阶函数,它的this已经指向了vm, 机智的小伙伴在这已经猜到为啥 methods 内部的this可以直接获取到 data了
到了这一步,此时vm下, 即被实例出来的Harexs对象,就有了一个sayName属性
initData
我们按下shift+F11跳出initMethods函数,接着F9单步到 initData函数F11进入函数内部
function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
if (!isPlainObject(data)) {
data = {};
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
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
{
if (methods && hasOwn(methods, key)) {
warn(
("Method \"" + key + "\" has already been defined as a data property."),
vm
);
}
}
if (props && hasOwn(props, key)) {
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 */);
}
函数内部先判断传进来的data对象是不是一个function, 主要和SFC组件有关,防止对象的拷贝,并且会判断处理后的data是不是一个纯对象, 我们接着看下面的主要逻辑
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
{
if (methods && hasOwn(methods, key)) {
warn(
("Method \"" + key + "\" has already been defined as a data property."),
vm
);
}
}
if (props && hasOwn(props, key)) {
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);
}
}
遍历data中的每个key 是否和 methods 以及 props 出现 同名的 key, 最后再判断是否有使用预留的关键字, 最后进入proxy 函数
proxy函数
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
sharedPropertyDefinition 是一个包含了存储描述符的对象, 用来给 Object.defineProperty 定义对象描述使用
proxy函数内部 先将 sharedPropertyDefinition的 get set 分别赋值,返回 this[souceKey][key],代入对象也就是this['_data']['name'], _data对应的就是我们传入的data属性
最后最关键的也就是对象的定义,也就是Object.defineProperty(vm,'name',sharedPropertyDefinition), 这里就类似methods那边的操作,给vm下同时挂载了这个属性,此时就可以直接使用this.name 来得到data中的这个属性
我们代入对象来看这个代码就清晰明了了
Object.defineProperty(vm, 'name', {
enumerable: true,
configurable: true,
get:function proxyGetter () {
return this['_data']['name']
},
set:function proxySetter (val) {
this['_data']['name'] = val;
}
});
至此methods 和data的初始化就结束了,我们简单概述下:
methods的初始化 会先判断 props 是否有同名key,再看本身是否是一个function,以及是否使用了预留关键字, 最后通过bind方法返回一个 指向了this的相同函数 挂载在 this下
data会创建一个_data副本属性, 存储以及读取是通过_data操作,先判断props以及methods是否存在同名的key, 最后将每个key挂载在 this下, 它们通过Object.defineProperty定义了get和set
实现
接下来我们实现一个 简化版的 methods 和 data
//空函数
const noop = () => { }
//对象定义
let ObjectDefine = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};
//对象定义
function proxy(target, soucekey, key) {
ObjectDefine.get = function proxyGetter() {
return target[soucekey][key]
}
ObjectDefine.set = function proxyGetter(val) {
target[soucekey][key] = val
}
Object.defineProperty(target, key, ObjectDefine)
}
function initMethods(vm, methods) {
let props = vm.$options.props
//遍历methods里的每个function
for (let key in methods) {
//是否是一个方法
if (typeof methods[key] !== 'function') {
throw new TypeError(`Method ${key} must be function`)
}
//是否已经在props存在
if (props && props.hasOwnProperty(key)) {
throw new TypeError(`Method ${key} has already been defined as a prop`)
}
//是否存在this上 并且使用了预留关键字
if ((key in vm) && (key.startsWith('$') || key.startsWith('_'))) {
throw new TypeError(`Method ${key} conflicts with an existing Vue instance method.
Avoid defining component methods that start with _ or $.`)
}
//挂载在this上
vm[key] = typeof methods[key] != 'function' ? noop : methods[key].bind(vm)
}
}
function initData(vm) {
vm._data = vm.$options.data
//得到data的key数组 以及 props和methods
let keys = Object.keys(vm.$options.data)
let props = vm.$options.props
let methods = vm.$options.methods
let i = keys.length
//循环
while (i--) {
let key = keys[i]
if (methods && methods.hasOwnProperty(key)) {
throw new TypeError(`data ${key} has already been defined as a methods`)
}
if (props && props.hasOwnProperty(key)) {
throw new TypeError(`data ${key} has already been defined as a props`)
}
if (key.startsWith('$') || key.startsWith('_')) {
throw new TypeError(`data ${key} conflicts with an existing Vue instance data.
Avoid defining component methods that start with _ or $.`)
}
proxy(vm, '_data', key)
}
}
function Harexs(options) {
let vm = this
//将传入的对象挂载在 this下的$options
vm.$options = options
let methods = vm.$options.methods
//初始化methods
initMethods(vm, methods)
//初始化data
initData(vm)
}
const haxs = new Harexs({
props: {
test: "haha"
},
data: {
name: 'Harexs'
},
methods: {
sayName() {
console.log(this.name)
}
}
})
//Harexs
haxs.sayName()
总结
- instanceof 运算符的 原理以及作用
- Vue2中 methods 以及 data 初始化流程
- hasOwnProperty 以及 startsWith 方法的使用
- bind 在 methods 中的 绑定作用,防止this丢失
转载自:https://juejin.cn/post/7130459967772950535