本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
这是源码共读的第23期,链接:【若川视野 x 源码共读】第23期 | 为什么 Vue2 this 能够直接获取到 data 和 methods。
1. 主题:
为什么vue2 this能够直接获取到data和methods?
源码解读原文:为什么 Vue2 this 能够直接获取到 data 和 methods ? 源码揭秘!
2. 源码解读补充:
如有不足或错误之处,欢迎各位大佬指正!
2.1 Vue 构造函数
js
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);
this._init(options);
这一行指的是,调用实例的 _init
方法,实例上没有定义,沿着原型链,找到实例的构造函数 Vue
上的 _init
方法。
2.2 _init 初始化函数
js
// 代码有删减
function initMixin (Vue) {
Vue.prototype._init = function (options) {
var vm = this;
// ......
// 初始化状态
initState(vm);
// ......
};
}
原型链和原型: 每一个对象,都有一个 __proto__
属性,它指向该对象的构造函数的 prototype
对象,这个 prototype
就是该对象的原型对象。而这个层层嵌套的链式关系,就是原型链。调用该对象的方法或属性时,在自身属性上找不到就会沿着原型链查找。
在 initMixin(Vue)
这个方法里,给构造函数 Vue
的 prototype
属性添加了 _init
方法。
2.3 initState 初始化状态
js
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); }
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
初始化顺序:props → methods → data → computed → watch。
2.4 initMethods 初始化方法
js
function initMethods (vm, methods) {
var props = vm.$options.props;
for (var key in methods) {
{
// 这里是对methods中方法的一些判断,判断是否是函数、命名是否与props中冲突、命名是否是保留字段
}
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
}
}
这里对methods中定义的合法的方法遍历,并给 vm
实例添加同名方法,赋值为绑定 this
指向到 vm
实例本身的原方法 。如比便可以直接通过 this.xxx()
调用methods中的方法了。
关键在于 bind(methods[key], vm)
。
2.5 initData 初始化 data
js
function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
if (!isPlainObject(data)) { // 这里校验data是否返回一个对象
// ......
}
// 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];
{
// 这里校验data和methods中的同名变量
}
if (props && hasOwn(props, key)) { // 这里校验data和props中的同名变量
// ......
} else if (!isReserved(key)) {
proxy(vm, "_data", key); // 合法的、不是内部私有的保留属性,做一层代理,代理到 _data 上。
}
}
// observe data
observe(data, true /* asRootData */); // 这里监听data,data中的数据转换为响应式数据(稍微看了下感觉有点复杂,没深入看)
}
关键在 proxy(vm, "_data", key)
这行。
proxy
方法利用 Object.defineProperty
定义对象,实现访问属性的转发 ,this.xxx
实际上是访问的 this._data.xxx
。
js
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);
}
proxy
中我对 sharedPropertyDefinition
只全局(相对全局)定义了一次,而不是在 proxy
中每次调用每次生成有疑惑,思考加询问豆包之后得出以下结论:
-
利用了闭包的特点:
-
当调用
proxy
方法时,key
作为参数传入。在sharedPropertyDefinition
的get
和set
函数内部,key
被引用。由于 JavaScript 的闭包特性,get
和set
函数会捕获并记住当前的key
值。proxy(vm, "_data", "name")
实际上会将sharedPropertyDefinition
的get
和set
函数修改为:jsfunction proxyGetter () { return this["_data"]["name"]; } function proxySetter (val) { this["_data"]["name"] = val; }
-
-
Object.defineProperty
会将这些修改后的get
和set
函数应用到target
对象的key
属性上。target
对象的key
属性会被赋予sharedPropertyDefinition
中的get
和set
函数的副本。一旦属性被定义,这些get
和set
函数就与sharedPropertyDefinition
的get
和set
解绑了。 -
这么做的目的,应该是出于性能考虑:
在 JavaScript 里,对象的创建和初始化操作会带来一定的开销,这包含内存分配和属性初始化。要是在
proxy
方法每次调用时都创建一个新的sharedPropertyDefinition
对象,就会有多次对象创建和属性初始化操作,这会对性能产生影响。而全局定义一次
sharedPropertyDefinition
对象,每次调用proxy
方法时仅仅修改这个对象的get
和set
属性,这样就避免了多次对象创建和属性初始化操作,降低了性能开销。
3. 总结:
手写实现 关键:
- methods:
- 对methods中定义的方法,是否与props中有重名的校验;
- 通过校验的方法,使用自定义bind方法将methods中的方法逐一对vm实例赋值同名函数并绑定this指向为实例;
- data:
- 对data类型的校验;
- 对data中定义的变量,是否与methods、props中有重名变量的校验;
- 通过校验的变量,使用自定义的proxy方法将data中的变量逐一对vm实例赋值同名变量并实现访问和修改的代理方法;
- proxy方法,本质上是通过Object.defineProperty方法实现,也即响应式原理实现关键。
4. 手写实现
开始默写了!(不考虑检验,只实现核心功能)
js
// 构造函数 MyVue
class MyVue {
constructor(options) {
this._init(options)
}
_init(options) {
// methods 实现
const methods = options.methods || {}
if (Object.prototype.toString.call(methods) === '[object Object]') {
const methodsKeys = Object.keys(methods)
if (methodsKeys.length > 0) {
for(const key of methodsKeys) {
this[key] = methods[key].bind(this)
}
}
}
// data 实现
const data = options.data() || {}
if (Object.prototype.toString.call(data) === '[object Object]') {
const dataKeys = Object.keys(data)
if (dataKeys.length > 0) {
for(const key of dataKeys) {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key]
},
set(val) {
data[key] = val
return val
}
})
}
}
}
}
}
// 实例调用
const myVm = new MyVue({
data() {
return {
msg: '这是MyVue的实例'
}
},
methods: {
getMsg() {
return 'getMsg: ' + this.msg
}
}
})
console.log(myVm.msg) // 这是MyVue的实例
console.log(myVm.getMsg()) // getMsg: 这是MyVue的实例