本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是源码共读的第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丢失