- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第23期,链接:juejin.cn/post/708298…
在写vue的时候,我们可以通过this直接拿到data和methods里面的内容,这在vue中是如何实现的呢?本篇文章通过源码解析来分析其实现方案。先说结论:
-
this直接获取data内属性的原因:首先会将data存放到Vue的实例vm上的
_data中,并利用Object.defineProperty方法,将我们访问this.xxx时,代理到this._data.xxx。 -
this直接获取methods内方法的原因:methods里的方法,因为使用了bind将函数的this指向了实例vm。所以我们可以直接用this访问到。
使用示例如下:
const vm = new Vue({
data: {
name: 'L_Look',
},
methods: {
say() {
console.log('======', this.name);
}
}
})
console.log(vm.name); // L_Look
console.log(vm.say()); // ====== L_Look
接下来,我们从源码来进行分析。
调试环境搭建
首先我们新建一个HTML文件,引入一个线上版的vue。
<body>
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
<script>
const vm = new Vue({
data: {
name: 'L_Look',
},
methods: {
say() {
console.log('======', this.name);
}
}
})
console.log(vm.name); // L_Look
console.log(vm.say()); // ====== L_Look
</script>
</body>
在vsCode中可以下载Live Server插件,用该插件起一个node服务,来调试我们的代码。
调试过程
在浏览器中打开devTools调试,进入sourcec面板,在const vm = new Vue({行打个断点。
进入vue构造函数
可以按下F11或点击上面调试操作面板红框的按钮,进入函数内部。
function Vue (options) {
if (!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
// 引入vue之后,做的一些初始化操作。
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);
这里可以看下在Vue构造函数中的第一个判断if (!(this instanceof Vue),是用来判断是否是用new关键字调用的构造函数。为什么要加一个这个判断呢?
我们接着看后面的this._init()这个方法,用的是this来调用的,在点进_init函数我们会看到这个方法是挂载在原型上的,这也就证明Vue是作为一个构造函数,必须用new来调用,这时this是指向构造函数的实例。如果用普通函数的方法调用,那这个this指向的就有不确定性了,根据其执行时的环境来判定。
this指向这里不做过多说明,可以查看其他相关文章
进入_init函数
这里我们只聚焦到,vue是如何处理data和methods的,其他方法暂时先放下。
function initMixin (Vue) {
Vue.prototype._init = function (options) {
var vm = this;
// 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');
};
}
这里我们可以依次点击一下_init这个函数里调用的方法,来大致浏览一下都是干什么用的。或者可以参考别人的源码分析,效率更高一些。这里我们发现是在initStat()这个方法里对data和methods做了处理。接下来我们看下这个方法。
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);
}
}
- 在
_init函数中已经把options参数挂载到vm实例上。 - 这个函数,主要就是处理options里面的数据,针对props、methods、data、computed、watch做处理。
本篇文章主要分析下methods方法和data方法。所以下面我们来看些initMethods方法和initData方法。
initMethods方法,初始化methods
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);
}
}
-
首先遍历methods里的方法,并判断是否符合预期
- 是否是函数,不是函数进行警告
- 是否与props冲突了,如果冲突进行警告
- 判断是否是Vue内置保留的方法名或已经存在在实例中。
-
除了上面的判断,就是将methods的方法,用bind绑定函数的this指向vue的实例vm。
这样也就解释了为什么this可以直接调用methods里的方法,因为使用了bind将函数的this指向了实例vm。
这里的bind函数是vue自己封装的一个,主要是为了兼容不兼容bind函数的运行环境。至于bind函数的作用,简单说就是返回一个新的函数,将bind()被调用时传入的第一个参数作为新函数的this对象,而其余参数作为新函数的参数,供调用时使用。 具体细节可以参考其他bind函数解析的文章。
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 */);
}
这个函数主要做了以下几个操作。
- 先判断data是不是一个函数,如果是函数调用getData获取返回的data数据源,然后将data赋值给_data。
- 判断取到的data是不是一个对象,不是对象的话,给警告。
- 对data进行遍历,对每一项进行判断,如果和methods冲突了,给警告,如果和props冲突了,给警告。并且最后判断是不是一个vue内部保留的方法或内部方法,不是的话使用proxy做一层代理,代理到_data上。
- 最后将data转换为响应式数据。
getData() 获取data对象
function getData (data, vm) {
// #7573 disable dep collection when invoking data getters
pushTarget();
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, "data()");
return {}
} finally {
popTarget();
}
}
这里需要注意,调用data函数时,要将this指向指向vue的实例,保证函数内this指向正确。
proxy 代理数据访问
这个方法就是vue2中的一个核心点,如何使用Object.defineProperty()来定义一个对象。
这里实现了访问this.xxx 就是访问this._data.xxx。
// proxy(vm, "_data", key);
function noop (a, b, c) {};
// 描述对象,描述对象的特性
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};
function proxy (target, sourceKey, key) {
// get描述符,当获取目标属性的值是,实质是获取_data对象上的属性值
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
// set描述符,同样设置值时,设置的也是_data对象的值。
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
学习下Object.defineProperty
会直接在第一个对象上定义一个新属性,或者修改一个对象的现有属性。
语法:Object.defineProperty(obj, prop, descriptor)。
参数说明:
- obj:目标对象
- prop: 需要定义或修改的属性名
- descriptor: 目标属性的属性描述符
属性描述符: 对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由getter函数和setter函数所描述的属性。
数据描述符:
- writable: 是否可以被重写,默认为false
- enumerable: 是否可以被枚举。默认false
- value: 值可以使任意类型的值,默认为undefined。
- configurable: 是否可以删除目标属性或是否可以再次修改属性的特性。默认false。
存取描述符:
当使用了getter或setter方法,则不被允许使用writable和value这两个属性。
举个例子:
let school = {};
let name = '小学';
Object.defineProperty(school, 'name', {
configurable: true,
enumerable: true,
get() {
return name
},
set(val) {
name = val
}
})
console.log(school.name ); // 小学
school.name = '中学';
console.log(name); // 中学
再看下源码的实现:
利用Object.defineProperty(),当我们使用this.xxx访问时,实际上是代理到this._data上面的。
这段源码中,出现了一些工具函数,比如hasOwn、noop、isReserved、bind等,可以参考我的另一篇文章 源码学习—VUE2中的那些工具函数
收获
- 源码的调试。调试vue2源码,踏出了第一步,看过很多的源码分析,但却是第一次主动去调试源码。
- 源码中代码功能的划分,可以看到在vue中,针对各个流程进行的函数划分,比如开始的
initMixin、stateMixin、eventsMixin、lifecycleMixin、renderMixin的这几个函数。还有_init方法中,initLifecycle、initEvents、initRender、initInjections、initStata、initProvide。这些在我们开发中,非常有借鉴意义。在代码的组织形式,结构设计时,针对性的模块划分,使代码的阅读性与扩展性都非常强。不要从头到尾一把梭。 - Object.defineProperty的复习与实践深化。其实这个方法在日常开发中很少用到,但通过对该方法深入学习,可以在日常开发中拓展自己的解决方案,满足特定场景,来简化操作逻辑。、
- 基础知识的复习。构造函数、this指向、call,apply,bind等方法的复习。
思考
- 以一个问题为切入点,去阅读源码,会更加有效率且容易聚焦,相当于有个目标,否则容易陷入到源码复杂的实现逻辑中,找不到重点。
- 看源码,看这些大牛的代码设计,处理某些问题的解决方案,然后真正的在日常开发中应用实践,能极大的提高我们的编程能力。写代码还是要带些洁癖的。
参考: