本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
环境准备
-
Vue2 最新的 2.7 版本涉及到V3的新语法,我们直接下载2.6分支下的代码即可,代码下载
https://github.com/vuejs/vue/blob/2.6/dist/vue.js -
新建一个 index.html 引入上面 js,并新建个 Vue 应用。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> <script src="./dist/vue.js"></script> </head> <body> <div id="app">{{msg}}</div> <script> let App = new Vue({ el: "#app", data: { msg: "hello world", }, }); </script> </body> </html> -
打上断点。根据上面代码可知,
new Vue({...})是我们代码入口,我们打开 Vue.js 5088行,可以加上断点。
Vue 的 _init 方法
当前方法挂载在Vue的原型上,接受一个参数,也就是我们创建Vue应用传入的参数,其中我们主要先看一下 initState 里面包含了 props methods data computed watch 的初始化和挂载。
Vue.prototype._init = function (options) {
// 当前 this 也就是当前 Vue 实例
var vm = this;
// a uid
vm._uid = uid$3++;
var startTag, endTag;
/* 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
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
// 初始化 props methods data computed watch
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);
}
};
initState
主要实现的功能
- initProps 初始化 props
- initMethods 初始化 methods
- initData、observe 数据监听
- initComputed 始化 computed 计算属性
- initWatch 初始化 watch 监听
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);
}
}
我们重点看 initMethods initData
initMethods
先判断 methods 中的属性是否是函数,不是则打印错误日志
判断 methods 中的属性名在 props 中是否已定义,已存在的话,打印出错误日志
判断如果方法名已存在在实例并且以 _ 或者 $ 开头,打印错误日志
将方法挂载到 vm 上,并更换执行时候的 this 绑定,绑定成当前的 Vue 实例 "vm"
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);
}
}
关于 bind、call、apply,三个方法都可以更改执行时候的 this,区别在于
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
function.bind(thisArg[, arg1[, arg2[, ...]]])
apply() 方法调用一个具有给定 `this` 值的函数,以及以一个数组或一个[类数组对象]的形式提供的参数。
call() 方法使用一个指定的 `this` 值和单独给出的一个或多个参数来调用一个函数。
initData
data 数据初始化
判断 data 是否是一个函数,是的话通过 getData 来获取数据,否则直接取对应 data 数据
判断 data 是否是一个普通对象,否则打印报错日志
判断 data 中定义的属性是否在 props、methods 中已存在,存在的话打印报错日志
data 值挂载到 vm
数据监听,处理成响应式
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 */);
}
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();
}
}
isReserved
判断是否是内部变量, _ 开头或者 $ 开头
function isReserved (str) {
var c = (str + '').charCodeAt(0);
return c === 0x24 || c === 0x5F
}
isReserved("_data") // true
isReserved("$data") // true
isReserved("data") // false
isReserved("props") // false
proxy 数据代理
访问 this.keyA 实际是访问 this._data.keyA
访问 this.keyB 实际是访问 this._props.keyB
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 定义一个新属性
Object.defineProperty(obj, prop, descriptor) 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。obj: 要定义属性的对象。 prop:要定义或修改的属性的名称或 Symbol。descriptor:属性描述符
Object.defineProperty(obj, prop, {
// 该属性对应的值
value: "",
// 该属性为true,上面的value才会可以修改,默认值是 false
writable: true,
// 可枚举 默认为 false
enumerable: true,
// 可配置 默认为 false
configurable: true,
// 获取value,会执行当前函数
get: noop,
// 设置value,会执行当前函数
set: noop
})
let obj = { name: "tom" }
Object.defineProperty(obj, 'ext1', {
value:"扩展字段",
})
obj.ext1 = "12" // 修改会失败
for(let key in obj) console.log(key) // ext1 无法被枚举
Object.defineProperty(obj, 'ext3', {
get(){
console.log('ext3 get')
return obj.ext3
},
set(val){
obj.ext3 = val;
}
})
obj.ext3 // undefined
obj.ext3 = "hello"
obj.ext3 // hello
总结
由上面可知其实 this 中可以访问到 data、methods数据,其实就是通过 proxy 方法进行数据代理访问的。