本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
本文是原文学习后的笔记和总结. 望对其他伙伴研读源码有一定的帮助.
一、调试准备
1. 本地建个文件夹作为工作目录, 新建个index.html, 内容如下:
<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>
这种方式非常适合快速研读源码, 之前我都是用vue的源码工程开始, 要研究一系列编译问题、依赖安装, 还要受flow等语法困扰,还有sourcemap映射问题。这些东西让蛮多人望而却步。非常赞赏作者的研究方式。
2.安装live server vscode插件
我喜欢用这个插件调试本地html页面,这个插件支持代码改动热更新。会提高调试效率。在html页面代码区右键点击“open with live server” 就可以开始代码调试了
3. 开始调试代码
打开chrome调试器的source面板,在 const vm = new Vue({ 这行断点。刷新页面,就可以开始了。作者提供的代码非常简单,避免受到其它代码的干扰。
二、记录调试过程
vue构造函数
断点处F11跟进去来到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来调用的构造函数,不是用new调用的话,后续代码无法正常运行。
_init函数
这个函数有点长,为了避免进入代码地狱,快速搞清楚今天的学习内容。我们可以根据函数命名大体猜测功能再加上断点查看this状况快速定位
- 没运行initState前:
可以看到两个都没值
- 运行后:
可以看到都有了值
可以看出是 initState(vm);这里面实现的data和methods的代理。
initState
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
// 处理props选项
if (opts.props) { initProps(vm, opts.props); }
// 配置了methods就处理初始化这些方法
if (opts.methods) { initMethods(vm, opts.methods); }
// 有配置data选项就初始化 data
if (opts.data) {
initData(vm);
// 没有配置data, 就用个空对象处理
} else {
observe(vm._data = {}, true /* asRootData */);
}
if (opts.computed) { initComputed(vm, opts.computed); }
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
在这个方法里可以看到 if (opts.methods) { initMethods(vm, opts.methods); } 从命名可以看出是操作methods的。那先到 initMethods 看看
initMethods
贴上代码好分析
function initMethods (vm, methods) {
// 取到props的定义,后面判断冲突要用
var props = vm.$options.props;
// 遍历methods数组
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
);
}
// 和props里的key重名了,提醒用户不能冲突重名
if (props && hasOwn(props, key)) {
warn(
("Method \"" + key + "\" has already been defined as a prop."),
vm
);
}
// 判断当前的方法名在vue实例上是否存在,若存在且是vue的保留命名(以_ 或 $ 开头),提醒用户使用错误。
if ((key in vm) && isReserved(key)) {
warn(
"Method \"" + key + "\" conflicts with an existing Vue instance method. " +
"Avoid defining component methods that start with _ or $."
);
}
}
// 不是函数就给个默认无操作函数
// 是函数要绑作用域到vue实例
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
}
}
这里可以看出把绑定作用域后的函数赋值给了vue实例。 这个函数还是很简单的。但这bind写了个兼容处理,一起看看:
// 用apply和call模拟bind
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;
initData
代码有点长, 用注释帮助理解
function initData (vm) {
var data = vm.$options.data;
// 看选项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];
// 这里有个大括号什么目的?
{
// 判断数据key是否和方法名冲突, 冲突给出提醒
if (methods && hasOwn(methods, key)) {
warn(
("Method \"" + key + "\" has already been defined as a data property."),
vm
);
}
}
// 数据key和props的key冲突给出提醒
if (props && hasOwn(props, key)) {
warn(
"The data property \"" + key + "\" is already declared as a prop. " +
"Use prop default value instead.",
vm
);
// 不是vue保留开头的就代理这个数据属性, 后面分析proxy
} else if (!isReserved(key)) {
proxy(vm, "_data", key);
}
}
// 观察数据,使其有响应能力
// observe data
observe(data, true /* asRootData */);
}
proxy
先看源码:
/**
* Perform no operation.
* Stubbing args to make Flow happy without leaving useless transpiled code
* with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
*/
function noop (a, b, c) {}
// 多次代码公用代理对象
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};
// 使用 Object.defineProperty 代理数据属性到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);
}
这里把key通过sharedPropertyDefinition公共对象代理vm上. 在使用this.xxx时,实际上是访问的this._data.xxx
属性的描述符property有以下选项:
value——当试图获取属性时所返回的值。
writable——该属性是否可写。
enumerable——该属性是否出现在对象的枚举属性中, 即:该属性在for in循环中是否会被枚举。
configurable——当且仅当该属性为 true 时,该属性的描述符才能够被改变,同时该属性才能从对应的对象上被删除。默认为false。
set()——该属性的更新操作所调用的函数。
get()——获取属性值时所调用的函数。执行时不传入任何参数,但是会传入 this 对象
总结
分析代码时, 还是有其它相关代码想深入分析的, 但考虑避免进入代码地狱, 深陷其中, 暂且分析这些. 日后再做更深入分析.
回忆下this.xxx能访问到data里的xxx的原因: data的属性会被放到this._data上. 通过定义描述符的getter和setter来代理到this上
this.xxx可以访问methods里的方法的原因是通过bind方法把函数的作用域绑定到vue实例this上,然后赋值this.