【若川视野 x 源码共读】第23期 | 为什么 Vue2 this 能够直接获取到 data 和 methods
前言
本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
开胃菜
在原因之前,我们先来回顾一下Vue2的简单使用吧。console
打印结果是什么?
const vm = new Vue({
data: {
msg: "Hello Vue";
},
methods: {
getMsg() {
console.log(this.msg);
}
}
});
console.log(vm.msg); // Hello Vue
console.log(vm.getMsg()); // Hello Vue
很简单,通过vm
可以直接访问到msg
和getMsg
,并且getMsg
里可以直接读取到上下文this
指向的当前的实例。那么Vue是如何实现的呢?通过函数方法获取this
指向,我们可以通过函数的call
、apply
以及bind
来实现,那么Vue会不会也是这样实现的呢?
我们通常写的类函数,如何做到Vue
的效果呢?
function MyVue(options) {
}
const vm = new MyVue({
data: {
msg: "Hello MyVue"
},
methods: {
getMsg() {
console.log(this.msg);
}
}
})
console.log(vm.name); // undefined
console.log(vm.getMsg()); // Uncaught TypeError: vm.getMsg is not a function
接下来,我们一起来探索下Vue是如何实现的吧!
源码剖析
在剖析源码之前,我们先在本地新建一个index.html
文件,在body
标签内加上如下代码
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
<script>
const vm = new Vue({
data: {
msg: 'Hello Vue',
},
methods: {
getMsg(){
console.log(this.msg);
}
}
});
console.log(vm.msg);
console.log(vm.getMsg());
</script>
在浏览器运行之后,我们点开Sources
,在const vm = new Vue({
所在行打上断点,刷新之后按F11
进入Vue
构造函数。
Vue构造函数
function Vue (options) {
if (!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
// 初始化
initMixin(Vue); // 后续我们会看到这个方法
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);
乍一看,短短几行代码就实现了Vue构造函数,很简单吧。
首先通过this instanceof Vue
检验是否使用new
关键词。
然后再通过_init
方法,完善options
里的所有配置项,那么_init
方法是打哪儿来的?没有明显的声明地方,那我们可以猜想是不是在Vue构造函数的原型上property
?
我们在this._init(options);
所在行再打上断点,按F11
进入_init
函数;
_init 初始化函数
我们先看一下_init函数里都做了哪些
function initMixin (Vue) {
Vue.prototype._init = function (options) {
var vm = this; // vm就是我们new出来的实例对象!!!
// 省略部分代码
// 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');
// 省略部分代码
};
}
我们在initMixin
里看到很重要的一行代码Vue.prototype._init
,跟我们之前的猜测是一样的,所以,也体现出对于js底层基本功的重要性了。
在这个方法里,我们着重看下initState(vm)
方法,看看它会不会给我们带来惊喜。
initState 初始化状态
通过打断点,刷新后我们看下
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);
}
}
这个方法里,主要是做了options
里其他配置项的初始化,包括props
、methods
、data
、computed
、watch
,留意下这个顺序!!!
顺着这个顺序我们先来看下initMethods
方法吧
initMethods 初始化方法
function initMethods (vm, methods) {
var props = vm.$options.props;
for (var key in methods) {
// 省略部分代码,此处做了一些校验
// 判断 methods 中的每一项是不是函数,如果不是警告。
// 判断 methods 中的每一项是不是和 props 冲突了,如果是,警告。
// 判断 methods 中的每一项是不是已经在 new Vue实例 vm 上存在,而且是方法名是保留的 _ $ (在JS中一般指内部变量标识)开头,如果是警告。
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
}
}
在这个方法里,我们看到两个熟悉的方法noop
和bind
方法,这两个方法我们在Vue2工具函数这一期里说到过,不了解的可以看一下。
这个方法里,通过遍历methods
里每一个方法,将它直接绑定到vm
(即new出来的实例)上,看到这儿有没有豁然开朗,原来能直接读取到method
是这么实现的,那其余能够直接读取到的,是不是也是这么实现的呢?带着这个猜想我们继续来看看initData
方法吧
initData 初始化data
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 */);
}
先来简单说下这个方法都干了啥
- 判断
options
里的data
是不是一个函数,如果不是函数,直接使用,默认为空对象;如果是一个函数,那需要先通过执行getData
来拿到返回的结果; - 判断最终的
data
是不是一个普通对象,如果不是则提示警告; - 判断
data
里的每个key
是否已经在props
和methods
里声明过,如果已存在,则提示警告,这个主要与代码执行顺序有关了。 - 判断每个
key
是不是内部私有的保留属性,详情请看Vue里关于data的介绍 - 最后做一层代理,将数据代理到
_data
上。
这里也验证了我们之前的猜想,也是去遍历data
里的每一项,将其绑定至vm
上,此处Vue
又引申出来两个方法getData
和proxy
,我们简单看下吧。
getData 获取data函数返回结果
function getData (data, vm) {
pushTarget();
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, "data()");
return {}
} finally {
popTarget();
}
}
这个方法里执行了data
函数,获取到其返回值。
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);
}
此处呢,我们可以看到,Vue
是通过Object.defineProperty
(非常重要的一个API)来代理data
里的每一项数据,也就是说,我们在使用vm.xx
的时候,是通过vm._data.xx
来读取的。
简化版的Vue
在这里,我们不考虑其他异常因素,简单实现一下
function MyVue(options) {
if(!(this instanceof MyVue)) {
throw Error('需要通过new使用');
}
const vm = this;
vm.$options = options;
if(options.data) {
initData(vm)
}
if(options.methods) {
initMethods(vm)
}
}
function initData(vm) {
let data = vm.$options.data;
data = typeof data === 'function' ? data.call(vm, vm) : (data || {});
const keys = Object.keys(data);
keys.forEach(key => {
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get() { return data[key]; },
set(val) { data[key] = val; }
})
})
}
function initMethods(vm) {
const methods = vm.$options.methods;
const keys = Object.keys(methods);
keys.forEach(key => {
vm[key] = methods[key].bind(vm);
})
}
var vm = new MyVue({
data() {
return {
msg: "Hello MyVue"
}
},
methods: {
getMsg() {
console.log(this.msg)
}
}
})
console.log(vm.msg) // Hello MyVue
console.log(vm.getMsg()) // Hello MyVue
总结
为什么 Vue2 this 能够直接获取到 data 和 methods?
是因为Vue
遍历了methods
和data
里的每一项,将其绑定到new出来的实例vm
上。
通过bind
指定了函数里this
为vm
;
通过Object.defineProperty
,将data
里的每一项数据代理至_data
对象上,访问 this.xxx
,就是访问this._data.xxx
。
转载自:https://juejin.cn/post/7171469822339842061