本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
学习目标
- 学习调试 vue2 源码
- data 中的数据为什么可以用 this 直接获取到
- methods 中的方法为什么可以用 this 直接获取到
- 学习源码中优秀代码和思想,投入到自己的项目中
示例
let vm = new Vue({
data() {
return {
name: 'bss'
}
},
methods: {
show() {
console.log(this.name)
}
}
})
console.log(vm.name)
console.log(vm.show())
调试方案
- 安装
http-server包 用于搭建本地服务
npm i http-server -g
- 新建
index.html引入vue.js并添加示例代码
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
<script>
let vm = new Vue({
data() {
return {
name: 'bss'
}
},
methods: {
show() {
console.log(this.name)
}
}
})
console.log(vm.name)
console.log(vm.show())
</script>
- 进入
index.html所在目录 运行如下命令
http-server .
更换端口
http-server -p 8081 .
- 此时打开
http://localhost:8080/就可以看到index.html页面了 - 打开浏览器的开发者工具 -> source -> 在
new Vue()这里打上断点, 如图:
6. 刷新页面 执行调试 下面为常用调试按钮(更全调试技巧)
源码解析
1. Vue函数定义
function Vue (options) {
if (!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
利用instanceof判断是否用new调用
是的话执行示例方法_init初始化Vue示例
否则给出警告
2. _init方法初始化
大致的内容如下
其他初始化内容可略过,大致包含实例的属性、生命周期、事件、渲染、调用生命周期及各种数据源
-
vm被赋值为当前实例
vm = this -
注意
options参数的处理整合实例vm的构造函数、options及实例vm的属性并挂在
vm.$options上 -
关注
initState(vm)
3. initState(vm)
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);
}
}
- 获取
vm.$options - 初始化
props - 初始化
methods - 初始化
data并做数据监听observe()
// data是否为空都会执行observe
observe(data ? data : vm._data, true /* asRootData */)
- 初始化
computed - 初始化
watch
4. initMethods(vm, opts.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中存在重复的method的key给出警告 -
method的key与Vue实例的保留属性重名给出警告 -
判断
method的值是函数则改变this指向 通过
bind函数返回一个新函数 并赋值给vm对应的key否则vm对应的key赋值为空
hasOwn
var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {
return hasOwnProperty.call(obj, key)
}
// 示例
var obj = {
p: 123
};
obj.hasOwnProperty('p') // true
isReserved 是否为保留属性
// 检查字符串是否以$或_开头
function isReserved (str) {
var c = (str + '').charCodeAt(0);
return c === 0x24 || c === 0x5F
}
bind绑定this
function polyfillBind (fn, ctx) {
function boundFn (a) {
var l = arguments.length; // vm的method的参数个数
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;
5. initData(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 */);
}
- 将data转为对象并更新到
vm._data - 如果不是普通对象则给出警告
- 遍历判断data的key是否与
methods、props的key重名 重名则给出警告 - 不是保留属性则进行代理
proxy - 最后对data进行监听
observe
data处理
function getData (data, vm) {
pushTarget();
try {
// 绑定上下文为vm data函数的参数为vm 执行结果返回
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, "data()");
// 返回空对象
return {}
} finally {
popTarget();
}
}
var data = vm.$options.data;
// 如果data是函数则getData()返回对象 否则直接返回data
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {};
由以上分析可以得知,我们在data()函数可以取到methods并执行(因为methods的初始化在data之前)例如:
let vm = new Vue({
data(vm) {
return {
age: vm.showAge()
}
},
methods: {
show() {
console.log(this.age)
},
showAge() {
return 28
}
}
})
console.log(vm.show())
proxy代理
实现通过this.data[key]真正访问的是this._data[key]
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);
}
defineProperty
Object.defineProperty(obj, prop, descriptor):通过描述对象,定义某个属性。Object.getOwnPropertyDescriptor():获取某个属性的描述对象。Object.defineProperties():通过描述对象,定义多个属性。
descriptor包含的属性如下:
value——当试图获取属性时所返回的值。
writable——该属性是否可写。
enumerable——该属性在for in循环中是否会被枚举。
configurable——该属性是否可被删除。
set()——该属性的更新操作所调用的函数。
get()——获取属性值时所调用的函数。
// 使用示例
var obj = {};
Object.defineProperties(obj, {
'property1': {
value: true,
writable: true
},
'property2': {
value: 'Hello',
writable: false
}
});
const descriptor1 = Object.getOwnPropertyDescriptor(obj, 'property1');
console.log(descriptor1)
observe后续再做分析 此处暂时忽略
手动编写代码实现示例效果
function noop (a, b, c) {}
let 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);
}
function Vue1(options) {
const vm = this
vm.$options = options
initState(vm)
}
const initState = function(vm) {
initMethods(vm)
initData(vm)
}
const initMethods = function(vm) {
const methods = vm.$options.methods
for(key in methods) {
vm[key] = typeof methods[key] !== 'function' ? noop : methods[key].bind(vm)
}
}
const initData = function(vm) {
let data = vm.$options.data
data = vm._data = typeof data === 'function' ? data.call(vm, vm) : data || {}
let keys = Object.keys(data);
let i = keys.length
while(i--) {
let key = keys[i]
proxy(vm, "_data", key);
}
}
将开头的示例改为:
data(vm) {
return {
name: 'bss',
age: vm.showAge()
}
},
methods: {
show() {
console.log(this.name) // bss
console.log(this.age) // 28
},
showAge() {
return 28
}
}
})
console.log(vm.name) // bss
console.log(vm.show())
ok 正常打印 实现完成✌️
总结
- 要灵活掌握
this的使用和Object.defineProperty - 源码的阅读是对提升编码的有效途径之一,学习不在于急于求成,而在于细嚼慢咽。
- 源码听起来好像很难,但是如果你真的静下心来看,一点点的去打通,再难的问题都不是问题。
- 一个大的项目,要学会拆解学习,分成小项目,阶段性完成,这样就会比较容易,最终实现大的目标。