【若川视野 x 源码共读】第23期 | 为什么 Vue2 this 能够直接获取到 data 和 methods
本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
示例
class Person {
constructor(options) {
let { data, methods } = options;
this.data = data;
this.methods = methods;
}
}
const p = new Person({
data: {
name: "女朋友",
},
methods: {
sayName() {
console.log("[ sayName ] >", "女朋友");
},
},
});
// 正确获取
console.log("[ this.data ] >", p.data.name);
console.log("[ this.methods ] >", p.methods.sayName);
// 那需要怎么做才能实现下面这种获取方式呢?
// console.log("[ this.data ] >", p.name);
// console.log("[ this.methods ] >", p.sayName());
搭建环境
新建 html
文件, 在<body></body>
中加上如下js
。
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
<script>
const vm = new Vue({
data: {
name: '女朋友',
},
methods: {
sayName(){
console.log(this.name);
}
},
});
console.log(vm.name);
console.log(vm.sayName());
</script>
启动本地服务
-
http-server
npm install http-server -g
全局安装http-server
到对应想启动服务文件夹启动
-
Five Server
vscode
搜索该插件- 对应文件,右键
Open With Five Server
调试:在
F12
打开调试,source
面板,在例子中const vm = new Vue({
打上断点。
打开页面之后,刷新页面 按 F11
即进入函数,这时断点就走进了 Vue
的构造函数
Vue
构造函数
function Vue (options) {
// !(this instanceof Vue) 判断是不是用了 new 关键词调用构造函数
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
// ...
// 初始化
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')
// ...
Vue
初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher
等等。
Vue
的初始化逻辑写的非常清楚,把不同的功能逻辑拆成一些单独的函数执行,让主线逻辑一目了然.
调试:继续在
this._init(options);
处打上断点,按F11
进入函数。
_init
初始化函数
进入 _init
函数后,这个函数比较长,做了挺多事情,我们猜测跟data
和methods
相关的实现在initState(vm)
函数里。
调试:接着我们在
initState(vm)
函数这里打算断点,按F8
可以直接跳转到这个断点,然后按F11
接着进入initState
函数。
initState
初始化状态
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
// 初始化 属性
if (opts.props) { initProps(vm, opts.props); }
// 有传入 methods,初始化方法
if (opts.methods) { initMethods(vm, opts.methods); }
// 有传入 data,初始化 data 检测数据
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
// 初始化 计算属性
if (opts.computed) { initComputed(vm, opts.computed); }
// 初始化 watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
调试:在
initMethods
这句打上断点,同时在initData(vm)
处打上断点,看完initMethods
函数后,可以直接按F8
回到initData(vm)
函数。 继续按F11
,先进入initMethods
函数。
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);
}
}
判断:
methods
每一项需要是函数,否则警告props
是否和methods
重名methods
中的每一项是不是已经在new Vue
实例vm
上存在,而且是方法名是保留的_ $
(在JS
中一般指内部变量标识)开头
通过this
直接访问到methods
里面的函数。
关键就在于这行代码vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
浏览器调式 : alt
放在函数名上,可出现提示,跳转到相对应位置
vscode
调式: ctrl
跳转到相应位置
这里查看 bind
这个函数
bind
返回一个函数,修改 this
指向
function polyfillBind (fn, ctx) {
function boundFn (a) {
var l = arguments.length;
return l
? l > 1
? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}
boundFn._length = fn.length;
return boundFn
}
function nativeBind (fn, ctx) {
return fn.bind(ctx)
}
var bind = Function.prototype.bind
? nativeBind
: polyfillBind;
简单来说就是兼容了老版本不支持 原生的bind函数。同时兼容写法,对参数多少做出了判断,使用call
和apply
实现,据说是因为性能问题。
apply
传递数组,需要遍历参数,所以单个参数的情况下call
性能好些 (大概吧,我也不确定 哈哈哈)
调试:看完了
initMethods
函数,按F8
回到上文提到的initData(vm)
函数断点处。
initData
初始化 data
function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
// 最后获取到的 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];
{
// methods 与 data 重复 警告
if (methods && hasOwn(methods, key)) {
warn(
("Method "" + key + "" has already been defined as a data property."),
vm
);
}
}
// props 与 data 重复 警告
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 */);
}
proxy 代理
其实就是用 Object.defineProperty
定义对象
这里用处是:this.xxx
则是访问的 this._data.xxx
。
function noop (a, b, c) {}
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);
}
总结
通过this
直接访问到methods
里面的函数的原因是:因为methods
里的方法通过 bind
指定了this
为 new Vue的实例(vm
)。
通过 this
直接访问到 data
里面的数据的原因是:data里的属性最终会存储到new Vue
的实例(vm
)上的 _data
对象中,访问 this.xxx
,是访问Object.defineProperty
代理后的 this._data.xxx
。