【若川视野 x 源码共读】第23期 | 为什么 Vue2 this 能够直接获取到 data 和 methods
本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
1. 前言
三大框架的源码对于大家应该是有些难度的,但是愚公尚可移山,铁杵也能磨成针,不就是慢慢摸鱼么。盘它
2. 源码解析
Vue中示例
const vue = new Vue({
data: {
name: 'curtis',
},
methods: {
getName(){
console.log(this.name);
}
},
});
console.log(vue.name); // curtis
console.log(vue.getName()); // curtis
复制代码
vue对象中可以直接获取内部data对象中的属性,而且method对象的方法可以直接通过this获取data对象中的属性值,但是对于JavaScript中的对象,这样的访问方式是不合理的。
const obj = new Object({
data: {
name: 'curtis',
},
method: {
getName(){
console.log(this.name);
}
},
});
console.log(obj.name); // undefined
console.log(obj.data.name); // curtis
console.log(obj.getName()); // Uncaught TypeError: obj.getName is not a function
console.log(obj.method.getName()); // undefined undefined
复制代码
环境准备
-
本地创建测试文件夹
examples,新建文件index.html,加入以下代码<!DOCTYPE html> <html> <header> <title>vue</title> </header> <body> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script> <script> const vue = new Vue({ data: { name: "curtis", }, methods: { getName() { console.log(this.name); }, }, }); console.log(vue.name); // curtis console.log(vue.getName()); // curtis </script> </body> </html> 复制代码 -
全局安装
http-server服务npm i -g http-server 复制代码 -
启动服务
cd examples http-server . 复制代码 -
启动完成后通过
http://localhost:8080/可以访问到index.html文件,之后可通过控制台上调试学习源码。
Vue构造函数
function Vue (options) {
if (!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
复制代码
- 通过
if (!(this instanceof Vue)){}可以判断是不是使用了new关键词调用构造函数。
_init
初始化函数
var uid$3 = 0;
function initMixin (Vue) {
Vue.prototype._init = function (options) {
var vm = this;
// a uid
// 为每个实例生成一个唯一uid
vm._uid = uid$3++;
var startTag, endTag;
// istanbul忽略覆盖率
/* 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
// 判断是否传入初始对象以传入的初始对象中是否包含_isComponent属性,即是否为组件,在本次调用中不会走到这儿
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); // 初始化render
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);
}
};
}
复制代码
resolveConstructorOptions()解析构造函数上的options属性,主要是为$options添加一些属性,这里不展开说,详细可参看人人都能懂的Vue源码系列(三)—resolveConstructorOptions函数-上initLifecycle(vm)初始化生命周期,大致看了下,内部主要是对vm对象的一些生命周期相关私有属性赋值,应该不会改变对象的属性的访问关系initEvents(vm)初始化事件监听,添加一些事件相关私有属性赋值initRender(vm)初始化render,仍然是添加属性callHook具体做了什么事情暂时还不太清楚,但也不像会改变对象的属性访问关系initInjections(vm)初始化注入器,具体作用还未知initState(vm)看到函数找那个使用了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);
}
}
复制代码
var opts = vm.$options,opts.method依旧是getName()方法,即创建对象时传入的method;但是data的值在vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm)时变为了mergedInstanceDataFn方法,暂时的知识盲区……- 之后就是最为重要的两个函数,也是全文的重点
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遍历method对象中的所有方法,在本次调用中只有getName()一个方法,if (typeof methods[key] !== "function")判断是否为方法,不是则报错if (props && hasOwn(props, key))判断props中是否包含该属性,有则警告;hasOwn()定义在vue的utils中,用于判断对象中是否包含某个属性if (key in vm && isReserved(key))判断key是vm实例的保留属性,是则警告- 以上判断条件都通过的话,将
methods属性中的方法赋值给vm,但是在赋值时首相判断属性是否为方法,不是则赋值为noop(function noop (a, b, c) {}全局定义的空函数)函数(这里不太明白之前已经判断过是否为函数了,为什么还要判断一次),函数的话则进行赋值,但在赋值时使用了bind()将函数的this绑定到了vm,因而函数中的this.data就相当于vm.data;
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方法获取数据 -
getData()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.call(vm, vm)是如何获取到传入的data的值的,所以重新看了下之前初始化时mergeOptions()的流程,虽然还是没太明白用意,但大致是清楚了data的函数是从哪里传来的,在源码1237行,最终data变成了函数mergedInstanceDataFn() -
mergedInstanceDataFn()return function mergedInstanceDataFn () { // instance merge var instanceData = typeof childVal === 'function' ? childVal.call(vm, vm) : childVal; var defaultData = typeof parentVal === 'function' ? parentVal.call(vm, vm) : parentVal; if (instanceData) { return mergeData(instanceData, defaultData) } else { return defaultData } } 复制代码- 其中
childVal即为实例化Vue时传入的data值
- 其中
-
因而,在
getData()函数调用data.call时直接就可获取到childVal的值,既是传入的data值。
-
-
如果
data不是纯粹的对象,返回警告;isPlainObject()在之前工具函数中看到过判断其类型为Object -
获取
data的所有属性,遍历,判断属性是否存在method或vm实例自身属性中,是则警告; -
使用
isReserved()判断是否无保留属性/** * Check if a string starts with $ or _ */ function isReserved (str) { var c = (str + '').charCodeAt(0); return c === 0x24 || c === 0x5F } 复制代码 -
使用
proxy(vm, "_data", key)进行访问代理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); } 复制代码首先看函数的最后一行
Object.defineProperty(target, key, sharedPropertyDefinition),为vm实例定义一个属性,属性名为key(本次调用值为'name')然后定义一系列属性描述符,其中最为重要的就是get和set;然后可以看到将get定义为return this[sourceKey][key],将set定义为this[sourceKey][key] = val;其中传入的sourceKey值为_data;因此,在访问vm.name时会被代理到vm._data.name,设置vm.name是同样会被代理访问到vm._data.name,而vm._data.name中保存的即为创建实例是传入的data关于
Object.defineProperty()的更多说明可参看MDN
自己尝试简单复刻
// 属性描述符定义
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: ()=>{},
set: ()=>{},
};
// 访问代理的方法,这里我是直接用的Vue中原本的方法
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);
}
// 声明构造函数
function MyVue(options) {
this._init(options);
}
MyVue.prototype._init = function (options) {
var vm = this;
vm["_data"] = options.data;
initMethods(vm, options.methods);
initData(vm, options.data);
};
// 初始化Methods的方法,简化版
function initMethods(vm, methods) {
for (const key in methods) {
vm[key] = methods[key].bind(vm);
}
}
// 初始化Data的方法,简化版
function initData(vm, data) {
var keys = Object.keys(data);
var i = keys.length;
while (i--) {
let key = keys[i];
proxy(vm, "_data", key);
}
}
// 创建实例测试测试
let mv = new MyVue({
data: {
name: "xinxinzi",
job: "coder",
},
methods: {
getName() {
console.log(this.name);
},
getJob() {
console.log(this.job);
},
},
});
// 成功输出
console.log(mv.name); // xinxinzi
console.log(mv.job); // coder
console.log(mv.getName()); // xinxinzi
console.log(mv.getJob()); // coder
复制代码
3. 总结
通过这期源码阅读活动又学习巩固了很多知识,比如bind绑定函数作用域,defineProperty定义对象属性等等;也有自己写一个mvvm框架的自信。